Hosting Rails apps for free on Oracle Cloud with Dokku

By Artem Avetisyan on April 29, 20229 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:

  • Postgres database
  • worker
  • custom domain
  • TLS certificate from Letsencrypt (with auto renewals)
  • periodic database backups to AWS S3
  • logging to external service (a la heroku log drains)

Free hardware

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

Dokku

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.

Install Dokku

Follow Dokku install guide.

Don’t use sslip.io in dokku domains:set-global because letsencrypt doesn’t seem to be working for sslip.io based vhosts (at the time of this writing). Either use your own domain if you have a spare one, or simply skip set-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

Deploying to Dokku

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 using binfmt 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.

Docker image deploy

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.

Adding domain

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).

Setting up TLS

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

Serve assets from rails

dokku config:set RAILS_SERVE_STATIC_FILES=1

Console logs

dokku config:set RAILS_LOG_TO_STDOUT=true

Papertrail logs

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

Postgres backups

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

Worker

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)

Continuous deploy from Github

Use Dokku Github action to automatically deploy on push to Github.

Useful free services

This is by no means an exhaustive list, but I’ve used these ones and they are good.

Caveat

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.