Kamal is a great way to deploy containerized applications to your own servers without maintaining a full Kubernetes setup. In this guide, you will set up a complete staging and production deployment flow with custom SSL certificates and clear environment separation.
What you will build
- One codebase
- Two environments:
stagingandproduction - Separate servers per environment
- Environment-specific secrets and runtime variables
- Custom SSL certificates managed by you
Prerequisites
Before starting, make sure you have:
- A Rails (or any Dockerized) app ready to run in production mode
- Two domains (example):
app-staging.example.comapp.example.com
- SSH access to both servers
- Docker installed on both servers
- Kamal installed locally
Install Kamal if needed:
gem install kamal
Step 1: Initialize Kamal
From your project root:
kamal init
This creates:
config/deploy.yml.kamal/secrets
Step 2: Define shared and environment-specific deploy config
Use one base config and two environment overlay files.
config/deploy.yml (base)
service: myapp
image: your-dockerhub-user/myapp
registry:
username: your-dockerhub-user
password:
- KAMAL_REGISTRY_PASSWORD
ssh:
user: deploy
builder:
arch: amd64
proxy:
app_port: 3000
ssl: false
config/deploy.staging.yml
servers:
web:
hosts:
- 10.0.1.10
env:
clear:
RAILS_ENV: staging
RACK_ENV: staging
APP_HOST: app-staging.example.com
secret:
- RAILS_MASTER_KEY
- DATABASE_URL
- SECRET_KEY_BASE
config/deploy.production.yml
servers:
web:
hosts:
- 10.0.2.10
env:
clear:
RAILS_ENV: production
RACK_ENV: production
APP_HOST: app.example.com
secret:
- RAILS_MASTER_KEY
- DATABASE_URL
- SECRET_KEY_BASE
Step 3: Add environment secrets
Store all secrets in .kamal/secrets:
KAMAL_REGISTRY_PASSWORD=...
# staging
STAGING_RAILS_MASTER_KEY=...
STAGING_DATABASE_URL=...
STAGING_SECRET_KEY_BASE=...
# production
PRODUCTION_RAILS_MASTER_KEY=...
PRODUCTION_DATABASE_URL=...
PRODUCTION_SECRET_KEY_BASE=...
Map env-specific values when deploying:
- For staging, export secrets from
STAGING_* - For production, export secrets from
PRODUCTION_*
One simple pattern is a tiny shell wrapper per environment.
Step 4: Configure custom SSL certificate files
If you manage certs manually (for example from your CA or internal PKI), keep them outside your repo:
mkdir -p ~/.ssl/myapp
Place files like:
~/.ssl/myapp/staging.crt~/.ssl/myapp/staging.key~/.ssl/myapp/production.crt~/.ssl/myapp/production.key
Then extend each environment config with proxy SSL mounts.
Add to config/deploy.staging.yml
proxy:
host: app-staging.example.com"
ssl:
certificate_pem: /home/deploy/.ssl/myapp/staging.crt
private_key_pem: /home/deploy/.ssl/myapp/staging.key
Add to config/deploy.production.yml
proxy:
hosts:
- "app.example.com"
- "*.example.com"
ssl:
certificate_pem: /home/deploy/.ssl/myapp/production.crt
private_key_pem: /home/deploy/.ssl/myapp/production.key
Make sure the cert and key are copied to each target server at the same absolute path and readable by the deploy user.
Step 5: Set DNS records (wildcard or explicit hosts)
You can use explicit host records or wildcard DNS:
- Explicit:
app-staging.example.com -> staging server IPapp.example.com -> production server IP
- Wildcard approach:
*.staging.example.com -> staging server IP*.example.com -> production server IP
If you use wildcard DNS, ensure your SSL certificate SAN/CN matches the hostnames.
Step 6: First-time server preparation
Run setup for each environment:
kamal setup -c config/deploy.yml -d staging
kamal setup -c config/deploy.yml -d production
If your Kamal version expects direct config files, use:
kamal setup -c config/deploy.staging.yml
kamal setup -c config/deploy.production.yml
Step 7: Deploy to staging, then production
Deploy to staging first:
kamal deploy -c config/deploy.staging.yml
Verify application and SSL:
curl -I https://app-staging.example.com
When staging is healthy, deploy production:
kamal deploy -c config/deploy.production.yml
Verify production:
curl -I https://app.example.com
Step 8: Safe release workflow
Recommended process:
- Merge to
main - Deploy
staging - Run smoke tests
- Deploy
production - Monitor logs and health endpoints
Useful commands:
kamal app logs -c config/deploy.staging.yml
kamal app logs -c config/deploy.production.yml
Common issues and fixes
- SSL handshake fails
- Check key/cert pair match
- Check cert chain includes intermediate CA
- Check file permissions on server
- Wrong environment variables loaded
- Confirm
env.clearandenv.secretin the active deploy config - Avoid reusing production secrets in staging
- Confirm
- 502/Bad Gateway from proxy
- Confirm
app_portmatches container app port - Confirm app process is listening on
0.0.0.0
- Confirm
Final notes
This setup gives you a clean and scalable deployment model:
- predictable environment isolation
- controlled custom SSL management
- safer release lifecycle through staging-first deployments
Once this is stable, your next improvement is adding automated health checks and CI-triggered staging deploys.
Comments