Skip to main content

Command Palette

Search for a command to run...

Deploying Rails 8 Applications: A Complete Guide with Docker, Kamal, and Cloudflare

Updated
4 min read
Deploying Rails 8 Applications: A Complete Guide with Docker, Kamal, and Cloudflare

Introduction

When I first started deploying Rails applications, the process felt overwhelming. Today, I'm excited to share a complete guide that transforms this complexity into a straightforward process. Let's dive into deploying a Rails 8 app using Docker, with PostgreSQL containerization, all orchestrated by Kamal on a Hetzner server.

Prerequisites

Before we begin our deployment journey, you'll need:

  • A Rails 8 application ready for production

  • Docker installed locally

  • A Hetzner account

  • Domain name and Cloudflare account

  • 1Password account (for secrets management)

  • Basic SSH knowledge

Step 1: Server and DNS Configuration

Hetzner Server Setup

Let's start by creating our production environment:

  1. Create a Hetzner server with these specifications:

     - Ubuntu 24.04
     - ARM64 (Ampere) CPU
     - CAX11 size (or choose based on your needs)
     - IPv4 only configuration
     - Your SSH keys added
    

Cloudflare Configuration

Set up your domain with proper DNS and security settings:

  1. DNS Configuration:

     - Add A record: @ → your-server-ip (Proxy enabled)
     - Add CNAME: www → @ (Proxy enabled)
    
  2. SSL/TLS Settings:

     - Edge Certificates: Enable "Always use HTTPS"
     - Overview: Set SSL/TLS mode to "Full"
    
  3. Page Rules for www to root redirect:

     URL: https://www.yourdomain.com/*
     Setting: Forwarding URL
     Status Code: 301 - Permanent Redirect
     Destination URL: https://yourdomain.com/$1
    

Step 2: Secrets Management

I've learned that proper secrets management is crucial. Let's set it up with 1Password:

  1. Create a secure note in 1Password with these credentials:

     KAMAL_REGISTRY_PASSWORD: your-docker-registry-password
     RAILS_MASTER_KEY: your-rails-master-key
     POSTGRES_PASSWORD: your-postgres-password
    
  2. Update .kamal/secrets to handle 1Password integration:

     SECRETS=$(kamal secrets fetch --adapter 1password --account YOUR_1PASSWORD_ACCOUNT_ID --from YOUR_VAULT_NAME/YOU_SECURE_NOTE KAMAL_REGISTRY_PASSWORD RAILS_PRODUCTION_KEY POSTGRES_PASSWORD)
    
     KAMAL_REGISTRY_PASSWORD=$(kamal secrets extract KAMAL_REGISTRY_PASSWORD $SECRETS)
     RAILS_MASTER_KEY=$(kamal secrets extract RAILS_PRODUCTION_KEY $SECRETS)
     POSTGRES_PASSWORD=$(kamal secrets extract POSTGRES_PASSWORD $SECRETS)
    

For more info visit Kamal Secrets.

Step 3: Database Configuration

PostgreSQL Setup

  1. Create config/init.sql for database initialization:

     CREATE DATABASE IF NOT EXISTS `your_app_production`;
     CREATE DATABASE IF NOT EXISTS `your_app_production_cache`;
     CREATE DATABASE IF NOT EXISTS `your_app_production_queue`;
     CREATE DATABASE IF NOT EXISTS `your_app_production_cable`;
    
  2. Configure PostgreSQL accessory in config/deploy.yml:

     accessories:
       postgres:
         image: postgres:16-alpine
         host: your-server-ip
         port: 5432
         options:
           restart: always
         env:
           clear:
             POSTGRES_USER: postgres
             POSTGRES_DB: your_app_production
           secret:
             - POSTGRES_PASSWORD
         files:
           - config/init.sql:/docker-entrypoint-initdb.d/init.sql
         volumes:
           - /var/lib/postgresql/your_app_production:/var/lib/postgresql/data
    
  3. Update config/database.yml for production:

     production:
         primary: &primary_production
             <<: *default
             host: <%= ENV["DB_HOST"] %>
             database: your_app_production
             username: postgres
             password: <%= ENV["POSTGRES_PASSWORD"] %>
         cache:
             <<: *primary_production
             database: your_app_production_cache
             migrations_paths: db/cache_migrate
         queue:
             <<: *primary_production 
             database: your_app_production_queue
             migrations_paths: db/queue_migrate
         cable: 
             <<: *primary_production
             database: your_app_production_cable
             migrations_paths: db/cable_migrate
    

Step 4: Production Environment Configuration

Update config/environments/production.rb with these essential settings:

Rails.application.configure do
  # Asset handling
  config.require_master_key = false
  config.assets.css_compressor = nil
  config.assets.compile = false
  config.public_file_server.enabled = true
  config.asset_host = "https://yourdomain.com"
  config.assets.enabled = true
  config.assets.version = "1.0"

  # SSL and security
  config.ssl_options = { 
    redirect: { 
      exclude: ->(request) { request.path == "/up" } 
    } 
  }

  # Health checks
  config.silence_healthcheck_path = "/up"

  # Domain configuration
  config.hosts = [
    "yourdomain.com",
    /.*\.yourdomain\.com/
  ]

  config.host_authorization = { 
    exclude: ->(request) { request.path == "/up" } 
  }

  # URL options
  config.action_mailer.default_url_options = { 
    host: "yourdomain.com", 
    protocol: "https" 
  }

  routes.default_url_options = {
    host: "yourdomain.com",
    protocol: "https"
  }
end

Step 5: Kamal Deployment Setup

Kamal, Rails 8's built-in deployment tool, makes containerized deployment straightforward:

  1. Update rest of config/deploy.yml:
service: your_app_name

registry:
  username: your_dockerhub_username
  password:
    - KAMAL_REGISTRY_PASSWORD

aliases:
    console: app exec --interactive --reuse "bin/rails console"
    shell: app exec --interactive --reuse "bash"
    logs: app logs -f
    dbc: app exec --interactive --reuse "bin/rails dbconsole"

image: your_dockerhub_username/your_app_name

builder:
    arch: arm64

proxy:
  ssl: true
  host: yourdomain.com

servers:
  web:
    hosts:
      - your_server_ip

volumes:
  - your_app_storage:/rails/storage:rw
  - /tmp/storage:/rails/tmp/storage:rw
  - your_app_data:/data

env:
  secret:
    - RAILS_MASTER_KEY
    - POSTGRES_PASSWORD
  clear:
    HOST: yourdomain.com
    RAILS_ENV: production
    DB_HOST: your_app-postgres
    SOLID_QUEUE_IN_PUMA: true
    RAILS_SERVE_STATIC_FILES: true
    RAILS_LOG_TO_STDOUT: true

Step 6: Deployment

Now for the exciting part - deploying our application:

  1. Initial setup:

     bin/kamal setup
    
  2. Watch the logs to ensure everything starts correctly:

     bin/kamal logs
    
  3. For subsequent deployments:

     bin/kamal deploy
    

Deployment Verification Checklist

After deployment, I always verify these key points:

  • [ ] Health check endpoint (/up) responds

  • [ ] Database migrations completed successfully

  • [ ] Assets are serving correctly

  • [ ] SSL certificate is valid

  • [ ] www to root domain redirect works

  • [ ] PostgreSQL container is running and accessible

Troubleshooting Tips

From my experience, here are some common issues and solutions:

  1. Database Connection Issues:

     bin/kamal dbc
    
  2. Container Inspection:

     bin/kamal shell
    
  3. PostgreSQL Logs:

     bin/kamal accessory logs postgres
    

For more info go to kamal-deploy.

Conclusion

Deploying a Rails 8 application might seem daunting at first, but with this structured approach, it becomes a manageable and repeatable process. I've learned that proper configuration of each component - from DNS to secrets management to database setup - is crucial for a robust production deployment.

Remember to always test your deployment in a staging environment first, and keep your secrets secure using proper management tools like 1Password.

Have you deployed a Rails application using this approach? I'd love to hear about your experience in the comments below! 🚀


Happy Coding!

2.4K views

More from this blog

S

Sulman Baig

20 posts

Senior Software Engineer with 11 years of expertise in Ruby on Rails and Vue.js, specializing in health, e-commerce, staffing, and transport. Experienced in software development and version analysis.