Getting Started with Hanami and Docker Compose


Introduction

Hanami 2.0 is the perfect Ruby framework for building robust and fast API applications. The 2.0 version comes without a persistency layer (that will be a 2.2 feature).

Today we’ll learn how to set up a Hanami app with a secure Redis instance using Docker Compose in a few steps.

As a prerequisite, you’ll need Docker, cURL, Ruby 3.2+, and Hanami 2.0+.

Steps

1. Generate the app

Generate a new Hanami 2.0 app.

⚡ hanami new bookshelf

2. Configure Docker

Configure a Docker image for the app. We’ll use Alpine Linux and multi-stage builds to reduce the size of the Docker image.

Here’s how our Dockerfile will look like:

FROM ruby:3.0.1-alpine as builder
RUN apk add build-base
COPY Gemfile* ./
RUN bundle install
FROM ruby:3.0.1-alpine as runner
WORKDIR /app
COPY --from=builder /usr/local/bundle/ /usr/local/bundle/
COPY . .
EXPOSE 2300
CMD ["bundle", "exec", "hanami", "server", "--host", "0.0.0.0"]

3. Set up Docker Compose

As a first thing edit .env like the following:

REDIS_PASSWORD=secret
REDIS_HOST=redis
REDIS_PORT=6379
REDIS_DB=0
REDIS_URL="redis://:${REDIS_PASSWORD}@${REDIS_HOST}:${REDIS_PORT}/${REDIS_DB}"

Then create a docker-compose.yml file at the root of the app:

version: "3.9"
services:
  app:
    image: bookshelf
    env_file:
      - .env
    ports:
      - 2300:2300
    depends_on:
      - redis
  redis:
    image: redis:7.0-alpine
    restart: unless-stopped
    volumes:
      - ./storage/redis/data:/data
    command: redis-server --protected-mode yes --requirepass $REDIS_PASSWORD
    env_file:
      - .env

The Docker image bookshelf we declared in the previous step will be used for the primary service app. This service depends on the redis service: Docker Compose will boot redis first, then app.

The Redis installation uses protected-mode and a password to enforce the Redis Security model.

4. Add a Hanami Provider for Redis

Add the redis gem:

⚡ bundle add redis

Reference the Redis URL from .env in application settings (config/settings.rb):

# frozen_string_literal: true

module Bookshelf
  class Settings < Hanami::Settings
    setting :redis_url, constructor: Types::Params::String
  end
end

Hanami maps environment variables into settings entries. For instance, the env var REDIS_URL is automatically detected, read, and assigned to redis_url setting.

Create the Hanami Provider for Redis (config/providers/redis.rb):

# frozen_string_literal: true

Hanami.app.register_provider(:redis) do
  prepare do
    require "redis"
  end

  start do
    client = Redis.new(url: target["settings"].redis_url)

    register "redis", client
  end
end

The target["settings"].redis_url is referencing the redis_url setting that we set up earlier.

5. Generate Actions

Generate actions to interact with the app.

We want to the actions accept and produce JSON format. Let’s configure in config/app.rb

# frozen_string_literal: true

require "hanami"

module Bookshelf
  class App < Hanami::App
    config.actions.format :json
  end
end

Generate an action to create books:

⚡ bundle exec hanami generate action books.create

Edit it (app/actions/books/create.rb):

# frozen_string_literal: true

module Bookshelf
  module Actions
    module Books
      class Create < Bookshelf::Action
        include Deps["redis"]

        def handle(req, res)
          req.params[:book] => {id:, **data}
          redis.hset("books:#{id}", data)

          halt 201
        end
      end
    end
  end
end

Generate another action to show a book:

⚡ bundle exec hanami generate action books.show

Edit it (app/actions/books/show.rb):

# frozen_string_literal: true

module Bookshelf
  module Actions
    module Books
      class Show < Bookshelf::Action
        include Deps["redis"]

        def handle(req, res)
          books = redis.hgetall("books:#{req.params[:id]}")
          res.body = books.to_json
        end
      end
    end
  end
end

6. Try it

Build the Docker image:

⚡ docker build -t bookshelf .

Start the app via Docker Compose:

⚡ docker compose up

Create a book:

⚡ curl \
  -X POST \
  -H "Content-Type: application/json" \
  -d '{"book": {"id": "1", "title": "Hanami book"}}' \
  http://localhost:2300/books

Created

Fetch it:

⚡ curl \
  -H "Accept: application/json" \
  http://localhost:2300/books/1

{"title":"Hanami book"}

Conclusion

We could set up a full working Hanami 2.0 API in a few steps, using Redis as a persistency layer and Docker Compose to run it.

Luca Guidi

Family man, software architect, Open Source indie developer, speaker.

Rome, Italy https://lucaguidi.com