Back to Blog
Tutorial11 min read2026-06-30

How to Deploy a Hanami Ruby App with PostgreSQL

Hanami 2 is a modern, modular Ruby framework with a clean architecture. Learn how to deploy it to production with PostgreSQL, ROM migrations, asset precompilation, and Puma.

Ajay Kumar
Ajay Kumar
Founder & DevOps, PandaStack

Hanami 2 is a refreshing take on Ruby web development — explicit dependency injection, a slice-based architecture, and ROM (Ruby Object Mapper) instead of Active Record. It's less ubiquitous than Rails, which means deployment guides are scarce. This article covers a production container deployment of Hanami 2 with a managed PostgreSQL database.

What makes Hanami deployment different

Unlike Rails, Hanami doesn't ship a kitchen-sink convention. Key things to know for production:

  • Hanami uses ROM with the hanami-db integration; migrations are managed via hanami db migrate.
  • The app boots through a container (Hanami.app) with explicit providers.
  • Assets are handled by hanami-assets, which wraps esbuild — you precompile at build time.
  • The web server is Puma, configured in config/puma.rb.

Database configuration

Hanami's database connection is driven by the DATABASE_URL environment variable. In config/app.rb or via the hanami-db provider, the connection is read from the environment, so you rarely hardcode anything:

# .env.production (values come from real env vars in prod)
DATABASE_URL=postgres://user:pass@host:5432/myapp

With a managed PostgreSQL instance, you link it and the platform injects DATABASE_URL. Hanami's ROM setup reads it directly. The one adjustment: ROM's pg adapter expects the postgres:// scheme, which managed providers typically use, so no rewriting is needed (unlike Doctrine in PHP).

Production Dockerfile

Because Hanami precompiles assets with esbuild, you need Node available at build time. A multi-stage build keeps the runtime image lean:

# ---- Build stage ----
FROM ruby:3.3-slim AS build

RUN apt-get update -qq && apt-get install -y --no-install-recommends \
    build-essential libpq-dev curl gnupg \
 && curl -fsSL https://deb.nodesource.com/setup_20.x | bash - \
 && apt-get install -y nodejs \
 && rm -rf /var/lib/apt/lists/*

WORKDIR /app

COPY Gemfile Gemfile.lock ./
RUN bundle config set --local without 'development test' \
 && bundle install --jobs 4

COPY package.json package-lock.json ./
RUN npm ci

COPY . .
ENV HANAMI_ENV=production
RUN bundle exec hanami assets compile

# ---- Runtime stage ----
FROM ruby:3.3-slim
RUN apt-get update -qq && apt-get install -y --no-install-recommends libpq5 \
 && rm -rf /var/lib/apt/lists/*
WORKDIR /app
COPY --from=build /usr/local/bundle /usr/local/bundle
COPY --from=build /app /app
ENV HANAMI_ENV=production
EXPOSE 2300
CMD ["bundle", "exec", "puma", "-C", "config/puma.rb"]

libpq-dev is needed to compile the pg gem in the build stage; libpq5 is the lighter runtime library.

Puma configuration

# config/puma.rb
max_threads = ENV.fetch('PUMA_MAX_THREADS', 5).to_i
threads 1, max_threads
port ENV.fetch('PORT', 2300)
environment ENV.fetch('HANAMI_ENV', 'development')
workers ENV.fetch('WEB_CONCURRENCY', 2).to_i
preload_app!

As with any container deploy, the PORT env var is authoritative. Hanami's default dev port is 2300, but in production you bind whatever the platform injects.

Running migrations

ROM migrations live in config/db/migrate/. Run them as a release step, never during the image build:

bundle exec hanami db migrate

If you're seeding reference data, do it in a separate idempotent step. On PandaStack, wire hanami db migrate as a one-off job or a once-per-release cronjob so the database is migrated before the new version takes traffic.

Secrets and sessions

Hanami uses a session secret for signed/encrypted cookies. Set it via env var:

HANAMI_WEB_SESSIONS_SECRET=$(bundle exec hanami secret)

Generate it once and store it as a stable secret. As with any framework, a rotating secret logs everyone out on each deploy.

Environment variable summary

VariablePurpose
HANAMI_ENVproduction
DATABASE_URLinjected by managed DB link
PORTbind address, injected
WEB_CONCURRENCYPuma workers
HANAMI_WEB_SESSIONS_SECRETcookie signing key

Health checks and observability

Add a simple action that returns 200 so the platform can probe readiness. Hanami actions are explicit classes:

# app/actions/health/show.rb
module MyApp
  module Actions
    module Health
      class Show < MyApp::Action
        def handle(_request, response)
          response.status = 200
          response.body = 'ok'
        end
      end
    end
  end
end

Route it at /health in config/routes.rb.

Deploying

Commit the Dockerfile, push, connect the repo, and link your PostgreSQL instance so DATABASE_URL is injected. Add the migration step, set your session secret, and deploy. Build and app logs stream live, so a failed hanami assets compile (often a missing Node dependency) is visible immediately.

git push origin main

Why Hanami is worth the slightly heavier setup

Hanami's slice architecture keeps large apps maintainable, and ROM's separation of persistence from domain logic ages well. The deployment is marginally more involved than Rails only because you precompile esbuild assets and run ROM migrations — both one-time setup costs.

Conclusion

Hanami 2 deploys cleanly once you account for esbuild asset compilation at build time, ROM migrations as a release step, a stable session secret, and the injected PORT. The framework's explicitness pays off in production maintainability.

Spin up a Hanami app plus a managed PostgreSQL database on PandaStack's free tier — connect your repo at [dashboard.pandastack.io](https://dashboard.pandastack.io) and the database wires itself in via DATABASE_URL.

References

  • [Hanami Guides](https://guides.hanamirb.org/)
  • [Hanami: Database (hanami-db / ROM)](https://guides.hanamirb.org/v2.2/database/overview/)
  • [Hanami: Assets](https://guides.hanamirb.org/v2.2/assets/overview/)
  • [ROM (Ruby Object Mapper)](https://rom-rb.org/)
  • [Puma Configuration](https://github.com/puma/puma#configuration)

Ready to deploy?

Start free on PandaStack.

Start free on PandaStack

More in Tutorial

Browse all Tutorial articles →

See also