Web Server in Production Mode

Rails applications can be deployed in many ways. Heroku-style platforms, Docker on a PaaS, bare-metal with Passenger, it all works. Rails 8 however has picked a default that we’ll cover first: a Docker container built from the generated Dockerfile, fronted by Thruster for static files and HTTPS, deployed and managed with Kamal 2.

This chapter replaces the Passenger/Nginx tutorial from the Rails 5.2 edition of this book. Passenger still works beautifully (see below), but the out-of-the-box experience in Rails 8 is Kamal. If you’ve never deployed a Rails app, start with Kamal.

Kamal 2 (the Rails 8 Default)

Kamal is an SSH-plus-Docker deploy tool that the Rails team sponsors. It builds a Docker image locally (or on any CI runner), pushes it to a registry, SSHes into the target server(s), and orchestrates a zero-downtime rollout.

Everything you need is already in a fresh Rails 8 application:

  • Dockerfile — the image recipe.

  • .dockerignore.

  • config/deploy.yml — Kamal’s deploy config.

  • .kamal/secrets — where you keep API tokens, database URLs, etc.

  • bin/kamal — the binstub for the Kamal CLI.

First Deploy

  1. Edit config/deploy.yml to point at your server(s) and registry:

    config/deploy.yml
    service: myapp
    image: yourname/myapp
    
    servers:
      web:
        - 203.0.113.10
    
    proxy:
      ssl: true
      host: app.example.com
    
    registry:
      server: ghcr.io
      username: yourname
      password:
        - KAMAL_REGISTRY_PASSWORD
    
    env:
      secret:
        - RAILS_MASTER_KEY
  2. Set the relevant secrets in .kamal/secrets (or via your shell environment). Never commit real values.

  3. Make sure Docker is running locally and you have SSH access to the server.

  4. Set up the server with the Kamal proxy once:

    $ bin/kamal setup

    This installs Docker, pulls the Kamal proxy container, pulls your image for the first time, runs the DB migrations and starts the app.

  5. For every subsequent deploy:

    $ bin/kamal deploy

    Kamal rebuilds the image if needed, rolls it out with zero downtime, and keeps an audit log of every deploy.

Thruster

Thruster is a small Go process that sits in front of Puma inside the Rails container. It does three things:

  • Serves compressed (gzip, brotli) responses for static assets straight from public/ without calling Ruby.

  • Handles HTTP/2 and HTTPS termination (when you terminate TLS at Thruster).

  • Adds X-Sendfile support for Active Storage downloads.

There is no configuration required: rails new already adds gem "thruster" and the generated Dockerfile wires it as the container’s entry point. When you run the image locally with docker run, requests go client → Thruster → Puma → Rails.

Environment Variables in Production

Rails 8’s default production configuration reads sensitive values from environment variables:

  • RAILS_MASTER_KEY — decrypts config/credentials.yml.enc.

  • DATABASE_URL — PostgreSQL/MySQL/SQLite connection string. (With SQLite on Kamal, Kamal mounts a persistent volume and points DATABASE_URL at it.)

  • SECRET_KEY_BASE — falls back to what’s inside credentials, but can be overridden.

Kamal injects the variables you list under env: in config/deploy.yml. For secrets use the env.secret: sub-key and add the matching entries to .kamal/secrets so the actual values stay out of git.

See Credentials for more on how Rails handles secrets.

Database in Production

A fresh Rails 8 app defaults to SQLite. Kamal, by default, makes that a persistent volume mounted into the container. For small and medium applications that’s genuinely fine: SQLite in WAL mode with Solid Queue / Solid Cache / Solid Cable handles a surprising amount of traffic.

If you need PostgreSQL or MySQL, change the database in config/database.yml, add the adapter gem (pg or mysql2), and set DATABASE_URL in the environment.

Deploying Without Kamal

You do not have to use Kamal. Any host that can run a Docker container will run the same image the Rails 8 Dockerfile builds. A handful of common options:

  • Heroku / Fly.io / Render / Railway — these platforms detect the Dockerfile (or have a "Ruby" buildpack). You push a git branch or hit "deploy" and they host the app. Good for getting something online fast.

  • Docker Compose on a single VM — keep things simple by running the container with docker compose up -d behind Nginx or Caddy.

  • Kubernetes — if you already run Kubernetes, deploy the image as a Deployment + Service. Helm charts exist.

The Classic Way: Passenger + Nginx

If you want a non-Docker deploy to a long-lived Linux VM, the classic path is Phusion Passenger + Nginx. The short version:

  1. Install the Passenger Nginx module following phusionpassenger.com.

  2. Clone your app to /var/www/myapp, configure Bundler for deployment with bundle config set --local deployment 'true' and bundle config set --local without 'development test', then run bundle install and RAILS_ENV=production bin/rails db:migrate assets:precompile.

  3. Configure Nginx to point at /var/www/myapp/public and enable Passenger for the server block.

  4. Restart Nginx. Reload the app with touch /var/www/myapp/tmp/restart.txt after each deploy.

This path is well-trodden and works reliably for teams that cannot (or prefer not to) run Docker. We keep the Rails 8 Kamal path as the recommended default because it requires less sysadmin and because the whole Rails team uses it to deploy 37signals' own Rails applications.

Prompt pattern: plan a Rails minor-version upgrade

Every Rails minor release brings new defaults, a handful of deprecations, and a bin/rails app:update that proposes a batch of config changes. Reading the upgrade guide end-to-end takes an hour; having an agent do the reading and produce a summary takes five minutes, and you still review every file change.

Prompt:

Read the Rails 8.1 upgrade notes at https://guides.rubyonrails.org/upgrading_ruby_on_rails.html, then run bin/rails app:update. For every change it proposes in config/ or bin/, give me a three-column summary: file, what changes, and whether I should accept as-is, accept with edits, or reject. Do not commit anything.

Verify: bin/rails test, bin/rails test:system, boot the app and click the critical paths. See Coding Rails With an AI Agent for the broader workflow. Never skip the "boot the app" step on a minor upgrade — the test suite doesn’t catch initializer problems.

Further Reading