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.