bensinclair.com
Hanami on OpenBSD with httpd and relayd
ben@octane~$ cat hanami-on-openbsd-with-httpd-and-relayd.txt

My next trick for hosting on OpenBSD was getting a simple Hanami app running, ideally using httpd(8) instead of nginx. This was mainly an exercise in keeping the system as minimal and OpenBSD-ey as possible, but it works!

For experimentation and to help make sure this is accurate I have Proxmox running OpenBSD as a virtual machine. I have a backup of a fresh 7.9 install I can spin up at any time and recreate the steps I used on the production machine. Maybe that should be the other way around, but testing on production is more exciting!

Here is the gist of what I did to get things running. I did a lot of tinkering and experimenting, so there might be some steps missing here!

Installing Hanami

First I had to install some OpenBSD packages. I really don't like node, but Hanami wants it.

$ doas pkg_add ruby ruby-shims node git

Right now under OpenBSD 7.9 you'll get ruby 4.0.2 installed. One issue was gem wanted to install gems outside of my home directory, which it couldn't do without root. To get around that I told it where to install gems, and set the gem binary path. You'll want to add this to your shell configuration so it persists.

$ export GEM_HOME=$HOME/.gems
$ export GEM_PATH=$HOME/.gems
$ export PATH=$HOME/.gems/bin:$PATH

Next I installed the hanami gem. Ruby on OpenBSD adds a version suffix to the gem binary names. So with 4.0.2 I ended up with .gems/bin/hanami40 (among others), which breaks things. Maybe there's a better way, but my workaround is just symlinks.

$ ln -s $HOME/.gems/bin/hanami40 $HOME/.gems/bin/hanami
$ ln -s $HOME/.gems/bin/foreman40 $HOME/.gems/bin/foreman

I created a new app, and did a bundle install. I had to mess with the path here too.


$ gem install hanami
$ hanami new hello
$ cd hello
$ bundle config set --local path vendor/bundle
$ bundle install

This machine is only running IPv4, and Puma was trying to bind to the IPv6 address and wouldn't work. I edited config/puma.rb and added a bind to a 127.0.0.1.

config/puma.rb
bind "tcp://127.0.0.1:2300"

Hopefully I didn't forget to mention any steps, but it worked!

$ bundle exec hanami dev
13:29:48 web.1 | started with pid 97525
13:29:48 assets.1 | started with pid 62525
13:29:55 assets.1 | [hello] [watch] build finished, watching for changes...
13:29:57 web.1 | 13:29:57 - INFO - Using Guardfile at /home/ben/src/hello/Guardfile.
13:29:57 web.1 | Please add the following to your Gemfile to avoid polling for changes:
13:29:57 web.1 | gem 'rb-kqueue', '>= 0.2'
13:29:58 web.1 | 13:29:58 - INFO - Puma starting on port 2300 in development environment.
13:29:58 web.1 | 13:29:58 - INFO - Guard is now watching at '/home/ben/src/hello'
13:29:59 web.1 | Puma starting in single mode...
13:29:59 web.1 | * Puma version: 8.0.2 ("Into the Arena")
13:29:59 web.1 | * Ruby version: ruby 4.0.2 (2026-03-17 revision d3da9fec82) +PRISM [x86_64-openbsd]
13:29:59 web.1 | * Min threads: 5
13:29:59 web.1 | * Max threads: 5
13:29:59 web.1 | * Environment: development
13:29:59 web.1 | * PID: 26826
13:30:06 web.1 | * Listening on http://127.0.0.1:2300
13:30:06 web.1 | * Listening on http://[::]:2300
13:30:06 web.1 | * Starting control server on http://[::1]:9293
13:30:06 web.1 | * Starting control server on http://127.0.0.1:9293
13:30:06 web.1 | Use Ctrl-C to stop

Configuring httpd and relayd

Initially I was trying to just use httpd, because why would I need anything else? Setting up static hosting under httpd with SSL was easy, and I thought there would be a way to send requests from httpd to Hanami. httpd by itself can't do that, but relayd(8) can do it!

It turns out relayd is pretty neat. Putting it in front of whatever services you're running gives you flexibility to do load balancing, route requests to static hosting, web applications, and more. I really like the concept of it being the front door to whatever you're hosting. It handles SSL too, and leaves your httpd configuration simple.

Setup plain httpd and SSL first

I think it's best if you start with just httpd and make sure you get things working with SSL. That will show you httpd basics, and get acme-client(1) running.

Your OpenBSD install will have /etc/examples/httpd.conf and /etc/examples/acme-client.conf. They are pretty much ready to go, you just need to change the domain name in each file. acme-client(1) also shows you how to add a crontab entry to keep your certificate renewed. Initially my static setup was pretty much just those example files, and it worked!

relayd!

Now we can setup relayd, which will replace httpd as the public facing service, then forward to httpd and hanami on different ports.

relayd looks for its certificates in a specific place and different filenames compared to the example acme-client.conf. I moved the files and then changed acme-client.conf to generate them in the right spot with the right names:

/etc/acme-client.conf
domain key "/etc/ssl/private/bensinclair.com.key"
domain full chain certificate "/etc/ssl/bensinclair.com.crt"

Now here's my relayd configuration with notes on what everything does:

/etc/acme-client.conf
table <httpd> { 127.0.0.1 } # Defines a set of httpd hosts. It could be multiple or other hosts, but this is all on the same machine.
table <hanami> { 127.0.0.1 } # Another set for hanami

# The following defines a protocol, which is a set of rules and options applied when a request is received.
http protocol "web" {
match request header append "X-Forwarded-For" value "$REMOTE_ADDR" # Without this httpd would think all requests came from localhost
match request header append "X-Forwarded-Proto" value "https" # Lets httpd know this is over https
tls keypair "bensinclair.com" # This is where the SSL cert comes in. It looks for /etc/ssl/bensinclair.com.crt and /etc/ssl/private/bensinclair.com.key
match request path "/hello/*" forward to <hanami> # Sends /hello requests off to hanami
pass # All other requests pass on through
}

# This sets up plain http on port 80, but sends all requests off to httpd which we'll configure to run on 8080 instead.
relay "http" {
listen on egress port 80
forward to <httpd> port 8080
}

# Here is the real https server. It uses the "web" protocol we defined earlier to get the certificates and rules, then sends requests over https which we've moved to 8081 in httpd. Hanami requests get sent to hanami listening on port 2300.
relay "https" {
listen on egress port 443 tls
protocol "web"
forward to <httpd> port 8081
forward to <hanami> port 2300
}

Next I reconfigured httpd to remove the SSL related configuration and switched ports to 8080 and 8081. It keeps the acme-challenge setup so certificates can be renewed. Note the redirect from http to https is still done here.

/etc/httpd.conf
server "bensinclair.com" {
listen on 127.0.0.1 port 8080
root "/htdocs"

log style combined

location "/.well-known/acme-challenge/*" {
root "/acme"
request strip 2
}

location * {
block return 302 "https://$HTTP_HOST$REQUEST_URI"
}
}

server "bensinclair.com" {
listen on 127.0.0.1 port 8081
root "/htdocs"
log style combined
}

It seems to be a nice setup so far!

ben@octane~$