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

Project Quality in IT - How to Make Sure You Will Get What You Want?

02
.
10
.
2024
Wiktor De Witte
Ruby on Rails
Project Management
Business

5 Trends in HR Tech For 2021

14
.
11
.
2023
Maciej Zdunek
Business
Project Management

Is Go Language the Right Choice for Your Next Project?

14
.
11
.
2023
Maciej Zdunek
Backend
Business

SXSW Tradeshow 2020: Get Your FREE Tickets and Meet Us

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

How to build effective website: simplicity & McDonald's

14
.
11
.
2023
Lukasz Jackiewicz
Ruby on Rails
Frontend
Design

WebUSB - Print Image and Text in Thermal Printers

14
.
11
.
2023
Burak Aybar
Backend
Tutorial
Software

Thermal Printer Protocols for Image and Text

14
.
11
.
2023
Burak Aybar
Backend
Tutorial
Software

What happened in Visuality in 2019

14
.
11
.
2023
Maciej Zdunek
Visuality
HR

Three strategies that work in board games and in real life

14
.
11
.
2023
Michał Łęcicki
Ruby on Rails

HR Wave - No Bullshit HR Conference 2019

14
.
11
.
2023
Alicja Gruszczyk
HR
Conferences

Stress in Project Management

02
.
10
.
2024
Wiktor De Witte
HR
Project Management

Lightning Talks in your company

14
.
11
.
2023
Jarosław Kowalewski
Ruby on Rails
Visuality

How to find good developers and keep them happy - Part 1

02
.
10
.
2024
Michał Krochecki
HR
Visuality

PKP Intercity - Redesign and case study of polish national carrier

14
.
11
.
2023
Katarzyna Szewc
Design
Business
Frontend

Let’s prepare for GITEX Dubai together!

14
.
11
.
2023
Michał Piórkowski
Conferences
Business

Ruby Quirks

14
.
11
.
2023
Jan Matusz
Ruby on Rails
Ruby

Visuality recognized as one of the Best Ruby on Rails Devs

14
.
11
.
2023
Maciej Zdunek
Ruby on Rails
Visuality
Business