Hanami 2 ist erschienen, aber die Dokumentation zur Docker-Integration ist dünn. Wer eine Hanami-API mit PostgreSQL aufsetzen will, ohne lokale Abhängigkeiten zu installieren, findet hier eine komplette Anleitung.

Voraussetzungen: Ruby und Hanami installieren

Ruby muss mindestens in Version 3.0.0 vorliegen. Dann den hanami Gem installieren:

gem install hanami

Neue Hanami-Anwendung erstellen:

hanami new simple_api

Dieser Befehl erstellt eine Reihe von Dateien im Verzeichnis simple_api.

Wie sieht das Dockerfile aus?

FROM ruby:3.2.1-alpine

RUN apk add --update build-base ruby-dev

WORKDIR /app

COPY Gemfile Gemfile.lock ./
RUN bundle install

COPY . .

EXPOSE 2300

CMD ["hanami", "server"]

Wir verwenden ruby:3.2.1-alpine, um ein schlankes Image zu erhalten. Dafür sind build-base und ruby-dev nötig. Hanami verwendet standardmässig Port 2300 (nicht 3000 wie Rails).

Image bauen:

docker build -t simple-api -f Dockerfile.dev .

Wie verbindet Docker Compose App und Datenbank?

# 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"
    ports:
      - "2300:2300"
    depends_on:
      - db
    env_file: .env

Zwei Dienste: db für PostgreSQL und app für den Webserver. Die Umgebungsvariable POSTGRES_PASSWORD gehört in die .env-Datei:

# simple_api/.env
POSTGRES_PASSWORD=XYZ123QWE

Container starten:

docker-compose up

Wie wird die Datenbankverbindung konfiguriert?

Zuerst die nötigen Gems zum Gemfile hinzufügen:

# simple_api/Gemfile
gem "rom", "~> 5.3"
gem "rom-sql", "~> 3.6"
gem "pg"

Dann postgresql-dev im Dockerfile ergänzen:

RUN apk add --update build-base ruby-dev postgresql-dev

Container stoppen, Image neu bauen, Container neu starten. Dann die Datenbank erstellen:

docker-compose exec db psql -U postgres -c "CREATE DATABASE simple_api_development;"

Jetzt den Persistenzanbieter in Hanami konfigurieren:

# 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

target["settings"].database_url verweist auf die Umgebungsvariable DATABASE_URL. Diese in .env ergänzen:

# simple_api/.env
DATABASE_URL=postgres://postgres:XYZ123QWE@db:5432/simple_api_development
POSTGRES_PASSWORD=XYZ123QWE

Die Einstellung database_url in der Settings-Klasse registrieren:

# simple_api/config/settings.rb
module SimpleAPI
  class Settings < Hanami::Settings
    setting :database_url, constructor: Types::String
  end
end

Image neu bauen und Container neu starten.

Funktioniert die Verbindung?

Zum Test: Migration erstellen, Daten einfügen, Daten lesen.

Rake-Migrationen aktivieren:

# 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

Migration für eine Tabelle:

# 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

Relation definieren:

# 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

Image neu bauen, Container starten, dann in die Shell verbinden:

docker-compose exec app sh
bundle exec rake db:migrate
hanami console
app["persistence.rom"].relations[:books].insert(title: 'The Alloy of Law', author: 'Brandon Sanderson')
app["persistence.rom"].relations[:books].to_a

Die Hanami-Anwendung ist mit PostgreSQL verbunden. Weitere Informationen im Hanami 2.0 Getting Started Guide.

Praktische Umsetzung: Der USEO-Ansatz

Docker Compose ist bei uns Standard für lokale Entwicklungsumgebungen. Drei Erkenntnisse aus dem produktiven Einsatz:

  • Health Checks statt depends_on. Die depends_on-Direktive wartet nur auf den Container-Start, nicht auf die Datenbank-Bereitschaft. Wir verwenden Health Checks oder ein Wrapper-Skript (wait-for-it.sh), das die Datenbankverbindung prüft, bevor der App-Server startet.
  • Volumes für Gem-Cache. Statt bei jedem Image-Build alle Gems neu zu installieren, mounten wir ein Volume für das Bundle-Verzeichnis. Das reduziert die Build-Zeit bei Gem-Änderungen von Minuten auf Sekunden.
  • Separate Compose-Dateien pro Umgebung. docker-compose.yml für die Basiskonfiguration, docker-compose.dev.yml für Entwickler-Overrides (z.B. Source-Code-Mounting, Debug-Ports). Das hält die Konfiguration sauber und erlaubt umgebungsspezifische Anpassungen.