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.