Deploying Ruby on Rails with Kamal on DigitalOcean: A Step-by-Step Guide

Deploying Ruby on Rails with Kamal on DigitalOcean: A Step-by-Step Guide

Deploying a Ruby on Rails application to production requires a robust, scalable, and secure infrastructure. While traditional methods involving manual server configuration and service management have their place, modern development workflows demand more efficient and automated solutions. This tutorial introduces Kamal, a modern deployment tool designed for containerized applications, and demonstrates how to leverage it for deploying a Ruby on Rails application onto DigitalOcean infrastructure. We will cover everything from server provisioning to the final deployment, ensuring you have a clear, actionable path to production.

Kamal, which stands for 'Kamal Automated Machine Learning,' is an open-source deployment tool developed by the engineers behind Rails. It simplifies the process of deploying web applications by orchestrating Docker containers, managing rolling updates, handling rollbacks, and integrating with reverse proxies like Traefik for SSL termination and load balancing. By abstracting away much of the complexity associated with container orchestration, Kamal empowers developers to focus on building features rather than wrestling with deployment intricacies.

DigitalOcean offers a compelling environment for hosting such applications. Its straightforward pricing, reliable infrastructure, and user-friendly control panel make it an accessible choice for developers of all levels. For containerized deployments, DigitalOcean's Droplets (virtual machines) provide the necessary compute resources, and their robust networking capabilities ensure smooth traffic management. Integrating Kamal with DigitalOcean allows for a seamless deployment pipeline, from the initial server setup to the live application serving traffic.

This guide is structured to provide a comprehensive, hands-on experience. We will walk through setting up your DigitalOcean environment, configuring Kamal, preparing your Rails application, and executing the deployment. Expect detailed command-line examples and configuration snippets throughout.

Prerequisites

Before embarking on this deployment journey, ensure you have the following prerequisites in place:

  • A DigitalOcean Account: You'll need an active DigitalOcean account to provision servers.
  • SSH Keys: Ensure you have generated SSH key pairs and added your public key to your DigitalOcean account for secure server access.
  • Local Development Environment: A local machine with Ruby, Rails, Docker, and Docker Compose installed.
  • Kamal Installed Locally: Install Kamal by adding the `kamal` gem to your application's `Gemfile` or by installing it as a standalone executable. For this tutorial, we assume it's installed as a gem.
  • A Ruby on Rails Application: A functional Rails application that you intend to deploy. This application should be capable of running within a Docker container.
  • Dockerfile: A `Dockerfile` in your Rails project's root directory to containerize your application.
DevOps Pro Tip: For production readiness, ensure your Dockerfile is optimized for small image sizes and fast build times. Utilize multi-stage builds to separate build dependencies from runtime dependencies.

Server Provisioning on DigitalOcean

The first step is to provision the necessary infrastructure on DigitalOcean. We will set up a few Droplets: one for the application deployment (which will run Kamal) and at least one (ideally more for high availability) for the actual application containers and Traefik. For simplicity in this guide, we'll provision one Droplet to act as the Kamal execution host and another for Traefik and application containers. In a real-world scenario, you'd want a more sophisticated setup, potentially using DigitalOcean's Managed Kubernetes or a dedicated Droplet for Traefik with load balancing.

1. Create a Droplet for Kamal Execution

This Droplet will be responsible for running Kamal commands. It needs to have Docker installed and sufficient permissions to manage other Docker containers.

  1. Log in to your DigitalOcean dashboard.
  2. Navigate to the "Droplets" section and click "Create Droplet".
  3. Choose an Image: Select a recent Ubuntu LTS version (e.g., Ubuntu 22.04 LTS).
  4. Choose a Plan: A basic plan (e.g., $12/month or higher) should suffice for the Kamal execution host, depending on your project's complexity and build times.
  5. Choose a Datacenter Region: Select a region close to your users.
  6. Authentication: Select "SSH keys" and ensure your public key is selected.
  7. Choose a Hostname: A descriptive name like `kamal-deployer`.
  8. Click "Create Droplet".

Once the Droplet is created, note its IP address. You'll need to SSH into it.

ssh root@[KAMAL_DEPLOYER_IP_ADDRESS]

On the `kamal-deployer` Droplet, install Docker and Docker Compose:

sudo apt update sudo apt install -y docker.io docker-compose sudo systemctl start docker sudo systemctl enable docker sudo usermod -aG docker $USER

You'll need to log out and log back in for the group changes to take effect.

exit ssh root@[KAMAL_DEPLOYER_IP_ADDRESS]

Verify Docker installation:

docker --version docker-compose --version

2. Create Droplets for Application and Traefik

For this tutorial, we will assume a single Droplet will host both Traefik (as a reverse proxy) and your Rails application containers. In a production environment, it's highly recommended to separate Traefik onto its own Droplet(s) or use DigitalOcean's Load Balancer service in conjunction with Traefik. For simplicity, we'll use a slightly more powerful Droplet for this combined role.

  1. Navigate back to "Create Droplet" on DigitalOcean.
  2. Choose an Image: Ubuntu 22.04 LTS.
  3. Choose a Plan: Select a plan that can accommodate your application's expected load and Traefik's resource needs. Start with a general-purpose plan (e.g., 2 CPU, 4GB RAM).
  4. Choose a Datacenter Region: Same region as the `kamal-deployer`.
  5. Authentication: SSH keys.
  6. Choose a Hostname: e.g., `app-server-01`.
  7. Click "Create Droplet".

Note the IP address of this new Droplet. Let's call this `[APP_SERVER_IP_ADDRESS]` going forward.

On the `app-server-01` Droplet, you will also need Docker and Docker Compose installed, similar to the `kamal-deployer` Droplet. Ensure the user you'll be deploying as has permission to run Docker commands (add to the `docker` group).

ssh root@[APP_SERVER_IP_ADDRESS] sudo apt update sudo apt install -y docker.io docker-compose sudo systemctl start docker sudo systemctl enable docker sudo usermod -aG docker $USER exit ssh root@[APP_SERVER_IP_ADDRESS]

3. Configure SSH Access

Kamal needs to SSH into your application server(s) to deploy containers. Ensure your SSH keys are set up correctly. The public key from the `kamal-deployer` Droplet needs to be added to the `~/.ssh/authorized_keys` file on the `app-server-01` Droplet for the user you will use for deployment. For simplicity, we'll use the `root` user for this tutorial, but in production, it's best practice to create a dedicated deployment user with appropriate sudo privileges.

On your local machine, copy the public key from your `kamal-deployer` Droplet:

# On your LOCAL machine cat ~/.ssh/id_rsa.pub

Then, SSH into your `app-server-01` Droplet and add this key to the authorized keys file for the user you'll use for deployment (e.g., root):

# On your APP SERVER sudo nano /root/.ssh/authorized_keys # Paste your public key here and save.

Test the SSH connection from your `kamal-deployer` Droplet to your `app-server-01` Droplet:

# On your KAMAL DEPLOYER Droplet ssh [APP_SERVER_IP_ADDRESS]

You should be able to SSH without a password. If not, revisit your SSH key setup.

Step-by-Step Configuration

1. Prepare Your Rails Application

a. Dockerfile

Ensure your Rails application has a `Dockerfile` in its root directory. Here's a typical example:

# Use an official Ruby runtime as a parent image FROM ruby:3.1.2 # Set the working directory in the container WORKDIR /myapp # Install essential packages RUN apt-get update -qq && apt-get install -y build-essential libpq-dev nodejs npm && rm -rf /var/lib/apt/lists/* # Copy the Gemfile and Gemfile.lock to the working directory COPY Gemfile Gemfile.lock ./ # Install gems RUN bundle install --jobs $(nproc) --retry 3 # Copy the rest of the application code COPY . . # Precompile assets (if using Rails assets) RUN bundle exec rails assets:precompile # Expose port 3000 to the outside world EXPOSE 3000 # Define environment variable ENV RAILS_ENV=production ENV SECRET_KEY_BASE=development # Run the application using Puma CMD ["bundle", "exec", "puma", "-C", "config/puma.rb"]

Note: Adjust `ruby:3.1.2` to your application's Ruby version. Ensure `config/puma.rb` is configured for production use.

b. Docker Compose (Optional but Recommended for Local Testing)

While Kamal handles container orchestration, having a `docker-compose.yml` locally can be beneficial for testing your Docker image before deployment.

version: '3.8' services:   app:     build: .     ports:       - "3000:3000"     environment:       RAILS_ENV: development       SECRET_KEY_BASE: testkey     volumes:       - .:/myapp       - bundle_cache:/usr/local/bundle volumes:   bundle_cache:

c. Container Registry

Kamal needs a place to store your Docker images. A container registry is essential. Options include Docker Hub, GitHub Container Registry (GHCR), or a private registry. For this guide, we'll assume you're using Docker Hub.

You'll need to create a repository on Docker Hub (e.g., `your-dockerhub-username/your-app-name`).

2. Configure Kamal

Kamal uses a `deploy.yml` file (or similar name) to define your deployment configuration. Create this file in the root of your Rails application.

# deploy.yml require 'yaml' set :domain, '[APP_SERVER_IP_ADDRESS]' set :repo_url, 'git@github.com:your-username/your-repo.git' # Your Git repository URL set :branch, 'main' # Or your deployment branch # Optional: Override default Dockerfile path # set :dockerfile_path, 'Dockerfile' # The name of your Docker image set :image_name, 'your-dockerhub-username/your-app-name' # The tag to use for the image set :tag, ENV['CI_COMMIT_SHA'] || `git rev-parse HEAD`.strip # Use commit SHA for traceability # Deployment user and host(s) set :deploy_user, 'root' set :servers, [   { host: fetch(:domain), user: fetch(:deploy_user), roles: %w(app) } ] # Directory on the server where the application will be deployed set :deploy_to, '/home/deploy/app' # Environment variables for your application # These will be available to your application containers set :env, {   RAILS_ENV: 'production',   SECRET_KEY_BASE: 'your_production_secret_key_base',   DATABASE_URL: 'postgresql://user:password@host:port/database', # Example DB URL   # Add any other necessary environment variables here } # Traefik configuration # This assumes you are using Traefik as your reverse proxy. # Kamal will manage Traefik configuration for your application. set :traefik, {   # Define the entry points for Traefik   entrypoints: ['web', 'websecure'],   # Define the dashboard configuration (optional)   dashboard: {     enabled: true,     api: {       dashboard: true,       debug: true     }   },   # Define routers and services   routers: {     'app-router' => {       rule: 'Host(`your-domain.com`)', # Replace with your actual domain       service: 'app',       entrypoints: ['websecure'],       tls: {         certresolver: 'letsencrypt' # Name of your certresolver in Traefik config       }     }   },   # Define services for Traefik   services: {     app: {       loadbalancer: {         servers: [{ url: 'http://[APP_SERVER_IP_ADDRESS]:3000' }] # Your app's internal URL and port       }     }   } } # Optional: Configure additional services like databases, cache, etc. # Kamal can manage these as separate Docker containers. # Example for a PostgreSQL database: # set :postgresql, { #   image: 'postgres:14', #   port: '5432:5432', #   volumes: ['postgres_data:/var/lib/postgresql/data'], #   env: { #     POSTGRES_DB: 'your_database_name', #     POSTGRES_USER: 'your_database_user', #     POSTGRES_PASSWORD: 'your_database_password' #   } # } # Specify volumes if needed set :volumes, [   'postgres_data:/var/lib/postgresql/data' # Example for PostgreSQL ] # Optional: Configure Traefik to use Let's Encrypt for SSL # This requires Traefik to be configured to use Let's Encrypt with a certresolver named 'letsencrypt'. # set :letsencrypt_email, 'your-email@example.com' # set :letsencrypt_production, true # Before deployment hooks before_hook do   # Commands to run before deploying end # After deployment hooks after_hook do   # Commands to run after deploying end

Key Configuration Points:

  • `domain`: The IP address of your application server.
  • `repo_url`: The URL of your Git repository.
  • `deploy_user`: The user Kamal will use to SSH into the server.
  • `servers`: Defines your deployment targets. `roles: %w(app)` indicates this server will host the application containers.
  • `deploy_to`: The directory on the remote server where your application code and Docker volumes will reside.
  • `env`: Crucial for your application's configuration. Ensure `SECRET_KEY_BASE` and `DATABASE_URL` (or relevant database connection details) are correctly set.
  • `traefik`: Kamal integrates with Traefik. This section defines how Traefik will route traffic to your application. Ensure `your-domain.com` and `letsencrypt` certresolver are correctly configured in your Traefik setup.

3. Set up Traefik

Kamal can deploy Traefik as part of your application stack. However, for a robust setup, it's often better to have Traefik running independently. If you're managing Traefik separately, ensure it's configured to discover and route to your application containers. For this tutorial, we'll assume Kamal will manage Traefik by defining it in `deploy.yml` as shown above.

If you are managing Traefik separately, you'll need a `docker-compose.yml` for Traefik itself on your `app-server-01` Droplet, and your `deploy.yml`'s `traefik` section would be simplified or removed, relying on Traefik's Docker discovery.

A minimal `docker-compose.yml` for Traefik, to be placed on your `app-server-01` Droplet:

# traefik-compose.yml version: '3.8' services:   traefik:     image: traefik:v2.10 # Use a specific version for stability     container_name: traefik     command:       - --api.insecure=true # Enable API dashboard without authentication (for testing)       - --providers.docker=true # Enable Docker provider       - --providers.docker.exposedbydefault=false # Do not expose containers by default       - --entrypoints.web.address=:80 # HTTP entrypoint       - --entrypoints.websecure.address=:443 # HTTPS entrypoint       # Let's Encrypt configuration (replace with your email)       - --certificatesresolvers.letsencrypt.acme.email=your-email@example.com       - --certificatesresolvers.letsencrypt.acme.storage=/letsencrypt/acme.json       - --certificatesresolvers.letsencrypt.acme.tlschallenge=true # Use TLS challenge     ports:       - "80:80"       - "443:443"       - "8080:8080" # Dashboard port     volumes:       - /var/run/docker.sock:/var/run/docker.sock:ro       - ./letsencrypt:/letsencrypt # Directory for Let's Encrypt certificates     labels:       - "traefik.enable=true"       - "traefik.http.routers.traefik.rule=Host(`traefik.your-domain.com`)" # Replace with your Traefik dashboard domain       - "traefik.http.routers.traefik.entrypoints=websecure"       - "traefik.http.routers.traefik.service=api@internal"       - "traefik.http.routers.traefik.tls.certresolver=letsencrypt" # Ensure this matches certresolver name in command       - "traefik.http.middlewares.traefik-auth.basicauth.users=admin:$$apr$$your_hashed_password" # Set up basic auth       - "traefik.http.routers.traefik.middlewares=traefik-auth" networks:   default:     external: true # Or create a network if needed

To use this, ensure you run `docker-compose up -d` in the directory containing `traefik-compose.yml` and `letsencrypt/acme.json` (create the directory and an empty `acme.json` file first).

You'll need to create a hashed password for Traefik's basic auth. You can use tools like `htpasswd` or online generators.

4. Configure Environment Variables

Kamal will automatically inject environment variables defined in `deploy.yml` into your application containers. For sensitive variables like database passwords or API keys, consider using a secrets management solution or securely inject them during deployment.

In your Rails application, ensure you are reading these environment variables correctly, e.g.:

# config/database.yml production:   url: <%= ENV['DATABASE_URL'] %> # Or for individual components: # adapter: postgresql # database: <%= ENV['POSTGRES_DB'] %> # username: <%= ENV['POSTGRES_USER'] %> # password: <%= ENV['POSTGRES_PASSWORD'] %> # host: <%= ENV['POSTGRES_HOST'] || 'localhost' %> # If DB is on a different host # port: <%= ENV['POSTGRES_PORT'] || 5432 %>

5. Log in to Docker Hub

Kamal will push your Docker image to Docker Hub. You need to be logged in on the `kamal-deployer` Droplet.

# On your KAMAL DEPLOYER Droplet docker login

Enter your Docker Hub username and password (or access token).

Deployment Process

With all configurations in place, you can now deploy your application.

1. Push Your Code

Ensure your latest changes are committed and pushed to your Git repository.

git add . git commit -m "Prepare for Kamal deployment" git push origin main # Or your deployment branch

2. Run the Deployment Command

Navigate to the root of your Rails application on your local machine (or on the `kamal-deployer` Droplet, if you prefer to run commands remotely).

# On your LOCAL machine bundle exec kamal deploy --config deploy.yml

Kamal will perform the following steps:

  1. Build your Docker image locally.
  2. Tag the image with the specified tag (e.g., commit SHA).
  3. Push the Docker image to your configured registry (e.g., Docker Hub).
  4. SSH into your `app-server-01` Droplet.
  5. Pull the new Docker image.
  6. Update Traefik configuration (if managed by Kamal) to point to the new application container.
  7. Start the new application container.
  8. Perform a rolling update, gradually replacing old containers with new ones.
  9. If configured, update DNS or load balancer settings.

You will see detailed output in your terminal indicating the progress of each step. Pay close attention to any errors or warnings.

3. Verify Deployment

Once the deployment is complete, check your application by navigating to your domain (e.g., `your-domain.com`) in a web browser. If you configured Traefik's dashboard, you can access it via its domain (e.g., `traefik.your-domain.com`) to inspect routers and services.

Troubleshooting

Deployments can encounter issues. Here are some common problems and their solutions:

1. Out of Memory Errors

Symptom: Containers crash immediately after starting, or the deployment fails with OOMKilled messages in Docker logs.

Cause: The application container is requesting more memory than the Droplet or Docker daemon can provide, or the application has memory leaks.

Solution:

  • Increase Droplet RAM: Upgrade your DigitalOcean Droplet plan.
  • Optimize Docker Image: Ensure your `Dockerfile` is efficient and doesn't bundle unnecessary dependencies.
  • Application Profiling: Use Ruby profiling tools to identify memory leaks within your Rails application.
  • Container Limits: While Kamal doesn't directly set container memory limits in `deploy.yml` by default (it relies on Docker's defaults), you can explore Docker Compose or other orchestration tools for explicit memory limits if needed.

2. SSL/Traefik Issues

Symptom: Application is inaccessible via HTTPS, or you see SSL certificate errors.

Cause: Incorrect Traefik configuration, Let's Encrypt setup, or firewall blocking port 443.

Solution:

  • Check Traefik Logs: Examine Traefik's logs for errors related to Let's Encrypt challenges or router configurations.
  • Verify `deploy.yml` Traefik Section: Ensure `your-domain.com`, `certresolver: letsencrypt`, and the service URL are correct.
  • Firewall Rules: Confirm that port 443 (and port 80 for Let's Encrypt challenges) are open on your DigitalOcean Droplet's firewall and any network firewalls.
  • Let's Encrypt Email: Double-check that the `letsencrypt_email` in your `deploy.yml` (or Traefik's config) is correct and valid.
  • DNS Propagation: Ensure your domain's DNS records are pointing to the correct IP address of your `app-server-01` Droplet.

3. Database Connection Errors

Symptom: Application starts but immediately fails with database connection errors.

Cause: Incorrect `DATABASE_URL` or database credentials in `deploy.yml`, or the database server is inaccessible.

Solution:

  • Verify `DATABASE_URL` in `deploy.yml`: Ensure the username, password, host, and database name are accurate.
  • Database Accessibility: If your database is on a separate server or service, confirm network connectivity and firewall rules allow access from the `app-server-01` Droplet.
  • Environment Variable Injection: Ensure the environment variables are correctly passed to the container. Check the container logs for any hints.

4. Deployment Fails with Git Errors

Symptom: Kamal fails during the Git checkout phase.

Cause: The `kamal-deployer` Droplet doesn't have access to your Git repository (e.g., SSH keys not configured for Git cloning).

Solution:

  • SSH Keys for Git: If your repository is private, ensure the SSH key configured on your `kamal-deployer` Droplet is added to your Git provider (GitHub, GitLab, etc.) as a deploy key or is associated with your account.
  • Public Repositories: For public repositories, this should not be an issue.
DevOps Pro Tip: For production environments, always use a dedicated deployment user with minimal privileges on your servers instead of `root`. Configure SSH access and Docker permissions for this user. Also, use environment variables for configuration rather than hardcoding values directly in your `deploy.yml` for better security and flexibility.

Conclusion & Next Steps

Deploying a Ruby on Rails application with Kamal on DigitalOcean provides a streamlined, efficient, and modern approach to production deployments. By leveraging containerization and automated orchestration, you can significantly reduce deployment time and minimize common errors.

This tutorial has provided a foundational setup. For a production-ready environment, consider the following:

  • High Availability: Deploy multiple application server Droplets and use a DigitalOcean Load Balancer or a highly available Traefik cluster.
  • Database Management: Utilize DigitalOcean's Managed Databases for a reliable and scalable database solution.
  • CI/CD Integration: Automate your deployments by integrating Kamal into your CI/CD pipeline (e.g., GitHub Actions, GitLab CI).
  • Monitoring and Alerting: Set up robust monitoring for your application and infrastructure (e.g., Prometheus, Grafana, Sentry).
  • Secrets Management: Integrate a dedicated secrets management tool (e.g., Vault, Doppler) for enhanced security.
  • Staging Environment: Maintain a separate staging environment for testing deployments before pushing to production.

By mastering tools like Kamal and cloud platforms like DigitalOcean, you are well-equipped to manage complex application deployments with confidence and efficiency.