Hanami 2 requires Ruby 3.0+ and ships with ROM for database access, but the official docs leave Docker setup as an exercise. This guide walks through the full configuration: Dockerfile, Compose file, persistence provider, and a working migration.
Prerequisites
- Ruby 3.0+
- Docker and Docker Compose installed
- Basic familiarity with Hanami and PostgreSQL
Install the Hanami gem and generate a new application:
gem install hanami
hanami new simple_api
How to write the Dockerfile
Create a lightweight Dockerfile using the Alpine Ruby image:
# simple_api/Dockerfile.dev
FROM ruby:3.2.1-alpine
RUN apk add --no-cache build-base ruby-dev
WORKDIR /app
COPY Gemfile Gemfile.lock ./
RUN bundle install
COPY . .
EXPOSE 2300
CMD ["hanami", "server", "--host", "0.0.0.0"]
Hanami’s default port is 2300 (unlike Rails’ 3000). The Alpine image keeps the image small, but requires build-base and ruby-dev for native extensions.
Build the image:
docker build -t simple-api -f Dockerfile.dev .
How to configure Docker Compose
# simple_api/docker-compose.yaml
version: '3.8'
volumes:
postgres-data:
services:
db:
image: postgres
volumes:
- postgres-data:/var/lib/postgresql/data
env_file: .env
app:
image: simple-api
command: sh -c "hanami server --host 0.0.0.0"
ports:
- "2300:2300"
depends_on:
- db
env_file: .env
Add environment variables:
# simple_api/.env
POSTGRES_PASSWORD=XYZ123QWE
Start the containers:
docker-compose up
How to connect Hanami to PostgreSQL
Add the required gems:
# simple_api/Gemfile
gem "rom", "~> 5.3"
gem "rom-sql", "~> 3.6"
gem "pg"
Update the Dockerfile to include the PostgreSQL dev library:
RUN apk add --no-cache build-base ruby-dev postgresql-dev
Rebuild the image, then create the database:
docker-compose exec db psql -U postgres -c "CREATE DATABASE simple_api_development;"
Register the persistence provider
# simple_api/config/providers/persistence.rb
Hanami.app.register_provider :persistence, namespace: true do
prepare do
require "rom"
config = ROM::Configuration.new(:sql, target["settings"].database_url)
register "config", config
register "db", config.gateways[:default].connection
end
start do
config = target["persistence.config"]
config.auto_registration(
target.root.join("lib/simple_api/persistence"),
namespace: "SimpleAPI::Persistence"
)
register "rom", ROM.container(config)
end
end
Update the .env with the full database URL:
# simple_api/.env
DATABASE_URL=postgres://postgres:XYZ123QWE@db:5432/simple_api_development
POSTGRES_PASSWORD=XYZ123QWE
Add the setting to the Hanami settings class:
# simple_api/config/settings.rb
module SimpleAPI
class Settings < Hanami::Settings
setting :database_url, constructor: Types::String
end
end
Rebuild the image and restart containers.
How to verify the connection with a migration
Enable Rake migrations:
# simple_api/Rakefile
require "rom/sql/rake_task"
task :environment do
require_relative "config/app"
require "hanami/prepare"
end
namespace :db do
task setup: :environment do
Hanami.app.prepare(:persistence)
ROM::SQL::RakeSupport.env = Hanami.app["persistence.config"]
end
end
Create a migration:
# simple_api/db/migrate/20230228200134_create_books.rb
ROM::SQL.migration do
change do
create_table :books do
primary_key :id
column :title, :text, null: false
column :author, :text, null: false
end
end
end
Define a relation:
# simple_api/lib/simple_api/persistence/relations/books.rb
module SimpleAPI
module Persistence
module Relations
class Books < ROM::Relation[:sql]
schema(:books, infer: true)
end
end
end
end
Rebuild, restart, then run the migration and test:
docker-compose exec app sh
bundle exec rake db:migrate
Open the Hanami console and verify:
app["persistence.rom"].relations[:books].insert(title: 'The Alloy of Law', author: 'Brandon Sanderson')
app["persistence.rom"].relations[:books].to_a
# => [{id: 1, title: "The Alloy of Law", author: "Brandon Sanderson"}]
The connection works. For more details, see the Hanami 2.0 getting started guide.
Practical Implementation: The USEO Approach
We run Hanami services alongside Rails monoliths in our microservice setups. The key Docker Compose pattern we rely on is a shared network with explicit service aliases. When a Hanami API and a Rails app both need the same Postgres instance, we define the database as a standalone service in a shared docker-compose.override.yml and reference it by alias. This avoids running duplicate Postgres containers during local development.
For the persistence provider, we add a health check step before the app container starts. Hanami will crash on boot if Postgres is not ready, and depends_on alone does not guarantee that. We use a small shell script that loops pg_isready before launching the Hanami server. This eliminates the race condition that depends_on leaves open.
We also version-pin the Postgres image (e.g., postgres:15-alpine instead of postgres:latest) and match it to our production version. This prevents subtle behavioral differences between local and deployed environments, especially around default authentication and JSON handling changes between major Postgres releases.