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
-
Edit
config/deploy.ymlto point at your server(s) and registry:config/deploy.ymlservice: 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 -
Set the relevant secrets in
.kamal/secrets(or via your shell environment). Never commit real values. -
Make sure Docker is running locally and you have SSH access to the server.
-
Set up the server with the Kamal proxy once:
$ bin/kamal setupThis installs Docker, pulls the Kamal proxy container, pulls your image for the first time, runs the DB migrations and starts the app.
-
For every subsequent deploy:
$ bin/kamal deployKamal 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— decryptsconfig/credentials.yml.enc. -
DATABASE_URL— PostgreSQL/MySQL/SQLite connection string. (With SQLite on Kamal, Kamal mounts a persistent volume and pointsDATABASE_URLat 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 -dbehind 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:
-
Install the Passenger Nginx module following phusionpassenger.com.
-
Clone your app to
/var/www/myapp, configure Bundler for deployment withbundle config set --local deployment 'true'andbundle config set --local without 'development test', then runbundle installandRAILS_ENV=production bin/rails db:migrate assets:precompile. -
Configure Nginx to point at
/var/www/myapp/publicand enable Passenger for the server block. -
Restart Nginx. Reload the app with
touch /var/www/myapp/tmp/restart.txtafter 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.
Further Reading
-
Kamal homepage: https://kamal-deploy.org
-
Thruster: https://github.com/basecamp/thruster
-
Phusion Passenger: https://www.phusionpassenger.com
-
Rails Guides — Configuring Rails Applications: https://guides.rubyonrails.org/configuring.html