# 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:
    
    ```plaintext
    - 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:
    
    ```plaintext
    - Add A record: @ → your-server-ip (Proxy enabled)
    - Add CNAME: www → @ (Proxy enabled)
    ```
    
2. SSL/TLS Settings:
    
    ```plaintext
    - Edge Certificates: Enable "Always use HTTPS"
    - Overview: Set SSL/TLS mode to "Full"
    ```
    
3. Page Rules for www to root redirect:
    
    ```plaintext
    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:
    
    ```plaintext
    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:
    
    ```bash
    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](https://kamal-deploy.org/docs/commands/secrets/).

## Step 3: Database Configuration

### PostgreSQL Setup

1. Create `config/init.sql` for database initialization:
    
    ```sql
    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`:
    
    ```yaml
    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:
    
    ```yaml
    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:

```ruby
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`:
    

```yaml
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:
    
    ```bash
    bin/kamal setup
    ```
    
2. Watch the logs to ensure everything starts correctly:
    
    ```bash
    bin/kamal logs
    ```
    
3. For subsequent deployments:
    
    ```bash
    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:
    
    ```bash
    bin/kamal dbc
    ```
    
2. Container Inspection:
    
    ```bash
    bin/kamal shell
    ```
    
3. PostgreSQL Logs:
    
    ```bash
    bin/kamal accessory logs postgres
    ```
    

For more info go to [kamal-deploy](https://kamal-deploy.org/).

## 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!
