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.