Deploy with Kamal
Kamal deploys Docker containers to your own servers via SSH. It handles zero-downtime deploys, SSL certificates, and rolling restarts out of the box.
This guide walks through deploying an Alchemy app with Kamal, based on a real production setup.
Don't want to manage your own infrastructure? Our sponsoroffers managed Alchemy hosting.
Prerequisites
- A server with SSH access (e.g. Hetzner, DigitalOcean, AWS EC2)
- A container registry account (GitHub Container Registry, Docker Hub, etc.)
- Docker running locally (unless building in CI/CD)
Install Kamal
Add Kamal to your Gemfile and run the initializer to generate the config files:
bundle add kamal
bin/kamal initDockerfile
Kamal deploys Docker images, so you need a Dockerfile. Rails 7.1+ generates one for you. The key parts for Alchemy:
FROM docker.io/library/ruby:3.4.8-slim AS base
WORKDIR /rails
# Install runtime packages including an image processor
RUN apt-get update -qq && \
apt-get install --no-install-recommends -y curl libjemalloc2 libvips sqlite3 && \
rm -rf /var/lib/apt/lists /var/cache/apt/archives
# ... build stage installs gems and precompiles assets ...
# Precompile assets without requiring credentials
RUN SECRET_KEY_BASE_DUMMY=1 ./bin/rails assets:precompile
# ... final stage copies built artifacts ...
# Entrypoint prepares the database
ENTRYPOINT ["/rails/bin/docker-entrypoint"]
EXPOSE 80
CMD ["./bin/thrust", "./bin/rails", "server"]TIP
Make sure libvips (or imagemagick) is in the base image's package list. Without it, image rendering will fail in production.
The entrypoint script should run database preparation on startup:
#!/bin/bash -e
if [ "${@: -2:1}" == "./bin/rails" ] && [ "${@: -1:1}" == "server" ]; then
./bin/rails db:prepare
fi
exec "${@}"This runs db:prepare only when starting the web server, which handles both initial setup and migrations.
Kamal Configuration
Configure config/deploy.yml:
service: my-alchemy-app
image: your-registry-user/my-alchemy-app
servers:
web:
- 203.0.113.1 # your server IP
proxy:
ssl: true
hosts:
- example.com
- www.example.com
registry:
server: ghcr.io
username: your-github-user
password:
- KAMAL_REGISTRY_PASSWORD
env:
secret:
- RAILS_MASTER_KEY
clear:
SOLID_QUEUE_IN_PUMA: true
volumes:
- "my_alchemy_app_storage:/rails/storage"
asset_path: /rails/public/assets
builder:
arch: amd64Key settings:
proxy.ssl: true-- Kamal's proxy automatically obtains Let's Encrypt certificatesvolumes-- Persists SQLite databases, Active Storage files, and cache across deploysasset_path-- Bridges assets between old and new versions during deploy to avoid 404s on in-flight requestsbuilder.arch-- Set toamd64if your server runs x86_64 (common for most VPS providers)
Secrets
Edit .kamal/secrets to pull secrets from environment variables or a password manager:
KAMAL_REGISTRY_PASSWORD=$KAMAL_REGISTRY_PASSWORD
RAILS_MASTER_KEY=$RAILS_MASTER_KEYWARNING
Never commit config/master.key to git. Pass it through secrets instead.
Add any additional secrets your app needs to this file and reference them in the env.secret list in config/deploy.yml.
Database Setup
For a simple deployment, SQLite with a persistent Docker volume works well. The volumes entry in config/deploy.yml ensures the database survives deploys:
volumes:
- "my_alchemy_app_storage:/rails/storage"For PostgreSQL or MySQL, configure an accessory in config/deploy.yml or use an external managed database and set DB_HOST in your environment.
The entrypoint's db:prepare handles both initial creation and subsequent migrations automatically.
Storage
For local disk storage, the Docker volume mount keeps files persistent. This is the simplest option for single-server deployments.
For remote storage (S3, GCS, Azure), configure ActiveStorage as described in the Deployment guide. Remote storage is required if you run multiple servers or want files to survive a server replacement.
First Deploy
bin/kamal setupThis will:
- Install Docker on your server (if needed)
- Start the Kamal proxy
- Build and push your Docker image
- Start the container
- Run the entrypoint (which calls
db:prepare)
Verify by visiting your domain. If you configured proxy.ssl, HTTPS should work automatically.
Subsequent Deploys
bin/kamal deployKamal performs zero-downtime deploys by default: it starts the new container, waits for it to pass health checks, then stops the old one.
Useful Commands
# Open a Rails console on the server
bin/kamal console
# Tail production logs
bin/kamal logs -f
# Run a one-off command
bin/kamal app exec "bin/rails alchemy:generate:thumbnails"
# SSH into the running container
bin/kamal app exec -i bashYou can define these as aliases in config/deploy.yml:
aliases:
console: app exec --interactive --reuse "bin/rails console"
logs: app logs -fDestinations
Kamal destinations let you deploy the same app to different environments. Create a destination-specific config file for each stage:
# config/deploy.staging.yml
servers:
web:
- 203.0.113.2 # staging server
proxy:
ssl: true
hosts:
- staging.example.com# config/deploy.production.yml
servers:
web:
- 203.0.113.1 # production server
proxy:
ssl: true
hosts:
- example.com
- www.example.comDeploy to a specific destination with the -d flag:
bin/kamal setup -d staging
bin/kamal deploy -d productionEach destination gets its own containers and proxy configuration on the target server. Secrets can also be scoped per destination using .kamal/secrets.staging and .kamal/secrets.production.
CI/CD
You can trigger Kamal deploys from GitHub Actions or any CI system. A minimal workflow:
# .github/workflows/deploy.yml
name: Deploy
on:
push:
branches: [main]
jobs:
deploy:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: ruby/setup-ruby@v1
with:
bundler-cache: true
- run: bin/kamal deploy
env:
KAMAL_REGISTRY_PASSWORD: ${{ secrets.KAMAL_REGISTRY_PASSWORD }}
RAILS_MASTER_KEY: ${{ secrets.RAILS_MASTER_KEY }}TIP
The Kamal documentation covers additional topics like rolling deploys, accessories, and multi-server setups.