OpenBSD migration
Now that the announced downtime (which lasted this entire afternoon) is successfully behind, I can blog about the reason: I switched my OVH VPS from AlmaLinux + Caddy to base OpenBSD.
Why §
I wanted something much simpler than RedHat/Debian (Alpine was considered), can rely on my trusty UNIX fu skills and Caddy was starting to feel too bloated for my simple use case (and like most Go stuff, too accepting of LLM "coding" for my taste).
So when news of OpenBSD's 7.9 release greeted my feed, I remembered that it got a builtin HTTP server quite some time ago. A little perusing of the documentation (and this cool blog) convinced me it had everything I could ever want (including precompressed gzip serving and (Fast)CGI if I ever want some dynamism).
Supplementary cool fact to consider: everything else I need (openrsync, acme-client, mg, even tmux) is in base. My choice was made.
Installation §
Sadly, OVH really makes it annoying to run something different than the images they propose. So buckle up and open your can of elbow grease!
First, go to the VPS admin panel and find the (slightly hidden) menu to "Reboot in rescue mode". That'll take a few minutes then you'll get a temporary password to ssh to your rescue session.

What do you do then? First, I tried to use qemu -nographic to boot the netinst
disk image (miniroot.img or ISO (cd.iso) only for the boot process to
get stuck at "entry point at 0x…". Too old qemu or PEBKAC? Don't know and didn't get much help on
IRC, but after an hour, I tried simply writing the miniroot to the disk and it Just Werked™. So:
$ ssh root@world-playground-deceit.net
$ curl -L https://cdn.openbsd.org/pub/OpenBSD/7.9/amd64/miniroot79.img |
dd of=/dev/sdb bs=1MThen reboot the VPS (in normal mode), open the web KVM and proceed normally with the OpenBSD installer but do use MBR instead of GPT
if you want a bootable result and don't forget to keep sshd enabled!
Only a few steps left to get a working server:
1. (Optional but useful) Enable persistent shell history, really saves some time.
$ echo 'export ENV=~/.kshrc' >>.profile $ cat <<EOF >>.kshrc export HISTFILE=~/.ksh_history export HISTSIZE=100000 export HISTCONTROL=ignoredups:ignorespace EOF
2. Configure httpd and acme-client to get a basic TLS website
setup up and running:
$ sed 's/example\.com/world-playground-deceit.net/g' \ /etc/examples/httpd.conf >/etc/httpd.conf $ httpd -n # Check config $ sed '/alternative names/d; s/example\.com/world-playground-deceit.net/g' \ /etc/examples/acme-client.conf >/etc/acme-client.conf $ acme-client -n # Check config $ rcctl enable httpd $ rcctl start httpd $ acme-client -v world-playground-deceit.net ... $ EDITOR=mg crontab -e # Add a daily acme-client call like in the manpage
3. ???
4. Profit! Only a few openrsync compatibility tweaks needed for the rsync command used to push website updates:
add a --rsync-path=openrsync option and remove --exclude '.*' (implies
"fixing" my generator to avoid littering the output directory) which apparently doesn't play well with --delete (gave me some protocol errors).
The last mile §
Well, my httpd.conf still needs some massaging to match (or improve upon) the
caddy setup I had: more mimetypes (mainly for video files), no dir listing to avoid being DOSed by
scrapers, (precompressed) gzip serving and two rewrite rules. The final config remains deceptively
simple, if you ignore the workaround that won't be needed for long:
prefork 2 types { include "/usr/share/misc/mime.types" application/atom+xml atom xml } server "world-playground-deceit.net" { listen on * port 80 location "/.well-known/acme-challenge/*" { root "/acme" request strip 2 } location * { block return 302 "https://$HTTP_HOST$REQUEST_URI" } } server "world-playground-deceit.net" { listen on * tls port 443 tls { certificate "/etc/ssl/world-playground-deceit.net.fullchain.pem" key "/etc/ssl/private/world-playground-deceit.net.key" } tcp nodelay directory no index gzip-static location "/" { request rewrite "/welcome.html" # cf https://blog.nutts.org/2026/02/24/gzip-static-location-rewrites-httpd-openbsd/ # Fix pending: https://marc.info/?l=openbsd-tech&m=177835277715839&w=2 gzip-static } location "/favicon.ico" { request rewrite "/resources/favicon.ico" } }
And finally, to get those static gzip sidecars, a relatively portable sh pipeline:
# NB: POSIX 2024 sez that `[PATH1 -nt PATH2]` succeeds if PATH2 doesn't exist, # but few implementations are compliant (e.g. dash >=0.5.13) find dst/ -type f | grep -E '\.(html|css|svg|xml)$' | tr '\n' '\000' | xargs -n1 -0 -P$(getconf NPROCESSORS_ONLN) sh -c \ 'if ! [ -f "$1".gz ] || [ "$1" -nt "$1".gz ]; then gzip -9fk "$1"; fi' argv0
(Though I recommend libdeflate or something else instead of plain zlib, cf benchmarks)