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/relayd.conf
table <httpd> { 127.0.0.1 } 
table <hanami> { 127.0.0.1 } 


http protocol "web" {
		match request header append "X-Forwarded-For" value "$REMOTE_ADDR"
		
		match request header append "X-Forwarded-Proto" value "https"
		
		tls keypair "bensinclair.com"
		
		match request path "/hello/*" forward to <hanami>
		
		pass
		
}


relay "http" {
		listen on egress port 80
		forward to <httpd> port 8080
}


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~$