For those who are adventurous enough to explore the non-http corners of the Internet, the Gemini protocol is a delightful experience to use. It has been around a number of years, making the biggest bang around the time when discontent with the web’s general demise started to reach current heights (so maybe around 2022).
My “capsule”, Vigilia, is self-hosted, and has been since its inception. It used to run on a disused Macbook Pro running Fedora Server, under our TV at home, but since then I have become much more confident in using OpenBSD. It used to run on a little Python CGI script I wrote, which also started to feel too bloated and complex, with too many bells and whistles that I frankly had no need for. It was time to make a change, so I replaced the old Macbook with a Raspberry Pi, and Fedora with OpenBSD, and then took my time to figure out a new “status quo”.
0. Philosophy
I wished to create a more Unix-minded stack. The more I have been using OpenBSD and Unix systems the more I have been sold on the “everything is a file” philosophy, as well as opting to use internal tools as much as possible rather than reinvent the wheel on my own. That is to say, I’d much rather work with simple scripts and shell commands than write complicated and buggy code.
So with that in mind, here’s the stack that I settled on after a some trial and error:
1. Hardware
I have absolutely no intention to expose our home IP address via DynDNS or similar. However, I like to be in control of my data as much as possible: ideally as little of my data should be hosted on “someone else’s computer”. If I can’t unplug the hard disk and put it in a drawer, I can’t guarantee it’s security from a hack.
So Vigilia is actually two servers. The server with the actual data is at home, in running on a Raspberry Pi 4B. But as a “public front” vigilia runs a reverse-proxying gemini server on a standard VPS over at OpenBSD.amsterdam.
2. Network setup
I will not go into the intricacies of the dual-wan setup in this post I have at home; but to keep things connected to each other I am using Tailscale to tie the servers together in a Virtual LAN. This is incredibly handy because they get to have easy to remember static IP addresses, all over an encrypted channel.
So here’s the rough idea:
- Vigilia.cc’s DNS records resolve to the OpenBSD.Amsterdam VPS running
gmid
- VPS and home server both run
tailscale
- VPS reverse-proxies incoming gemini connections to home server
3. Gemini server config
Both the VPS and the local server run gmid
. It’s a fast and simple gemini server that mirrors OpenBSD’s httpd
; which means it is very easy to configure, it is stable and secure. It can run in chroot
ed environments, and as its own user, so it’s just a Good Thing all over. Most importantly, it can relay and reverse-proxy TCP connections with sni
fields intact, which is something for example OpenBSD’s relayd
, built primarily for HTTP, does not do.
My gmid
config files look something like this:
#
## REMOTE_SERVER:/etc/gmid.conf
#
user "_gmid" # running it as its own user to achieve privilege separation
chroot "/var/gemini" # and in a chroot so it can't just access random bits of the file system
log {
syslog # log to /var/log/messages
}
vigilia_pem = "/etc/ssl/PUBLICKEY.pem"
vigilia_key = "/etc/ssl/private/PRIVATEKEY.key"
public_ip = "46.23.93.41" # OpenBSD Amsterdam VPS' public address
homeserver = "100.REDACTED.REDACTED.101" # TailScale IP of the home machine
public_port = "1965"
homeserver_port = "2965"
server "vigilia.cc" {
listen on $public_ip port $public_port
cert $vigilia_pem
key $vigilia_key
proxy {
proxy-v1 # this directive enables some advanced features like forwarding IP Addresses of visitors
verifyname off # I found I need to specify this somehow, maybe because of self-signed certs
sni "vigilia.cc"
relay-to $homeserver $homeserver_port
}
}
This above allows to listen for connections to vigilia.cc:1965
and forward them to HOME_SERVER:2965
. So thus the homeserver has the following configuration:
#
## HOME_SERVER:/etc/gmid.conf
#
user "_gmid"
chroot "/var/gemini"
log {
syslog
}
internal_address = "100.REDACTED.REDACTED.101" # TailScale IP of the home machine
internal_port = "2965"
# The below are the same certificates that are in use on the VPS
vigilia_pem = "/etc/ssl/PUBLICKEY.pem"
vigilia_key = "/etc/ssl/private/PRIVATEKEY.key"
server "vigilia.cc" {
listen on $internal_address port $internal_port proxy-v1 # add proxy-v1 support for relayed connections
cert $vigilia_pem
key $vigilia_key
log on
location "*" {
auto index on # enables directory listing
}
}
4. Getting the files to the Server
Because I am lazy I want to edit files locally and I want them to magically appear on my capsule. So I am using syncthing
to copy things over automagically from DESKTOP:~/public_gemini
to HOME_SERVER:/var/gemini
.
Syncthing runs most reliably as my own user, I found. To do this it is best to follow the documentation for the Syncthing OpenBSD package — but basically it involves starting it via the user’s crontab
with the “@reboot
” directive. But as it runs as my own user, I need to set the permissions properly. HOME_SERVER:/var/gemini
is owned by the _gmid
user in the _gmid
group so I also added MYUSER
on both machines to the same _gmid
group, and made sure MYUSER
has write access:
#!/bin/sh
# HOME_SERVER
usermod -G _gmid MUYSER
chown -r _gmid /var/gemini
chmod -r ug=rwx,o=r /var/gemini
Then I set up syncthing on HOME_SERVER
. As it is running headless, I needed to access the web interface, which I achieved via SSH tunneling:
$ ssh -L 9999:localhost:8384 HOME_SERVER
This way I could open a browser on DESKTOP
and access the server’s Syncthing settings.
So here are the settings:
On the DESKTOP:
- Syncthing web interface -> Add folder
- Folder path:
~/public_gemini
- Folder label: Gemini files (or something)
- Ignore patterns: “
*.sock
” (Unix sockets might confuse the poor thing) - Sharing: HOME_SERVER
- Pause syncing for now
On HOME_SERVER:
- Establish ssh tunnel to HOME_SERVER as described above
- Open remote Syncthing webinterface on DESKTOP: https://localhost:9999
- Accept the incoming share request for “Gemini files” from DESKTOP; but point it to /var/gemini
- Folder path:
/var/gemini
- Folder label Gemini files
- Advanced: UNTICK “Wach for changes” because OpenBSD doesn’t seem to allow Syncthing to poke around in
/var
with those various Go modules and you’d just get errors, like I did - Check the Ignore patterns — if it didn’t synchronise “
*.sock
” then specify it manually
On DESKTOP:
- Unpause syncing
Now any file you write into DESKTOP:~/public_gemini
will sync across to HOME_SERVER:/var/gemini.
Yay!
6. Setting up automatic static site generation
Now if you are content to maintain your capsule manually, you are done. As I said I am lazy so I want my little “ssg” script, Lumen, to create index pages for each directory for me. Lumen, I promise, will be made available once I tidy it up.
Lumen basically lists all files recursively and generates an index.gmi
for each directory. This means that Lumen has to be re-run each time the folder changes. OpenBSD is acquiring some degree of file watching natively.1 However entr
already exists in ports.
It took a bit of tweaking but basically here’s the command I ended up using, adapted from one of the examples provided in the entr
manpage:
$ while sleep 0.1; do find /var/gemini/vigilia.cc/* | entr -nd python3 /var/gemini/cgi/lumen.py -d /var/gemini/vigilia.cc; done
What it does is, in a loop it recursively lists all files every 0.1 seconds in /var/gemini/vigilia.cc
, and feeds the output to entr
. Then entr
runs with -n
to specify a non-interactive session (in interactive sessions it also responds to e.g. keystrokes and tty changes – so to be safe, I don’t want that); and with -d
to specify it should be looking for changes in the parent folder of any changing files. The looping and the -d
directive were added because sometimes I ran into issues when a file got deleted: entr
just quit because it could not find the removed file in a “stale” file list it was provided on launch. Lumen needs a -d
argument as well to specifiy which directory it needs to work on.
7. System config
Because there are a few other servers like “auld.vigilia.cc” also running on the home machine (the configs for wich aren’t reproduced above for brevity’s sake) and because those rely on a number of CGI scripts I have to start them on launch. I ended up using supervisor
d for these. Supervisor is a cool little daemon for launching things. I could use rc
but supervisord
allows me to specify a few extra bits more easily, like redirecting output to syslog
and other things.
So for HOME_SERVER, here is my supervisord
configuration:
#
### HOME_SERVER:/etc/supervisord.conf
#
# [... snip ...]
[program:gmid]
command=/usr/local/bin/gmid -f ; the program (relative uses PATH, can take args)
process_name=%(program_name)s ; process_name expr (default %(program_name)s)
directory=/var/gemini/ ; directory to cwd to before exec (def no cwd)
priority=100 ; the relative start priority (default 999)
autostart=true ; start at supervisord start (default: true)
startretries=3 ; max # of serial start failures when starting (default 3)
autorestart=true ; when to restart if exited after running (def: unexpected)
killasgroup=true ; SIGKILL the UNIX process group (def false)
stdout_syslog=true ; send stdout to syslog with process name (default false)
stderr_syslog=true ; send stderr to syslog with process name (default false)
[program:lumen-vigilia_cc]
command=/bin/ksh -c 'while sleep 0.1; do find /var/gemini/vigilia.cc/* | entr -nd python3 /var/gemini/cgi/lumen.py -d /var/gemini/vigilia.cc; done'
process_name=%(program_name)s
directory=/var/gemini/
priority=102
autostart=true
startretries=3
autorestart=true
user=MYUSERNAME
stderr_syslog=true
stdout_syslog=true
There are other directives that start the CGI scripts for “auld.vigilia.cc” in the config, omitted here.
Note that you can specify “priority” to control in what order you want the scripts to run. I first want the gemini server to run (100); then I want it to run the CGI scripts (101 — left out of the above example); then I want to run the static site generator’s watcher (102). Notice I am telling explicitly it to run /bin/ksh
with a command specified in -c
; this is because simply feeding it a complex command confuses supervisor
d, as I discovered.
One nice feature of supervisord
is that it can redirect both stderr
and stdout
to syslog, so any commands and processes supervisord
runs will have their output sent to /var/log/messages
, neatly tagged and organised.
Conclusion
So there you have it — my Gemini stack from start to finish. It was a really fun experiment to start to use OpenBSD, instead of reinventing the wheel, or relying on some monolithic CGI scripts. You can do quite a lot with just system internals and a few packages.
- The
watch
utility was added to 7.7-current on 2025-05-19; it will make its way into 7.8 hopefully. ↩︎
Adapted from the original article “Vigilia’s New Gemini Stack” published via Gemini at vigilia.cc on 21 July 2025.
Solène :flan_hacker:
@journal you could configure syncthing to only send files from desktop and only receive files on the server, this would prevent changes (which should not happen) on the server to propagate on the desktop
Remote Reply
Original Comment URL
Your Profile