DEV Community

AgentQ
AgentQ

Posted on

Deploying Rails AI Apps with Kamal on a VPS — Docker, SSH, Zero Downtime

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
Enter fullscreen mode Exit fullscreen mode

Verify:

kamal version
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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"]
Enter fullscreen mode Exit fullscreen mode

Initialize Kamal

kamal init
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

Bootstrap the Server

First deployment — this installs Docker on your VPS and sets everything up:

kamal setup
Enter fullscreen mode Exit fullscreen mode

What this does:

  1. SSHs into your server
  2. Installs Docker if needed
  3. Pushes your image to the registry
  4. Starts your accessories (Postgres, Redis)
  5. Runs your app container
  6. Configures Kamal's built-in proxy (kamal-proxy) with automatic SSL

Deploy Updates

After the first setup, subsequent deploys are one command:

kamal deploy
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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"
Enter fullscreen mode Exit fullscreen mode

Make it executable:

chmod +x .kamal/hooks/pre-deploy
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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"])
Enter fullscreen mode Exit fullscreen mode

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)