19
.
03
.
2025
13
.
03
.
2025
Backend

Migration from Heroku using Kamal

Jarosław Kowalewski
Ruby Developer
Jarosław Kowalewski - Migration from Heroku using Kamal

Recently, we started working on an MVP for a client. As with many new projects, we initially set it up on Heroku, which provides an easy-to-use PaaS for deploying and managing applications in the cloud. However, in the long run, Heroku can become quite expensive and, in some cases, challenging to manage. Anticipating future needs, we decided to migrate the application early in its development, especially since, at that stage, it was still relatively simple.

With the release of Rails 8, Kamal has become the default deployment tool for Rails applications, so we chose to leverage it for this migration. Of course, we also needed a server to host our application. Here, DigitalOcean Droplets came to the rescue, as we’ve already been using them for some of our other projects.

Another key decision in this migration was switching our database from PostgreSQL to SQLite. In recent years, SQLite has become a solid choice for small to medium-sized projects that can handle traffic with a single server. Additionally, Heroku’s ephemeral filesystem does not allow modifications to files outside the deployment process, which means SQLite isn’t an option on Heroku - forcing developers to use an external database service.

By making this migration early, we ensured greater flexibility, lower costs, and more control over our infrastructure while still keeping our deployment process simple with Kamal.

Create a DigitalOcean Droplet

The first step is to create a DigitalOcean Droplet. The full guide on setting it up is available here:

🔗 DigitalOcean Droplet Setup Guide

For our needs, we used the basic droplet configuration. If necessary, we can upgrade it later.

During the droplet creation process, we need to add the SSH key of the machine we’ll be deploying from. This allows us to:

  • Securely log in to the virtual machine
  • Properly configure deployment using Kamal

Once the droplet is set up correctly, we should be able to log in from the terminal:

ssh root@<server-ip>

Replacing Database Configuration

Since we’re switching our database from PostgreSQL to SQLite, we need to adjust our application’s configuration accordingly.

  • Replace the pg gem with the sqlite3 gem:
- gem "pg", "~> 1.1"
+ gem "sqlite3", ">= 2.1"
  • Update the database.yml file:
default: &default
  adapter: sqlite3
  encoding: unicode
  pool: <%= ENV.fetch("RAILS_MAX_THREADS") { 5 } %>

development:
  primary:
    <<: *default
    database: storage/development.sqlite3
  cache:
    <<: *default
    database: storage/development_cache.sqlite3
    migrations_paths: db/cache_migrate
  queue:
    <<: *default
    database: storage/development_queue.sqlite3
    migrations_paths: db/queue_migrate
  cable:
    <<: *default
    database: storage/development_cable.sqlite3
    migrations_paths: db/cable_migrate
test:
  <<: *default
  database: storage/test.sqlite3
production:
  primary:
    <<: *default
    database: storage/production.sqlite3
  cache:
    <<: *default
    database: storage/production_cache.sqlite3
    migrations_paths: db/cache_migrate
  queue:
    <<: *default
    database: storage/production_queue.sqlite3
    migrations_paths: db/queue_migrate
  cable:
    <<: *default
    database: storage/production_cable.sqlite3
    migrations_paths: db/cable_migrate

This database.yml file configures the SQLite databases for different environments (development, test, and production) in a multi-database setup.

  • The primary database stores the main application data.
  • Additional databases (cache, queue, and cable) are defined for caching, background jobs, and ActionCable, with separate migration paths.
  • In development and production, all databases are explicitly configured, while test only uses a single database.

For our case, that was it! 🚀 Since we could regenerate our data from seeds, no data migration was necessary.

Of course, if we had to migrate existing data or ensure data continuity, the process would be much more complex - but that’s a topic for another article. 😉

Kamal Configuration

Enter Kamal, our modern deployment tool. In previous Rails solutions, we relied on Capistrano for direct SSH deployments. Capistrano connected to servers via SSH, executed commands, and managed releases directly on the server. However, with Rails 8, Kamal takes a more modern approach, streamlining the process by using Docker to build and push container images to a registry before orchestrating the deployment on the server.

Getting started is simple - just add the kamal gem to the Gemfile. For new Rails projects, it’s already included by default. After that, install dependencies and initialize Kamal using kamal init. In new projects, this initialization happens automatically as part of the project setup.

Now, let’s configure Kamal to get our deployment running. The first step is setting up the config/deploy.yml file.

service: example
image: example/example
servers:
  web:
    hosts:
      - 12.123.123.12
proxy:
  app_port: 3000
  ssl: true
  hosts:
    - example.com
    - www.example.com
  healthcheck:
    interval: 2
registry:
  server: ghcr.io
  username: KAMAL_REGISTRY_USERNAME
  password:
    - KAMAL_REGISTRY_PASSWORD
volumes:
  - "/db_storage:/rails/storage"
builder:
  arch: amd64
env:
  clear:
    WEB_CONCURRENCY: 0
    RAILS_ENV: production
    HOST: example.com
  secret:
    - SECRET_KEY_BASE

This is a Kamal deployment configuration file (deploy.yml), which defines how the application is built, deployed, and managed on the server. Here's a breakdown of its key sections:

Service Configuration

  • service: example – Specifies the name of the application or service being deployed.
  • image: example/example – Defines the Docker image name used for deployment.

Servers

  • web: – Declares a group of servers (in this case, the web server).
  • hosts: – Lists the server IP addresses where the application will be deployed (12.123.123.12).

Volumes

  • The /db_storage:/rails/storage volume binds the local storage directory to the application's storage directory inside the container for persistent data storage.

Proxy Settings

  • app_port: 3000 – Specifies the port on which the application runs inside the container.
  • ssl: true – Indicates that SSL is enabled for secure connections.
  • hosts: – Defines the domain names that will be used (example.com, www.example.com).
  • healthcheck: – Configures a health check interval (2 seconds) to monitor the application's availability.

Registry Configuration

  • server: ghcr.io – Specifies the container registry (in this case, GitHub Container Registry).
  • username: and password: – Use environment variables (KAMAL_REGISTRY_USERNAME and KAMAL_REGISTRY_PASSWORD) to authenticate with the registry.

Build Configuration

  • arch: amd64 – Ensures that the application is built for AMD64 (x86_64) architecture.

Environment Variables

  • clear: – Defines environment variables that are publicly available:
    • WEB_CONCURRENCY: 0 – Controls the number of worker processes (defaulting to automatic scaling).
    • RAILS_ENV: production – Sets the Rails environment to production.
    • HOST: example.com – Defines the application host.
  • secret: – Contains sensitive environment variables that should be securely managed, such as SECRET_KEY_BASE.

Next one is .kamal/secrets :

KAMAL_REGISTRY_USERNAME=$KAMAL_REGISTRY_USERNAME
KAMAL_REGISTRY_PASSWORD=$KAMAL_REGISTRY_PASSWORD
SECRET_KEY_BASE=$SECRET_KEY_BASE

The .kamal/secrets file stores environment variables that are passed to the deployment process. Instead of storing direct values, we reference existing environment variables. This file should be safe to commit to the repository since and not contain sensitive data directly.

Instead of keeping secrets in .kamal/secrets, we should opt for environment variables managed with dotenv-rails. To use it, simply add it to the Gemfile and run bundle install. Another alternative is Rails credentials, which requires generating a master key and passing it to Kamal.

Here are lines added to .env file :

KAMAL_REGISTRY_USERNAME=username
KAMAL_REGISTRY_PASSWORD=password
SECRET_KEY_BASE=1234

The final step is setting up the Docker image. Since this article focuses primarily on Kamal, we won’t go into the details of Dockerfile configuration. The setup largely depends on the tools and versions required for a specific project.

For our needs, the default Rails-generated Docker configuration was sufficient in 95% of cases, requiring only minor adjustments when necessary.

Deployment time!

With all prerequisites in place, it’s time to deploy.

For the initial setup, run:

dotenv kamal setup

This ensures Kamal is properly configured and verifies that Docker is installed on the VM.

For subsequent deployments, use:

dotenv kamal deploy

The dotenv command is required when using environment variables. If you're relying on Rails credentials, you can omit it.

Now, sit back and enjoy a coffee ☕ - don’t be alarmed by red text in the logs. Unless you see ERROR, most messages are just warnings or general output.

Once deployment completes, your app should be accessible via the server’s IP address. Since we enabled SSL, the final step is updating your DNS records. Simply go to Cloudflare (or your domain provider) and point your domain to the new server IP.

Final thoughts

Moving from Heroku to DigitalOcean with Kamal turned out to be a solid decision. Heroku is great for quick setups, but costs can pile up fast, and you don’t have much control over the infrastructure. With Kamal, we now build, push, and deploy Docker images easily, keeping things lightweight and efficient.

Once we had deploy.yml configured and our secrets management in place, deployments became a breeze. Instead of relying on Heroku’s platform-specific tools, we now have full control over our server while keeping the deployment process just as simple.

In the end, this move gave us more flexibility, lower costs, and a straightforward deployment flow - without losing the ease of use we enjoyed with Heroku. 🚀

Jarosław Kowalewski
Ruby Developer

Check my Twitter

Check my Linkedin

Did you like it? 

Sign up To VIsuality newsletter

READ ALSO

Visuality Poznań is here!

14
.
11
.
2023
Michał Piórkowski
Visuality
Business
HR

Why we chose Ruby on Rails and React.js for our main technologies? (FAQ).

02
.
10
.
2024
Michał Krochecki
Ruby on Rails
Backend
Frontend
Visuality

Branding: How to style your Jira?

14
.
11
.
2023
Lukasz Jackiewicz
Tutorial
Design
Project Management

How to start your UX/UI designer career

14
.
11
.
2023
Bartłomiej Bednarski
Design
Tutorial
HR

WebUSB - Bridge between USB devices and web browsers

14
.
11
.
2023
Burak Aybar
Ruby on Rails
Frontend
Backend
Tutorial

Visuality comes to town - this time it's Poznań

14
.
11
.
2023
Michał Piórkowski
Visuality
HR

How to choose a software house.

14
.
11
.
2023
Michał Piórkowski
Ruby on Rails
Business
Visuality

CSS Modules in Rails

14
.
11
.
2023
Adam Król
Ruby on Rails
Tutorial
Backend
Frontend

JSON API versus the NIH syndrome

14
.
11
.
2023
Nadia Miętkiewicz
Backend
Frontend
Tutorial

From Idea to Concept

02
.
10
.
2024
Michał Krochecki
Ruby on Rails
Business
Startups

Styling React Components

14
.
11
.
2023
Umit Naimian
Ruby on Rails
Frontend
Tutorial

How good design can help your business grow

14
.
11
.
2023
Lukasz Jackiewicz
Design
Business
Marketing

TODO not. Do, or do not.

29
.
11
.
2023
Stanisław Zawadzki
Ruby on Rails
Software

CS Lessons #003: Density map in three ways

14
.
11
.
2023
Michał Młoźniak
Ruby
Backend
Tutorial
Software

Clean code for the win

14
.
11
.
2023
Michał Piórkowski
Ruby on Rails
Backend
Frontend
Business

Crowd-operated Christmas Lights

14
.
11
.
2023
Nadia Miętkiewicz
Ruby on Rails
Backend

How to startup and be mature about it

14
.
11
.
2023
Rafał Maliszewski
Ruby on Rails
Startups
Business

A journey of a thousand miles begins with workshops

14
.
11
.
2023
Michał Piórkowski
Software
HR

CS Lessons #002: Data structures

14
.
11
.
2023
Michał Młoźniak
Ruby
Software

Summary of Phoenix workshop at Visuality

14
.
11
.
2023
Karol Słuszniak
Ruby on Rails
Visuality
Backend

CS Lessons #001: Working with binary files

14
.
11
.
2023
Michał Młoźniak
Ruby
Software

CS Lessons #000: Introduction and motivation

14
.
11
.
2023
Michał Młoźniak
Ruby
Software

Working with 40-minute intervals

14
.
11
.
2023
Sakir Temel
Software
HR

THE MATURE TECH STARTUP DILEMMA: WHAT'S NEXT

14
.
11
.
2023
Susanna Romantsova
Startups