By Artem Avetisyan on April 29, 2022 • 9 min read
We all know and love “git push” deploys popularized by Heroku some eons ego. Heroku also got us spoiled with a great CLI. But hosting on Heroku (or similar services) costs money. Which is fine for serious stuff, but sometimes you just want to host a side project where the only acceptable price is zero.
In this post we’ll setup a self-hosted Heroku like expirience on a free1 cloud vm. Using a Heroku like CLI, we will then deploy a Rails app featuring:
I recently learned about Oracle Cloud always free offer, so there’s our free server.
A free instance gets up to 3 OCPU cores (whataver that means), 6GB of RAM, 46GB of block storage and 10TB of outbound traffic. Pretty good. It’s worth pointing out that those riches are only available for Oracle Ampere Arm CPUs. This will present some challanges, but nothing unavoidable - all necessary workarounds are covered in the post.
For the purpose of this post I opted for Ubuntu image (simply because I am more familiar with it). The experience on the default Oracle Linux may or may not vary.
Once the vm is up, make sure passwordless ssh is configured (this is done either when an instance is created or later with ssh-copy-id
). Login to the instance and make sure packages are up to date:
sudo apt get update && sudo apt get upgrade
After the that’s done, we need to open http ports 443 and 80. This is done by adding an Ingress Rule in Security List of your instance’s Virtual Cloud Network (see details in the official guide).
For some reason the above didn’t actually open the ports, but the following extra steps did the trick:
sudo apt install firewalld sudo firewall-cmd --zone=public --permanent --add-port=80/tcp sudo firewall-cmd --zone=public --permanent --add-port=443/tcp sudo firewall-cmd --reload
Heroku experience is provided by Dokku - a self hosted PAAS. Dokku is a popular, mature project that is capable of more than just hosting pet projects. Having said that, Dokku runs on a single server, so there’s a natural limit to where it’s applicable.
Follow Dokku install guide.
Don’t use
sslip.io
indokku domains:set-global
because letsencrypt doesn’t seem to be working forsslip.io
based vhosts (at the time of this writing). Either use your own domain if you have a spare one, or simply skipset-global
part altogether. This way you won’t have automatic virtual hostnames (e.g.your-app-name.your-vhost.com
), but that’s not a show stopper and it can be enabled later at any point.
To be able to run Dokku commands from local machine, install a Dokku client:
brew install dokku/repo/dokku
Then export DOKKU_HOST
environment variable pointing to the server public IP.
Run some Dokku command to make sure it works:
❯ dokku apps:list
=====> My Apps
! You haven't deployed any applications yet
Your Rails app is likely to require Postgres, so we need to add a postgres plugin to Dokku. This is done with the following command on the Dokku host:
sudo dokku plugin:install https://github.com/dokku/dokku-postgres.git
Now back to your local machine, let’s create the app. In the app folder:
dokku apps:create rails-testing-post
For the database, we need to create a service an attach it to our app:
dokku postgres:create railsdatabase
This however fails on Ampere CPU with the following error:
❯ dokku postgres:create railsdatabase Waiting for container to be ready WARNING: The requested image's platform (linux/amd64) does not match the detected host platform (linux/arm64/v8) and no specific platform was requested standard_init_linux.go:228: exec user process caused: exec format error
A cursory glance on the Internet revealed a workaround (run this on the dokku host):
sudo docker run --privileged --rm tonistiigi/binfmt --install all
Note: this needs to be run every time the host machine reboots.
Now repeat the
dokku postgres:create
and it is going to succeed. Note, that usingbinfmt
might incur a performance hit on some tasks.
Link Postgres service to the app:
dokku postgres:link railsdatabase
At this point, we are ready to git push dokku master
.
However that’s failing due to Dokku version of Node (my test Rails app happens to require Node for Webpacker) being incompatible with the Ampere CPU architecture. We can workaround that by using Dockerfile
deployment. This isn’t the only alternative - see cloud native buildpacks - but that’s the one that worked for me.
Another reason to consider Dockerfile deploy is that it’s faster. For one, Docker cache skips bundle install
which takes an awful lot longer on a cloud VM, than locally.
By default Dokku attempts to autodetect the type of your app (e.g. Rails, Node). If, however, it encounters a Dockerfile
, it simply builds an image and then runs a container. This requires a couple of extra files in our project.
app.json
:
{
"name": "Rails testing post",
"scripts": {
"dokku": {
"predeploy": "bundle exec rails webpacker:compile",
"postdeploy": "bundle exec rails db:migrate"
}
}
}
Dockerfile
:
FROM ruby:2.7.5
RUN curl -fsSL https://deb.nodesource.com/setup_16.x | bash -
RUN apt-get update -qq && apt-get install -y nodejs
RUN npm install -g yarn
WORKDIR /app
ENV RAILS_ENV=production
ENV PORT=3000
ADD Gemfile* /app/
RUN bundle install
ADD package.json yarn.lock /app/
RUN yarn install
ADD . /app/
CMD bundle exec rails server
Now we can deploy the app:
git push dokku master
The web process might crash because it can’t get secret_key_base
from encrypted credentials. Just like on Heroku, we can use the CLI to set the necessary environment variable:
dokku config:set RAILS_MASTER_KEY=$(cat config/master.key)
Congrats, you’re up! But, unless you set up vhosts, the app is not yet accessible from the Internet.
dokku domains:add your-domain.dev
You need to add an A
record for your domain that points to the public IP address of your server (or to the vhost, if you created one).
Dokku provides a Letsencrypt plugin
Install it on the host:
sudo dokku plugin:install https://github.com/dokku/dokku-letsencrypt.git
Then locally:
dokku config:set --no-restart [email protected]
dokku letsencrypt:enable
To enable auto-renew (optional):
dokku letsencrypt:cron-job --add
dokku config:set RAILS_SERVE_STATIC_FILES=1
dokku config:set RAILS_LOG_TO_STDOUT=true
Papertrail is a good logging service with a free tier. Create a log destination and use it as an endpoint in the following command:
dokku logs:set vector-sink "papertrail://?endpoint=logs.papertrailapp.com:11111&encoding=text"
If this fails for whatever reason it does so silently. You can view vector logs to make sure the above command actually succeeded:
dokku logs:vector-logs
Dokku Postgres let’s you backup to AWS S3.
dokku postgres:backup-auth railsdatabase $AWS_ACCESS_KEY_ID $AWS_SECRET_ACCESS_KEY
Create S3 bucket railsdatabase-backup
. Then for one off backup:
dokku postgres:backup railsdatabase railsdatabase-backup
And to setup periodic backups:
dokku postgres:backup-schedule railsdatabase "0 3 * * *" railsdatabase_backup
This might give you the following error:
! Invalid flag provided, only '--use-iam' allowed
This is a very confusing message because it’s got nothing to do with the flags and everything to do with the fact that “0 3 * * *” is being expanded on its way over ssh. Or something along those lines. Anyway, you can run the same command on the remote host without an issue:
sudo dokku postgres:backup-schedule railsdatabase "0 3 * * *" railsdatabase_backup
In order to have more than just one web process, we need to add a Procfile
:
web: bundle exec rails server -p $PORT
worker: bundle exec rails runner 'sleep 1 while true' # dummy example
By default Dokku only scales web
process to 1, so we need to turn on the worker after deploy:
dokku ps:scale worker=1
Now we can see that it’s up:
❯ dokku ps:report
=====> rails-testing-post ps information
Deployed: true
Processes: 2
Ps can scale: true
Ps computed procfile path: Procfile
Ps global procfile path: Procfile
Ps procfile path:
Ps restart policy: on-failure:10
Restore: true
Running: true
Status web 1: running (CID: ce5a22a9afe)
Status worker 1: running (CID: e6ab6bac412)
Use Dokku Github action to automatically deploy on push to Github.
This is by no means an exhaustive list, but I’ve used these ones and they are good.
There are no free lunches. There are no free servers either. Yes, you can create an Oracle VM for free at (almost) any point, but, as it turned out, it can just as easily vanish at any point. My particular experience was during a week (!) long service disruption when the server got shut down and I wasn’t able to create another one. And then I found out that this happens all the time. I am guessing killing free servers is their coping strategy for when things start falling apart. It’s probably mentioned somewhere deep in the TOS, but I couldn’t find it. I hope Oracle will pull their stuff together (or at least make this very clear upfront). In the meantime, be warned.
But, perhaps, some things are worth paying for after all. I’ve been using Contabo for a couple of years now and I have NEVER had any issues. For a 5€/month I get a server with enough RAM/CPU/storage/bandwidth to comfortably host a slew of pet projects as well as two Minecraft servers. And no Ampere workarounds required.
There is a Reddit discussion of this post.