Using Postgres in Docker with Hanami
I have started to move my database dependencies into Docker, because this makes it so mach easier and cleaner to deal with different database versions for different applications. To get this to work as smoothly as possible with Hanami, I have written down this step by step guide, so you can follow along at home.
For this to work, I am using mise-en-place to automatically set the path when I change into the code directory. direnv probably has a similar functionality.
Starting with a new app#
We are going to start the process from scratch with a fresh new app.
hanami new torment_nexus --database=postgres
Switch to the app directory.
cd torment_nexus
Since I am using mise to manage my ruby versions, let’s pin the version to the one current at the time of this writing.
echo 3.4.7 > .ruby-version
Setting up the shims#
We’ll set up the shims for the binaries that Hanami uses for dealing with Postgres, so that they redirect to the binaries inside the Postgres docker container. All of theses shims reside in the ./bin directory.
psql#
bin/psql
#!/bin/sh
C=''
for i in "$@"; do
case "$i" in
*\'*)
i=$(printf "%s" "$i" | sed "s/'/'\"'\"'/g")
;;
*) : ;;
esac
C="$C '$i'"
docker compose -f docker-compose-dev.yml exec -e "PGPASSWD=${DATABASE_PASSWORD}" -e "PGUSER=${DATABASE_USER}" postgres sh -c "exec psql $C"createdb#
bin/createdb
#!/bin/sh
C=''
for i in "$@"; do
case "$i" in
*\'*)
i=$(printf "%s" "$i" | sed "s/'/'\"'\"'/g")
;;
*) : ;;
esac
C="$C '$i'"
done
docker compose -f docker-compose-dev.yml exec -e "PGPASSWD=${DATABASE_PASSWORD}" -e "PGUSER=${DATABASE_USER}" postgres sh -c "exec createdb ${C}"pg_dump#
bin/pg_dump
#!/bin/sh
C=''
for i in "$@"; do
case "$i" in
*\'*)
i=$(printf "%s" "$i" | sed "s/'/'\"'\"'/g")
;;
*) : ;;
esac
C="$C '$i'"
done
docker compose -f docker-compose-dev.yml exec -e "PGPASSWD=${DATABASE_PASSWORD}" -e "PGUSER=${DATABASE_USER}" postgres sh -c "exec pg_dump ${C}"After we have created all the shims, we make them executable:
chmod 755 bin/pg_dump bin/psql bin/createdb
Setting up mise#
This can go either into mise.toml or mise.local.toml. I usually put it into mise.local.toml. That one is in my global .gitignore file, and this configuration is just meant for my local development machine.
Since Hanami is already using the standard direnv files , we’ll make them available to the shell as well.
.mise.local.toml
[env]
_.file = [".env", ".env.development", ".env.local", ".env.development.local"]
_.path = ["./bin"]We’re putting all the database-related environment variables into .env.local so that they don’t accidentally end up in the git repository.
.env.local
DATABASE_PASSWORD=SuperSecretPassword
DATABASE_USER=torment-nexus
DATABASE_NAME=torment-nexus
DATABASE_HOST=localhost
DATABASE_URL="postgres://${DATABASE_USER}:${DATABASE_PASSWORD}@${DATABASE_HOST}/${DATABASE_NAME}"
PGPASSWORD=${DATABASE_PASSWORD}Setting up docker compose#
We’ll put the development compose configuration into a separate docker-compose-dev.yml file:
docker-compose-dev.yml
x-database-password: &database-password ${DATABASE_PASSWORD:-password}
x-database-port: &database-port ${DATABASE_PORT:-5432}
x-database-user: &database-user ${DATABASE_USER:-postgres}
x-env-files: &env-files
env_file:
- path: .env
required: true
- path: .env.local
required: true
name: torment_nexus
services:
postgres:
<<: *env-files
container_name: torment_nexus-postgres
environment:
PGDATA: /var/lib/postgresql/data/pgdata
PGPORT: *database-port
POSTGRES_PASSWORD: *database-password
POSTGRES_USER: *database-user
healthcheck:
interval: 10s
retries: 5
test: sh -c "pg_isready -U $$POSTGRES_USER -p $$PGPORT"
timeout: 5s
image: postgres:18
ports:
- mode: host
protocol: tcp
published: *database-port
target: *database-port
volumes:
- db-data:/var/lib/postgresql/data
volumes:
db-data:
name: torment_nexus-postgres-data-18Setting up Hanami dev environment#
We’ll add the database to to Procfile.dev so it is always started together with the development web server:
Procfile.dev
web: bundle exec hanami server
assets: bundle exec hanami assets watch
db: docker compose -f docker-compose-dev.yml upIf you don’t need to start up the whole dev environment, only the database, you can use ./bin/dev db
Putting it all together#
To set up the database, we’ll have to do a little dance one time:
- Pull the postgres image if it does not exist locally:
docker-compose -f docker-compose-dev.yml pull - Start up the database container temporarily:
docker-compose -f docker-compose-dev.yml up - Open another terminal, change into the code directory and run
bundle exec hanami db create - Stop the temporary database container with
docker-compose -f docker-compose-dev.yml up
From now on we can use hanami dev as we are used to.
Testing it out#
We start the dev environment back up in another terminal with bundle exec hanami dev
Let’s generate a simple first migration:
bundle exec hanami generate migration create_entries
config/db/migrate/20251124134551_create_entries.rb
ROM::SQL.migration do
# Add your migration here.
# # See https://guides.hanamirb.org/v2.2/database/migrations/ for details.
change do
create_table :entries do
primary_key :id
column :title, String, null: false
column :content, String, null: false
column :created_at, Time, null: false
column :modified_at, Time, null: false
end
end
endWhen we run hanami db migrate, it should work without a problem.
If there are problems and you need help, you can reach me via Mastodon at @rickenharp@popkulturreferenz.de or on Bluesky at @rickrickenharp.bsky.social. I also hang out on the Hanami Discord.