Skip to content
Mick Zijdel

How To Set Up A Postgres Database Accessory Container with Kamal 2 in Ruby on Rails

May 17, 2025 12 min read

If you are confused by this guide or notice a mistake, please get in touch so I can fix it.

Kamal is one of my favourite parts of Ruby on Rails 8, but when you generate a new Rails project with postgres (using rails new <your-service-name> --database=postgresql), this does not automatically set up a postgres accessory container correctly for Kamal 2. Fortunately, it’s just a few small changes to get it working. This is not meant to be a comprehensive introduction to Kamal, but if you follow along, you should have a working setup by the end of it.

You can either download these new template files and drop them into your project, or follow the instructions below. I recommend committing the Rails generated files first and carefully looking at the git diff to make sure the changes still make sense with newer versions than Rails 8.0.2, Postgres 15, and Kamal 2.6.1.

Sidenote: Why use postgres and not sqlite? I love the simplicity of sqlite3, but I kept getting errors due to simultaneous read/write operations, so I just use postgres by default.

Setting up your server for Kamal

This is outside the scope of this tutorial (I might write it up later if I ever do this again), but a quick summary is: Buy VPS hosting, install the most recent Ubuntu Long-Term Support, set up authorization using ssh keys, disable password login, create a deploy user and add them to the docker group, set up authorization using ssh key for that too, set up a firewall.

For more detail, this script from this tutorial looks good, but I have not tried and vetted it.

config/database.yml

In these files, replace all templates, such as <your-service-name> with the hyphenated name of your Rails app, such as my-blog.

Replace the production part of your config/database.yml file with this:

production:
  primary: &primary_production
    <<: *default
    username: <%= ENV["POSTGRES_USER"] %>
    database: <%= ENV["POSTGRES_DB"] %>
    password: <%= ENV["POSTGRES_PASSWORD"] %>
    host: <%= ENV["DB_HOST"] %>
    port: <%= ENV["POSTGRES_DB_PORT"] %>
  cache:
    <<: *primary_production
    database: <%= ENV["POSTGRES_DB"] %>_cache
    migrations_paths: db/cache_migrate
  queue:
    <<: *primary_production
    database: <%= ENV["POSTGRES_DB"] %>_queue
    migrations_paths: db/queue_migrate
  cable:
    <<: *primary_production
    database: <%= ENV["POSTGRES_DB"] %>_cable
    migrations_paths: db/cable_migrate

config/deploy.yml

Now open the Kamal config file at config/deploy.yml and define all those environment variables above by replacing the top of the env: section with this:

env:
  secret:
    - RAILS_MASTER_KEY
    - POSTGRES_PASSWORD
  clear:
    # Run the Solid Queue Supervisor inside the web server's Puma process to do jobs.
    # When you start using multiple servers, you should split out job processing to a dedicated machine.
    SOLID_QUEUE_IN_PUMA: true

    DB_HOST: <your-service-name>-postgres
    POSTGRES_USER: <your-service-name>
    POSTGRES_DB: <your-service-name>_production

And setup the postgres accessory by overriding the commented out section defining the db accessory. This cannot access the env variables you defined for the main service container, so we need to define them again.

accessories:
  postgres:
    image: postgres:15
    host: <your-server-ip>
    # If you define a port here, that port will be accessible from the host machine.
    # This means that you cannot use the same port for multiple services/accessories.
    # This will cause issues if you have multiple services with a postgres accessory,
    # and you will need to modify which port each accessory uses by modifying the number before the :.
    # You can still access the database using `<your-service-name>-postgres` as the hostname and `5432` as the port
    # regardless of which port you expose here.
    # Warning: If you remove the `127.0.0.1` part, this port is exposed to the world.
    # port: "127.0.0.1:5432:5432"
    env:
      clear:
        POSTGRES_USER: <your-service-name>
      secret:
        - POSTGRES_PASSWORD
    files:
      - db/production_setup.sql:/docker-entrypoint-initdb.d/setup.sql
    directories:
      - data:/var/lib/postgresql/data

Bonus: If you followed the advice to use a dedicated deploy user above, make sure to set it by looking for the commented out line starting with # ssh:.

.kamal/secrets and credentials/postgres.key

Now we need to include the postgres password. I like to store this in a file, so add the following line to the .kamal/secrets file:

POSTGRES_PASSWORD=$(cat config/credentials/postgres.key)

Create a new folder in config called credentials, and create a new file called postgres.key in this new folder. The contents should just be the password you want to use for your default postgres user. Generate one using a password manager or similar.

These key files should not be in your Version Control, so include the line config/credentials/* in your .gitignore file.

Bonus: I do not like the default way of fetching the registry password, so I put this in another key file at config/credentials/kamal_registry_password.key. If you do not know how to get the registry token, scroll down to the bonus section for an explanation.

db/production_setup.sql

Create a file called production_setup.sql in the db folder and add a CREATE DATABASE command for each server you use in production as listed in database.yml, for example:

CREATE DATABASE <your-service-name>_production;
CREATE DATABASE <your-service-name>_production_cache;
CREATE DATABASE <your-service-name>_production_queue;
CREATE DATABASE <your-service-name>_production_cable;

Deploy

Before deploying, make sure the changes to your database.yml file are committed, or the app will try to use the old version, and the deployment will fail.

You can setup Kamal, deploy the accessory, and deploy the app by running

kamal setup

If Kamal is already set up on the server you’re deploying to, you can also separately deploy the accessory by running kamal accessory boot postgres and deploy the main service using kamal deploy.

Did it not work? Check below for troubleshooting.

Troubleshooting

Troubleshooting: Permission denied during rails assets:precompile

If you get a permission denied error when precompiling assets during build, like this:

#17 [build 6/6] RUN SECRET_KEY_BASE_DUMMY=1 ./bin/rails assets:precompile
#17 0.078 /bin/sh: 1: ./bin/rails: Permission denied
#17 ERROR: process "/bin/sh -c SECRET_KEY_BASE_DUMMY=1 ./bin/rails assets:precompile" did not complete successfully: exit code: 126
------
 > [build 6/6] RUN SECRET_KEY_BASE_DUMMY=1 ./bin/rails assets:precompile:
0.078 /bin/sh: 1: ./bin/rails: Permission denied
------
Dockerfile:49
--------------------
  47 |     
  48 |     # Precompiling assets for production without requiring secret RAILS_MASTER_KEY
  49 | >>> RUN SECRET_KEY_BASE_DUMMY=1 ./bin/rails assets:precompile
  50 |     
  51 |     
--------------------
ERROR: failed to solve: process "/bin/sh -c SECRET_KEY_BASE_DUMMY=1 ./bin/rails assets:precompile" did not complete successfully: exit code: 126
docker stderr: Nothing written

Find the following line that runs precompile at roughly line 48:

# Precompiling assets for production without requiring secret RAILS_MASTER_KEY
RUN SECRET_KEY_BASE_DUMMY=1 ./bin/rails assets:precompile

and add the following statement before:

# Adjust binfiles to be executable on Linux
RUN chmod +x bin/* && \
    sed -i "s/\r$//g" bin/* && \
    sed -i 's/ruby\.exe$/ruby/' bin/*

You need to commit this change before it will work!

Troubleshooting: password authentication failed for user

If you get an error like

PG::ConnectionBad: connection to server at "172.18.0.9", port 5432 failed: FATAL:  password authentication failed for user "<your-service-name>" (PG::ConnectionBad)

that likely means you are not using the same username and/or password in both your env definition and accessory definition. Double-check this. If you do notice a mistake in the accessory definition, you need to remove the old version by running kamal accessory remove postgres. If you get the error:

  ERROR (SSHKit::Command::Failed): Exception while executing on host <host-ip>: rm exit status: 1
rm stdout: Nothing written
rm stderr: rm: cannot remove '<your-service-name>-postgres/data': Permission denied

you need to SSH into the host server as root and find the <your-service-name>-postgres folder in the deploy user’s home folder ( cd /home/deploy), and remove it by:

rm -rf <your-service-name>-postgres

Once you’ve done this, you can run the deployment again:

kamal setup

Troubleshooting: Cannot deploy another postgres accessory because the port 5432 is already used

If you set a full port mapping (if you follow this tutorial, you will not have done this unless you explicitly set a value for port: in the accessory section of deploy.yml) to expose the postgres accessory as a port on the server and then tried to deploy another postgres accessory container or already had a postgres server installed on the server (if this is you, ask yourself why you have a postgres database directly on the server and another one in a container, and see if you get a satisfying answer), you’ve likely been hit by a long, confusing error that ends in this: docker: Error response from daemon: failed to set up container networking: driver failed programming external connectivity on endpoint \<your-service-name\>-postgres (\<long-string-of-numbers\>): Bind for 127.0.0.1:5432 failed: port is already allocated

To get around this, you need to change on which host machine port you expose the accessory. For example, if you had 127.0.0.1:5432:5432, you can expose it on port 5433 instead using 127.0.0.1:5433:5432. You can also still access the database from another container using <container-name>:5432 regardless of which host machine port you expose it to.

Bonus: How to manually create the user/role

When something goes wrong passing the POSTGRES_PASSWORD and POSTGRES_USER when creating the container, you might end up without a user. No problem, we’ll just create it manually:

Step 1: SSH into your Kamal server

ssh deploy@your-server-host

Step 2: Access the bash shell of your postgres accessory container. It needs to be booted and running for this to work, and the user you do this with needs to be root or in the docker group.

docker exec -it \<container-name\> bash

Step 3: Now you’re in the container shell, open up the postgres shell as the default/root user called postgres:

psql -U postgres

Step 4: And now you can create the missing user with the same values as for the environment values you’re using in your deploy.yml:

CREATE USER <POSTGRES_USER> WITH SUPERUSER PASSWORD '<POSTGRES_PASSWORD>' CREATEDB;

Warning: For production, you do not want to just have a user with superuser permissions, but be more fine-grained. This is a whole separate topic so I will not cover it here.

Bonus

Bonus: Getting a registry token

You can host your own registry, but the easiest way is to sign up for Docker Hub, go to ‘Account Settings’, and create a Personal Access Token with ‘Read & Write’ permissions. Copy this token into the kamal_registry_password.key file you created earlier, or set it as an environment variable.

You can verify that everything works by running kamal registry login.

Bonus: Updating the development part of database.yml

I like to define my development database credentials in the Rails credentials file. If you want to do this too, replace the default: and development: sections of the file with:

# Warning: This is different from the default, even though it does not affect production.
# I prefer storing my development database credentials in the Rails credentials, but use whatever method you prefer.
default: &default
  adapter: postgresql
  encoding: unicode
  # For details on connection pooling, see Rails configuration guide
  # https://guides.rubyonrails.org/configuring.html#database-pooling
  pool: <%= ENV.fetch("RAILS_MAX_THREADS") { 5 } %>
  username: <%= Rails.application.credentials.dig(:database, :username) %>
  password: <%= Rails.application.credentials.dig(:database, :password) %>
  host: localhost

development:
  <<: *default
  database: <your-service-name>_development

  # The TCP port the server listens on. Defaults to 5432.
  # If your server runs on a different port number, change accordingly.
  #port: 5432

  # Schema search path. The server defaults to $user,public
  #schema_search_path: myapp,sharedapp,public

  # Minimum log levels, in increasing order:
  #   debug5, debug4, debug3, debug2, debug1,
  #   log, notice, warning, error, fatal, and panic
  # Defaults to warning.
  #min_messages: notice

# Warning: The database defined as "test" will be erased and
# re-generated from your development database when you run "rake".
# Do not set this db to the same as development or production.

and define the development and test database credentials in rails credentials:edit like so:

database:
  username: <local-database-username>
  password: <local-database-password>

Subscribe to the Newsletter

Enjoyed this article? Subscribe to get notified when I publish new content.

No spam. Unsubscribe at any time. Or let's RSS.