We've spent 27 posts writing code. Now it's time to put it on a server.
Not a managed platform. Not a serverless function. A real server that you control, running Docker containers deployed with Kamal — Rails' official deployment tool.
This is post #29 in the Ruby for AI series. Let's deploy.
What Is Kamal?
Kamal (formerly MRSK) is a deployment tool from the Rails team. It uses Docker to package your app and SSH to deploy it to any server. No proprietary runtime. No vendor lock-in. Just Docker + SSH + a VPS.
Think of it as Capistrano for the container age.
Prerequisites
You need:
- A VPS (Ubuntu 22.04+ or Debian 12+) with SSH access
- Docker installed locally
- Ruby 3.2+ locally
- A Docker Hub account (or any container registry)
Install Kamal
gem install kamal
Verify:
kamal version
Prepare Your Rails App
If your app doesn't have a Dockerfile yet, Rails 7.1+ generates one by default. If you're on an older version:
rails generate dockerfile
This creates a production-ready, multi-stage Dockerfile. Check it looks something like this:
# syntax=docker/dockerfile:1
FROM ruby:3.3-slim AS base
WORKDIR /rails
ENV RAILS_ENV="production" \
BUNDLE_DEPLOYMENT="1" \
BUNDLE_PATH="/usr/local/bundle"
FROM base AS build
RUN apt-get update -qq && \
apt-get install --no-install-recommends -y build-essential git libpq-dev
COPY Gemfile Gemfile.lock ./
RUN bundle install && \
rm -rf ~/.bundle/ "${BUNDLE_PATH}"/ruby/*/cache
COPY . .
RUN bundle exec rails assets:precompile
FROM base
RUN apt-get update -qq && \
apt-get install --no-install-recommends -y libpq5 curl && \
rm -rf /var/lib/apt/lists /var/cache/apt/archives
COPY --from=build /usr/local/bundle /usr/local/bundle
COPY --from=build /rails /rails
RUN groupadd --system --gid 1000 rails && \
useradd rails --uid 1000 --gid 1000 --create-home --shell /bin/bash && \
chown -R rails:rails db log storage tmp
USER 1000:1000
ENTRYPOINT ["/rails/bin/docker-entrypoint"]
EXPOSE 3000
CMD ["./bin/rails", "server"]
Initialize Kamal
kamal init
This creates config/deploy.yml — the heart of your deployment config. Edit it:
service: myaiapp
image: yourdockerhub/myaiapp
servers:
web:
hosts:
- 203.0.113.10
labels:
traefik.http.routers.myaiapp.rule: Host(`myaiapp.com`)
traefik.http.routers.myaiapp.tls.certresolver: letsencrypt
proxy:
ssl: true
host: myaiapp.com
registry:
username: yourdockerhub
password:
- KAMAL_REGISTRY_PASSWORD
env:
clear:
RAILS_ENV: production
RAILS_LOG_TO_STDOUT: "1"
RAILS_SERVE_STATIC_FILES: "true"
secret:
- RAILS_MASTER_KEY
- DATABASE_URL
- REDIS_URL
- OPENAI_API_KEY
accessories:
db:
image: postgres:16
host: 203.0.113.10
port: "127.0.0.1:5432:5432"
env:
clear:
POSTGRES_DB: myaiapp_production
secret:
- POSTGRES_PASSWORD
directories:
- data:/var/lib/postgresql/data
redis:
image: redis:7
host: 203.0.113.10
port: "127.0.0.1:6379:6379"
directories:
- data:/data
Set Your Secrets
Create .kamal/secrets (this file is gitignored):
KAMAL_REGISTRY_PASSWORD=your_docker_hub_token
RAILS_MASTER_KEY=your_master_key_from_config/master.key
DATABASE_URL=postgresql://postgres:yourpassword@myaiapp-db:5432/myaiapp_production
REDIS_URL=redis://myaiapp-redis:6379/0
OPENAI_API_KEY=sk-your-openai-key
POSTGRES_PASSWORD=yourpassword
Bootstrap the Server
First deployment — this installs Docker on your VPS and sets everything up:
kamal setup
What this does:
- SSHs into your server
- Installs Docker if needed
- Pushes your image to the registry
- Starts your accessories (Postgres, Redis)
- Runs your app container
- Configures Kamal's built-in proxy (kamal-proxy) with automatic SSL
Deploy Updates
After the first setup, subsequent deploys are one command:
kamal deploy
This builds your Docker image, pushes it, and performs a zero-downtime rolling deploy. The old container keeps serving requests until the new one is healthy.
Useful Kamal Commands
# Check what's running
kamal details
# View logs
kamal app logs
# Open a Rails console on the server
kamal app exec -i "bin/rails console"
# Run database migrations
kamal app exec "bin/rails db:migrate"
# Rollback to previous version
kamal rollback
# Start/stop accessories
kamal accessory start db
kamal accessory stop redis
Running Migrations on Deploy
Add a pre-deploy hook. Create .kamal/hooks/pre-deploy:
#!/bin/bash
echo "Running database migrations..."
kamal app exec "bin/rails db:migrate"
Make it executable:
chmod +x .kamal/hooks/pre-deploy
Health Checks
Kamal checks your app's health before routing traffic. It hits /up by default (Rails 7.1+ includes this route). Make sure yours works:
# config/routes.rb
get "up" => "rails/health#show", as: :rails_health_check
If the health check fails, Kamal won't route traffic to the new container. The old one keeps running. No downtime from bad deploys.
Environment Variables for AI
Your AI app needs API keys. Keep them in .kamal/secrets and reference them in deploy.yml under env.secret. They're injected at runtime as environment variables — never baked into the Docker image.
# In your Rails app, access them normally:
OpenAI::Client.new(access_token: ENV["OPENAI_API_KEY"])
What You Have Now
After this post:
- Your Rails AI app runs in a Docker container on a VPS you control
- Kamal handles zero-downtime deploys via SSH
- Postgres and Redis run as Docker accessories on the same server
- SSL is handled automatically via kamal-proxy and Let's Encrypt
- Secrets are injected at runtime, never in the image
- Health checks prevent bad deploys from taking down your app
No vendor lock-in. No surprise bills. No "free tier" that becomes $500/month. Just your code, on your server.
Next up: Post #30 goes deeper into VPS setup — Nginx, Puma tuning, systemd, and SSL the manual way. For when you want full control over every layer.
Top comments (0)