Post

Hosting multiple Rails 8 apps on a VPS using Kamal 2

Hosting multiple Rails 8 apps on a VPS using Kamal 2

I couldn’t find a tutorial that hosts multiple Rails 8 apps with Postgres databases on a single VPS(I use the cheapest Hetzner one). So here it is…

Hosting Multiple Rails 8 Apps with PostgreSQL on a Single VPS

This guide walks through deploying two (or more) independent Rails 8 applications — each with its own PostgreSQL database — onto one VPS using Kamal. Every app gets its own subdomain, SSL certificate, and isolated database, while sharing a single reverse proxy and one physical server.

What you’ll end up with:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
                    ┌─────────────────────────────────────┐
  app1.example.com ─┤                                     │
                    │  kamal-proxy (ports 80/443, SSL)    │
  app2.example.com ─┤                                     │
                    └──────────┬──────────────────────────┘
                               │
                    ┌──────────▼──────────────────────────┐
                    │         VPS (e.g. 136.534.162.123)  │
                    │                                     │
                    │  ┌─────────┐      ┌─────────┐       │
                    │  │  app1   │      │  app2   │       │
                    │  │ (Rails) │      │ (Rails) │       │
                    │  └────┬────┘      └────┬────┘       │
                    │       │                │            │
                    │  ┌────▼────┐      ┌────▼────┐       │
                    │  │ app1-db │      │ app2-db │       │
                    │  │ Postgres│      │ Postgres│       │
                    │  └─────────┘      └─────────┘       │
                    └─────────────────────────────────────┘

Prerequisites

Before you start, make sure you have:

RequirementNotes
A VPSAny provider (Hetzner, DigitalOcean, Linode, etc.). Ubuntu 22.04+ works well.
Root SSH accessssh root@YOUR_SERVER_IP should work with your SSH key.
A domaine.g. example.com with DNS you control.
Docker Hub accountKamal builds images locally and pushes them to a registry the server can pull from.
Ruby & Docker locallyTo build images and run Kamal commands. Just follow the instructions
Two Rails 8 appsCreated with --database=postgresql.

Step 1: Create Your Rails Apps

Rails 8 ships with Kamal, Docker, and the Solid stack (cache, queue, cable) out of the box. Create each app with PostgreSQL:

1
2
rails new app1 --css tailwind --database=postgresql
rails new app2 --css tailwind --database=postgresql

Each app will already contain:

  • Dockerfile — production-ready, with postgresql-client installed
  • config/deploy.yml — Kamal configuration (needs editing)
  • .kamal/secrets — secret management template
  • bin/kamal — Kamal CLI wrapper

Rails 8’s default database.yml production section uses four databases per app (primary, cache, queue, cable). We’ll configure all of them to talk to the same Postgres accessory.


Step 2: Configure DNS

Point each subdomain at your VPS before deploying. Let’s Encrypt needs the domains to resolve to issue SSL certificates.

TypeNameValue
Aapp1.example.comYOUR_SERVER_IP
Aapp2.example.comYOUR_SERVER_IP

Step 3: Set Up Secrets

Kamal reads secrets from .kamal/secrets at deploy time. Never put raw passwords in this file, just pull them from environment variables instead.

Export these (add them to ~/.zshrc ~/.bashrc or a password manager):

1
2
3
4
5
6
7
8
# Docker Hub access token (not your account password)
# Create under Personal Access Tokens, with Read & Write permission
export KAMAL_REGISTRY_PASSWORD="dckr_pat_..."

# Strong random passwords. Save these! They're baked into the Postgres
# data volume on first boot and must stay the same for future deploys.
export APP1_DATABASE_PASSWORD="pass"
export APP2_DATABASE_PASSWORD="pass2"

Then configure .kamal/secrets in each app. The file is safe to commit to git because it only references env vars.

app1/.kamal/secrets:

1
2
3
4
KAMAL_REGISTRY_PASSWORD=$KAMAL_REGISTRY_PASSWORD
RAILS_MASTER_KEY=$(cat config/master.key)
APP1_DATABASE_PASSWORD=$APP1_DATABASE_PASSWORD
POSTGRES_PASSWORD=$APP1_DATABASE_PASSWORD

app2/.kamal/secrets:

1
2
3
4
KAMAL_REGISTRY_PASSWORD=$KAMAL_REGISTRY_PASSWORD
RAILS_MASTER_KEY=$(cat config/master.key)
APP2_DATABASE_PASSWORD=$APP2_DATABASE_PASSWORD
POSTGRES_PASSWORD=$APP2_DATABASE_PASSWORD

The POSTGRES_PASSWORD line is important: Kamal’s Postgres accessory reads POSTGRES_PASSWORD, while Rails reads APP1_DATABASE_PASSWORD / APP2_DATABASE_PASSWORD. Aliasing them to the same value keeps the app and database in sync.


Step 4: Configure Kamal (config/deploy.yml)

This is the main configuration file. Below is a complete example for app1. The app2 file is identical except for names (service, image, host, DB user, env vars).

app1/config/deploy.yml:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
service: app1

image: YOUR_DOCKERHUB_USERNAME/app1

servers:
  web:
    - YOUR_SERVER_IP

# kamal-proxy handles SSL and routes traffic by hostname.
# Both apps share one proxy on ports 80/443.
proxy:
  ssl: true
  host: app1.example.com

registry:
  username: YOUR_DOCKERHUB_USERNAME
  password:
    - KAMAL_REGISTRY_PASSWORD

env:
  secret:
    - RAILS_MASTER_KEY
    - APP1_DATABASE_PASSWORD
  clear:
    SOLID_QUEUE_IN_PUMA: true
    DB_HOST: app1-db

aliases:
  console: app exec --interactive --reuse "bin/rails console"
  shell: app exec --interactive --reuse "bash"
  logs: app logs -f
  dbc: app exec --interactive --reuse "bin/rails dbconsole --include-password"

volumes:
  - "app1_storage:/rails/storage"

asset_path: /rails/public/assets

builder:
  arch: amd64

# Each app gets its own Postgres container (accessory).
# Kamal names it "app1-db" (service name + accessory name).
accessories:
  db:
    image: postgres:17
    host: YOUR_SERVER_IP
    env:
      clear:
        POSTGRES_USER: app1
        POSTGRES_DB: app1_production
      secret:
        - POSTGRES_PASSWORD
    directories:
      - data:/var/lib/postgresql/data

Key settings explained

SettingPurpose
serviceUnique name per app. Kamal prefixes containers, volumes, and networks with this.
proxy.ssl + proxy.hostEnables Let’s Encrypt SSL and tells kamal-proxy which hostname routes to this app.
DB_HOST: app1-dbThe hostname of the Postgres accessory on Kamal’s private Docker network.
accessories.dbRuns a dedicated Postgres 17 container.
volumesPersists Active Storage uploads across deploys.
asset_pathBridges fingerprinted assets between deploys to avoid 404s on in-flight requests.

The app2 difference

For app2, change every app1 reference:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
service: app2
image: YOUR_DOCKERHUB_USERNAME/app2
proxy:
  host: app2.example.com
env:
  secret:
    - APP2_DATABASE_PASSWORD
  clear:
    DB_HOST: app2-db
volumes:
  - "app2_storage:/rails/storage"
accessories:
  db:
    env:
      clear:
        POSTGRES_USER: app2
        POSTGRES_DB: app2_production
...

Both apps point accessories.db.host at the same YOUR_SERVER_IP. Kamal runs each accessory as a separate container on that host.


Step 5: Configure the Database (config/database.yml)

Rails 8’s generated database.yml already has the right structure for multi-database production. You only need to add two things to the production primary block:

  1. A password from an environment variable
  2. A host pointing at the Kamal accessory

app1/config/database.yml (production section):

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
production:
  primary: &primary_production
    <<: *default
    database: app1_production
    username: app1
    password: <%= ENV["APP1_DATABASE_PASSWORD"] %>
    host: <%= ENV["DB_HOST"] %>
  cache:
    <<: *primary_production
    database: app1_production_cache
    migrations_paths: db/cache_migrate
  queue:
    <<: *primary_production
    database: app1_production_queue
    migrations_paths: db/queue_migrate
  cable:
    <<: *primary_production
    database: app1_production_cable
    migrations_paths: db/cable_migrate

The &primary_production anchor means cache, queue, and cable inherit host, username, and password from primary, so they all connect to the same Postgres server but use separate databases.

For app2, mirror the same pattern with app2 / APP2_DATABASE_PASSWORD.


Step 6: Enable SSL in Production

Because kamal-proxy terminates SSL, Rails needs to know all requests arrive over HTTPS. Uncomment (or add) these two lines in config/environments/production.rb:

1
2
config.assume_ssl = true
config.force_ssl = true

Without these, Rails may generate http:// URLs and redirect loops can occur behind the proxy.


Step 7: Validate Configuration

Before deploying, sanity-check each app’s config:

1
2
3
4
5
cd app1
bin/kamal config   # should print the resolved YAML without errors

cd ../app2
bin/kamal config

Fix any errors before proceeding.


Step 8: Deploy

Commit your changes first — Kamal builds from your git checkout:

1
2
3
cd app1
git add -A && git commit -m "Configure Kamal deployment"
bin/kamal setup

kamal setup on the first app does everything:

  1. Installs Docker on the VPS (if not present)
  2. Boots the app1-db Postgres accessory
  3. Builds the Docker image locally
  4. Pushes it to Docker Hub
  5. Starts kamal-proxy on ports 80 and 443
  6. Boots the Rails app container
  7. Requests a Let’s Encrypt certificate for app1.example.com

Rails’ Docker entrypoint runs db:prepare on boot, which creates and migrates all four databases automatically.

Then deploy the second app:

1
2
3
cd ../app2
git add -A && git commit -m "Configure Kamal deployment"
bin/kamal setup

The second setup reuses the existing kamal-proxy and Docker installation. It boots app2-db, deploys the app, and registers app2.example.com as a second hostname on the proxy.

Visit https://app1.example.com, https://app2.example.com and both should be live with valid SSL.

Subsequent deploys

After the initial setup, updates are a single command:

1
bin/kamal deploy

How It Works

One proxy, many apps

Kamal 2 uses kamal-proxy instead of Traefik. A single proxy container listens on ports 80 and 443. When you deploy multiple apps to the same server, each app’s proxy.host registers as a routing rule. Incoming requests are matched by Host header and forwarded to the correct app container.

The first app to run kamal setup creates the proxy. Every subsequent app piggybacks on it.

Isolated databases via accessories

Each app’s Postgres runs as a Kamal accessory, which is a separate Docker container managed by Kamal but not part of the app’s deploy lifecycle. Accessories:

  • Have their own persistent data directory on the server
  • Are reachable only on Kamal’s private Docker network (no ports exposed to the internet)
  • Are named {service}-{accessory} — e.g. app1-db, app2-db

This gives you database isolation without running a full database server or managing users/databases manually.

Container naming

Kamal prefixes everything with the service name to avoid collisions:

Resourceapp1app2
App containerapp1-web-*app2-web-*
DB accessoryapp1-dbapp2-db
Storage volumeapp1_storageapp2_storage
Docker networkkamal (shared)kamal (shared)

Day-to-Day Operations

Useful aliases

Both apps ship with Kamal aliases in deploy.yml:

1
2
3
4
5
bin/kamal logs              # Tail application logs
bin/kamal console           # Rails console on the server
bin/kamal shell             # Bash shell inside the container
bin/kamal dbc               # Database console (psql)
bin/kamal app details       # Container status and version

Running migrations

Migrations run automatically on deploy via db:prepare in the Docker entrypoint. To run them manually:

1
bin/kamal app exec "bin/rails db:migrate"

Checking server health

1
2
3
bin/kamal app details
bin/kamal proxy details
bin/kamal accessory details db

Or SSH in directly:

1
2
3
4
ssh root@YOUR_SERVER_IP
docker ps                   # All running containers
docker logs app1-web-<hash> # App logs
docker logs app1-db         # Postgres logs

Adding a Third (or Nth) App

The pattern scales linearly. For app3:

  1. rails new app3 --database=postgresql
  2. Copy the Kamal config pattern from app1, changing all names
  3. Add a DNS A record: app3.example.com → YOUR_SERVER_IP
  4. Export APP3_DATABASE_PASSWORD
  5. bin/kamal setup

No changes needed to existing apps. The proxy picks up the new hostname automatically.


Removing an App

To tear down a test app without affecting others:

1
2
3
4
5
6
7
cd app1

# Remove the Rails app containers
bin/kamal app remove

# Remove the Postgres accessory
bin/kamal accessory remove db

The kamal-proxy and other apps keep running. To also clean up persistent data on the server:

1
2
3
4
ssh root@YOUR_SERVER_IP

docker volume rm app1_storage
rm -rf ~/app1-db

Warning: This permanently deletes the database. Only do this for apps you no longer need.

To remove the shared proxy (only when no apps remain on the server):

1
bin/kamal proxy remove

Troubleshooting

SSL certificate fails on first deploy

Let’s Encrypt can’t issue a cert if DNS doesn’t resolve yet. Wait for propagation, then run bin/kamal deploy again.

Database connection refused

Check that the accessory is running:

1
bin/kamal accessory details db

Common causes:

  • DB_HOST in deploy.yml doesn’t match the accessory name (app1-db, not db or localhost)
  • host: is missing from database.yml production config
  • APP1_DATABASE_PASSWORD and POSTGRES_PASSWORD don’t match

502 Bad Gateway

The app container may still be booting or crashed. Check logs:

1
bin/kamal logs

kamal-proxy uses Rails 8’s health check endpoint /up to know when the app is ready.

“Container not found” on console/shell

The container may have stopped. Check status and redeploy:

1
2
bin/kamal app details
bin/kamal deploy

Uncommitted changes warning

Kamal builds from git. Commit or stash changes before deploying:

1
2
git add -A && git commit -m "Your changes"
bin/kamal deploy

Complete Checklist

Use this as a quick reference when setting up a new app:

  • Rails app created with --database=postgresql
  • DNS A record pointing subdomain to VPS IP
  • KAMAL_REGISTRY_PASSWORD exported (Docker Hub access token)
  • APP_DATABASE_PASSWORD exported (strong random value, saved securely)
  • config/deploy.yml — server IP, proxy host, registry, DB accessory
  • config/database.ymlhost and password in production primary
  • config/environments/production.rbassume_ssl and force_ssl enabled
  • .kamal/secrets — registry password, master key, DB password
  • bin/kamal config passes without errors
  • Changes committed to git
  • bin/kamal setup (first deploy) or bin/kamal deploy (updates)

Summary

Hosting multiple Rails 8 apps on one VPS with Kamal boils down to:

  1. One kamal-proxy routes traffic by subdomain with automatic SSL
  2. One Postgres accessory per app gives isolated databases without extra infrastructure
  3. Unique service names prevent container and volume collisions
  4. Environment variables connect each app to its own database over the private Docker network

Each app deploys independently. The whole setup costs a single VPS(6.59EUR per month for the smallest Hetzner machine cx23) and scales to as many apps as the server can handle.

Bonus

DHH uses 1password for Omarchy, so I am giving it a shot. Here is how to setup a password for the databases:

1
2
op vault create Kamal
op item create --vault Kamal --category login --title "app1" --generate-password

So here is how the new file looks

1
2
3
4
5
6
7
8
SECRETS=$(kamal secrets fetch --adapter 1password --account my.1password.com --from Kamal docker-hub/password app1-db/password)

KAMAL_REGISTRY_PASSWORD=$(kamal secrets extract Kamal/docker-hub ${SECRETS})
APP1_DATABASE_PASSWORD=$(kamal secrets extract Kamal/app1-db ${SECRETS})
POSTGRES_PASSWORD=$APP1_DATABASE_PASSWORD

RAILS_MASTER_KEY=$(cat config/master.key)

Pitfalss

Not commiting stuff

In order for your changes to be “seen” by Kamal, you need to commit them… No need to push, but commiting them is neccessery

Failed to connect to the docker API at unix:///home/$USER/

On kamal setup:

1
2
3
4
5
6
 DEBUG [a9f5e9f0] Command: docker buildx build --output=type=registry --platform linux/amd64 --builder kamal-local-docker-container -t yoiv/supporter:6af216b1008a5c90ef34fa2fffac600e5b96f839 -t yoiv/supporter:latest --label service="supporter" --file Dockerfile . 2>&1
 DEBUG [a9f5e9f0]       ERROR: failed to build: failed to connect to the docker API at unix:///home/mint/.docker/desktop/docker.sock; check if the path is correct and if the daemon is running: dial unix /home/mint/.docker/desktop/docker.sock: connect: no such file or directory
  Finished all in 2.1 seconds
  ERROR (SSHKit::Command::Failed): docker exit status: 256
docker stdout: ERROR: failed to build: failed to connect to the docker API at unix:///home/mint/.docker/desktop/docker.sock; check if the path is correct and if the daemon is running: dial unix /home/mint/.docker/desktop/docker.sock: connect: no such file or directory
docker stderr: Nothing written

Fix: official docs

To create the docker group and add your user:

  1. Create the docker group.
    1
    
    sudo groupadd docker
    
  2. Add your user to the docker group.
    1
    
    sudo usermod -aG docker $USER
    

Log out and log back in so that your group membership is re-evaluated.

If you’re running Linux in a virtual machine, it may be necessary to restart the virtual machine for changes to take effect.

You can also run the following command to activate the changes to groups:

1
newgrp docker

Missing secret_key_base for ‘production’ environment, set this string with bin/rails credentials:edit

Error:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
  INFO [4b10a14d] Running docker container ls --all --filter 'name=^supporter-web-7320b5702cc96aaa02a4de3029c82c9d7ef1d632$' --quiet | xargs docker logs --timestamps 2>&1 on 167.233.112.40
  INFO [4b10a14d] Finished in 0.338 seconds with exit status 0 (successful).
 ERROR 2026-07-21T11:24:35.998574044Z bin/rails aborted!
2026-07-21T11:24:35.999806438Z ArgumentError: Missing `secret_key_base` for 'production' environment, set this string with `bin/rails credentials:edit` (ArgumentError)
2026-07-21T11:24:35.999814678Z
2026-07-21T11:24:35.999817098Z           raise ArgumentError, "Missing `secret_key_base` for '#{Rails.env}' environment, set this string with `bin/rails credentials:edit`"
2026-07-21T11:24:35.999819268Z                 ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
2026-07-21T11:24:35.999895378Z /rails/config/environment.rb:5:in `<main>'
2026-07-21T11:24:35.999952259Z Tasks: TOP => db:prepare => db:load_config => environment
2026-07-21T11:24:35.999975629Z (See full trace by running task with --trace)
2026-07-21T11:24:37.364258126Z bin/rails aborted!
2026-07-21T11:24:37.365486480Z ArgumentError: Missing `secret_key_base` for 'production' environment, set this string with `bin/rails credentials:edit` (ArgumentError)
2026-07-21T11:24:37.365495920Z
2026-07-21T11:24:37.365499290Z           raise ArgumentError, "Missing `secret_key_base` for '#{Rails.env}' environment, set this string with `bin/rails credentials:edit`"
2026-07-21T11:24:37.365502460Z                 ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
2026-07-21T11:24:37.365587760Z /rails/config/environment.rb:5:in `<main>'
2026-07-21T11:24:37.365641300Z Tasks: TOP => db:prepare => db:load_config => environment
2026-07-21T11:24:37.365678260Z (See full trace by running task with --trace)
2026-07-21T11:24:38.844590032Z bin/rails aborted!
2026-07-21T11:24:38.846401048Z ArgumentError: Missing `secret_key_base` for 'production' environment, set this string with `bin/rails credentials:edit` (ArgumentError)
2026-07-21T11:24:38.846412798Z
2026-07-21T11:24:38.846416948Z           raise ArgumentError, "Missing `secret_key_base` for '#{Rails.env}' environment, set this string with `bin/rails credentials:edit`"
2026-07-21T11:24:38.846421428Z                 ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
2026-07-21T11:24:38.846558068Z /rails/config/environment.rb:5:in `<main>'
2026-07-21T11:24:38.846648628Z Tasks: TOP => db:prepare => db:load_config => environment
2026-07-21T11:24:38.846699589Z (See full trace by running task with --trace)
2026-07-21T11:24:40.483993732Z bin/rails aborted!
2026-07-21T11:24:40.485458137Z ArgumentError: Missing `secret_key_base` for 'production' environment, set this string with `bin/rails credentials:edit` (ArgumentError)
2026-07-21T11:24:40.485467527Z
2026-07-21T11:24:40.485470847Z           raise ArgumentError, "Missing `secret_key_base` for '#{Rails.env}' environment, set this string with `bin/rails credentials:edit`"
2026-07-21T11:24:40.485474067Z                 ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
2026-07-21T11:24:40.485586627Z /rails/config/environment.rb:5:in `<main>'
2026-07-21T11:24:40.485638968Z Tasks: TOP => db:prepare => db:load_config => environment
2026-07-21T11:24:40.485662468Z (See full trace by running task with --trace)
2026-07-21T11:24:42.596931672Z bin/rails aborted!
2026-07-21T11:24:42.598150136Z ArgumentError: Missing `secret_key_base` for 'production' environment, set this string with `bin/rails credentials:edit` (ArgumentError)
2026-07-21T11:24:42.598159436Z
2026-07-21T11:24:42.598162746Z           raise ArgumentError, "Missing `secret_key_base` for '#{Rails.env}' environment, set this string with `bin/rails credentials:edit`"
2026-07-21T11:24:42.598165966Z                 ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
2026-07-21T11:24:42.598243287Z /rails/config/environment.rb:5:in `<main>'
2026-07-21T11:24:42.598291737Z Tasks: TOP => db:prepare => db:load_config => environment
2026-07-21T11:24:42.598316017Z (See full trace by running task with --trace)
2026-07-21T11:24:45.446263631Z bin/rails aborted!
2026-07-21T11:24:45.447465006Z ArgumentError: Missing `secret_key_base` for 'production' environment, set this string with `bin/rails credentials:edit` (ArgumentError)
2026-07-21T11:24:45.447474336Z
2026-07-21T11:24:45.447477766Z           raise ArgumentError, "Missing `secret_key_base` for '#{Rails.env}' environment, set this string with `bin/rails credentials:edit`"
2026-07-21T11:24:45.447480996Z                 ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
2026-07-21T11:24:45.447574226Z /rails/config/environment.rb:5:in `<main>'
2026-07-21T11:24:45.447614166Z Tasks: TOP => db:prepare => db:load_config => environment
2026-07-21T11:24:45.447657626Z (See full trace by running task with --trace)
2026-07-21T11:24:49.899540493Z bin/rails aborted!
2026-07-21T11:24:49.900780087Z ArgumentError: Missing `secret_key_base` for 'production' environment, set this string with `bin/rails credentials:edit` (ArgumentError)
2026-07-21T11:24:49.900790237Z
2026-07-21T11:24:49.900793567Z           raise ArgumentError, "Missing `secret_key_base` for '#{Rails.env}' environment, set this string with `bin/rails credentials:edit`"
2026-07-21T11:24:49.900796787Z                 ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
2026-07-21T11:24:49.900885488Z /rails/config/environment.rb:5:in `<main>'
2026-07-21T11:24:49.900935008Z Tasks: TOP => db:prepare => db:load_config => environment
2026-07-21T11:24:49.900981878Z (See full trace by running task with --trace)
2026-07-21T11:24:57.560624950Z bin/rails aborted!
2026-07-21T11:24:57.561809375Z ArgumentError: Missing `secret_key_base` for 'production' environment, set this string with `bin/rails credentials:edit` (ArgumentError)
2026-07-21T11:24:57.561818395Z
2026-07-21T11:24:57.561821715Z           raise ArgumentError, "Missing `secret_key_base` for '#{Rails.env}' environment, set this string with `bin/rails credentials:edit`"
2026-07-21T11:24:57.561824855Z                 ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
2026-07-21T11:24:57.561942345Z /rails/config/environment.rb:5:in `<main>'
2026-07-21T11:24:57.561947665Z Tasks: TOP => db:prepare => db:load_config => environment
2026-07-21T11:24:57.561950795Z (See full trace by running task with --trace)
  INFO [ecb1fcd7] Running docker container ls --all --filter 'name=^supporter-web-7320b5702cc96aaa02a4de3029c82c9d7ef1d632$' --quiet | xargs docker inspect --format '' on 167.233.112.40
  INFO [ecb1fcd7] Finished in 0.276 seconds with exit status 0 (successful).
 ERROR null
  INFO [46fbab4d] Running docker container ls --all --filter 'name=^supporter-web-7320b5702cc96aaa02a4de3029c82c9d7ef1d632$' --quiet | xargs docker stop on 167.233.112.40
  INFO [46fbab4d] Finished in 0.311 seconds with exit status 0 (successful).
  Finished all in 53.6 seconds
Releasing the deploy lock...
  Finished all in 55.7 seconds
  ERROR (SSHKit::Command::Failed): Exception while executing on host 167.233.112.40: docker exit status: 1
docker stdout: Nothing written
docker stderr: Error: target failed to become healthy within configured timeout (30s)

It says it in the logs, but just set secret_key_base for ‘production’ environment, set this string with EDITOR="code --wait" rails credentials:edit -e production

1
secret_key_base: "some random secure symbols"

Using pgvector

In order to use a vector db we need a different Postgress image.

1
2
3
accessories:
  db:
    image: pgvector/pgvector:pg17 # instead of postgres:17
1
bin/kamal accessory reboot db

this gave me this:

1
2
ERROR 2026-07-21T13:32:29.959762691Z WARNING:  database "supporter_production" has a collation version mismatch
2026-07-21T13:32:29.959781511Z DETAIL:  The database was created using collation version 2.41, but the operating system provides version 2.36.

PostgreSQL will still start and Rails will still connect. It usually happens when: the database was created using one Linux image/libc version, then you switched to another image (for example, from postgres:17 to pgvector/pgvector:pg17).

Real fix (when we don’t have production data). We do this only if it is our initial deploy.

1
2
kamal accessory remove db
kamal accessory boot db
This post is licensed under CC BY-NC 4.0 by the author.