When Heroku first announced the end of free plans I knew I’d need to figure out where to move any projects soon, since spending $16-31 per side-project would be rough. They have since announced low-cost plans, but that would still run $13-19 per project. Comparing the prices to DigitalOcean, you can get two to three Droplets with 1GB of memory each for the same price. Moving on from Heroku was an easy decision based on pricing alone, even if I have been using it nearly as long as I’ve been a Rails developer.
The next step was deciding how to manage the infrastructure and deploy applications. And while solutions like Kubernetes, Terraform, and the like are the go-to for a lot of people, it seemed like a lot to learn and manage for personal projects.
Instead I explored Platform-as-a-Service, or PaaS, options to keep a similar feel to Heroku. I chose Dokku in the end because it’s been around a while, it’s marketed as a mini-Heroku, and it satisfies all the requirements for applications I run. The one pitfall is it’s not built for running separate servers in a cluster, but I’ll cross that path if I ever have the need. Although, you can run the database services on separate services if needed, which should provide plenty of scaling capabilities.
Installing Dokku
First we need to create a Droplet on DigitalOcean. I’m using Ubuntu 22.04 and Dokku recommends at least 1GB of memory. Once the Droplet is live you should be able to SSH in to get started installing Dokku.
Note: Even with 1GB of memory, I’d still recommend creating a swap file since I ran into build issues with Bundler due to limited free memory with the services running.
Install Dokku on the Droplet by following the official installation instructions, which will take around 5-10 minutes to complete.
After the installation completes you can copy your local SSH key to add it as an administrator in Dokku.
Add your SSH key as an administrator to Dokku on the server.
Creating an Application
For the rest of this article I’m going to be setting up an instance of Miroha, a personal project of mine and the first project I migrated to use Dokku. If you’re setting up a project of your own, be sure to update the naming for each command.
Warning: Miroha switched to using Docker for deployment to prepare for using Kamal in production. If you’d like follow along using it, you need to clone this commit.
First on the server we’ll create the application.
And while we’re still on the server via SSH, we can install the plug-ins we’re going to use since they have to run with sudo
.
We shouldn’t need to SSH into the server anymore, so to continue the setup we’re going to install the official client. It’s a convenience wrapper around running remote commands with SSH, which may be your solution if you’re not on macOS.
By default the client looks for a dokku
remote in the Git repository, so we can add that next. You can customize the name via DOKKU_GIT_REMOTE
which can be helpful if you are running more than one instance, such as staging and production. See brew info dokku
for all the options.
We can verify our local configuration and the application remotely by retrieving a report for the application. The information may look different compared to the example below, but you are good to go as long as it succeeds.
Creating the Databases
We already added the PostgreSQL and Redis plug-ins on the server, so it’s one command to create the database and one to link it to the application. To see all of the commands for each plug-in you can run dokku postgres
or dokku redis
.
Since you are running all the services on a single instance you may want to adjust the Redis memory settings. You can change the memory limit to 32 megabytes and the policy to evict the least frequently used key. To do so run the redis:connect
command with the database name and run the appropriate Redis commands. See the default redis.conf for more details on each setting.
For other Redis commands and settings see the official Redis documentation.
Preparing for Deployment
We’re almost ready for our first deployment, but there’s a bit of configuration we need to get through first.
Setting the Default Branch
If you’re using main
or another branch name, you’ll want to change the deploy branch in Dokku. For a staging instance, I change it to be a staging
branch to be more explicit when pushing.
Adding Buildpacks
If you are sticking with the default builder, Herokuish, then we need to add a couple of buildpacks to build the application.
Note that while the Ruby buildpack does have Node.js available, it doesn’t appear to cache the Yarn dependencies at the time of writing and results in a much slower deployment. And if you add the Node.js buildpack the Ruby buildpack will still install the dependencies a second time unless you disable it. Miroha uses an environment variable to alter the task.
Setting the Master Key
Next we need to set the RAILS_MASTER_KEY
environment variable to ensure the application can boot. The --no-restart
option argument is helpful to avoid restarting the application when changing configuration.
Adding a Domain
Adding a domain is as simple as Heroku, but we need to remove the default local domain. I remove the local domain due to running into issues with adding SSL, which we’ll get to next.
You’ll want to update the DNS for the domain to point to DROPLET_IP_ADDRESS
, if you don’t the SSL certificate will fail.
Enabling Automatic SSL
To use the Let’s Encrypt plug-in, you first need to set an e-mail address for requested certificates with.
With the e-mail set, we can enable the plug-in and add the automatic renewal job. See dokku-letsencrypt for more details.
Tip: Don’t forget to enforce SSL in production. See the ActionDispatch::SSL documentation.
Scaling Processes
The last step to deploy is to scale up our clock and web processes so they will start after deployment.
Deploying
To deploy the application it’s as simple as pushing to Heroku. Be sure to use the correct remote and branch names, if you picked different versions.
If you don’t have a release
process set up you’ll need to migrate and seed your database as needed. Miroha uses a release script to automatically run migrations and allow it to be reset via an environment variable after each deployment.
If you don’t see any error during the push or database migration, you should see the application live at the domain you added. Congratulations!
Bonus Improvements
Enable YJIT
The new Ruby JIT compiler, YJIT, offers substantial speed improvements over the default compiler. The Ruby buildpack has built-in support, so we can enable it by adding a RUBY_YJIT_ENABLE
environment variable.
You may want to run dokku ps:rebuild
to ensure the change is picked up. Now your application should have up to a 17% speedup and use less memory. Perhaps one of the easiest performance changes ever.
Automatic Deployment with GitHub Actions
If you’re looking to automatically deploy when you merge on GitHub, check out the official GitHub Action with the simple example being the easiest place to get started. If you have a staging branch, you may want to consider enabling force push and adding a post-deploy script to reset the database.
Switching to Docker
If you’d prefer to switch from buildpacks to Docker, you’ll need to clear the buildpacks and the ports in the application. And if you’re using a RAILS_MASTER_KEY
you’ll need to add it as a build argument.
See the Dockerfile Deployment documentation for more information.