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:
- 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:
- DNS Configuration:
- Add A record: @ → your-server-ip (Proxy enabled) - Add CNAME: www → @ (Proxy enabled)
- SSL/TLS Settings:
- Edge Certificates: Enable "Always use HTTPS" - Overview: Set SSL/TLS mode to "Full"
- 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:
- 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
- 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
- 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`;
- 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
-
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:
- 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:
- Initial setup:
bin/kamal setup
- Watch the logs to ensure everything starts correctly:
bin/kamal logs
- 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:
- Database Connection Issues:
bin/kamal dbc
- Container Inspection:
bin/kamal shell
- 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!
Originally published at sulmanweb.com
Top comments (0)