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.rbbind "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 9752513:29:48 assets.1 | started with pid 6252513: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: 513:29:59 web.1 | * Max threads: 513:29:59 web.1 | * Environment: development13:29:59 web.1 | * PID: 2682613:30:06 web.1 | * Listening on http://127.0.0.1:230013:30:06 web.1 | * Listening on http://[::]:230013:30:06 web.1 | * Starting control server on http://[::1]:929313:30:06 web.1 | * Starting control server on http://127.0.0.1:929313: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.confdomain 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.conftable <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.confserver "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!