My Setup

I've updated my setup, now supporting multiple sites on the same server. You can still see my very simple setup here.

Old "Simple" Setup

File upload for multiple instances

I am still running syncthing to give access to my files on my laptop, phone and anywhere else I want to be. A little bit of work needs to be done to make the files hostable, changing owner and permissions. All this is done by tracking when changes occur.

#!/bin/sh
#
# /usr/local/bin/sync-gemini.sh

if [ $# -ne 1 ]; then
  echo "Supply instance name"
  exit 1
fi

SRC_DIR="/var/syncthing/gemini-$1"
DST_DIR="/usr/local/gemini-$1"
ACTIONS="create,delete,move,modify"

while :
do
        if inotifywait -rq -e ${ACTIONS} "${SRC_DIR}" 2>&1 1>/dev/null; 
        then
                sleep 5 # To allow for other changes
                rsync -avh "${SRC_DIR}/" "${DST_DIR}/" --delete
                chown -R molly:whell "${DST_DIR}"
                chmod -R 777 "${DST_DIR}"
        fi
done

I then created a systemd unit file that supports multiple instances: `gemini-sync@.service`

[Unit]
Description=Gemini Sync
After=network.target

[Service]
Type=simple
Restart=always
User=molly
ExecStart=/usr/local/bin/sync-gemini %i

[Install]
WantedBy=multi-user.target

Multiple Gemini instances

I'm running molly-brown as my gemini server. Configuration is pretty straight forward, just use the default config and fill in your details. But since I'm running multiple instances I create a config file per instance `/etc/molly-<instance>.conf`. Each needs its own port, none of which use 1965. Using nginx we can proxy incoming requests to the correct instance depending on the SSL request.

The molly-brown service unit file: `molly-brown@.service`

[Unit]
Description=Molly Brown gemini server
After=network.target

[Service]
Type=simple
Restart=always
User=molly
ExecStart=/usr/local/bin/molly-brown -c /etc/molly-%i.conf

[Install]
WantedBy=multi-user.target

My `nginx.conf` file.

load_module /usr/lib/nginx/modules/ngx_stream_module.so;

user www-data;
worker_processes 1;
pid /var/run/nginx.pid;

events {
  worker_connections 1024;
}

http {
  server_names_hash_bucket_size 128; # For Matrix and Certbot
  include /etc/nginx/config-enabled/*.conf;
  include /etc/nginx/sites-enabled/*.conf;
}

stream {
    # connection-limiting
    limit_conn_zone               $binary_remote_addr zone=addr:10m;
    limit_conn_log_level          warn;
    limit_conn                    addr 1;

    # logging
    log_format                    basic '$remote_addr $upstream_addr [$time_local] '
                                  '$protocol $status $bytes_sent $bytes_received '
                                  '$session_time';
    access_log                    /var/log/nginx/gemini.access.log basic;
    error_log                     /var/log/nginx/error.log warn;

    # map SNI -> backend service
    map $ssl_preread_server_name  $name {
        gemini.sh0.xyz sh0;
        ad0qm.com ad0qm;
    }

    # Gemini
    server {
        listen                    1965;
        ssl_preread               on;
        proxy_buffer_size         16k;

        # pass requests directly to the corresponding Gemini server
        proxy_pass                $name;
    }

    upstream  ad0qm {
        server                    127.0.0.1:1966;
    }
    upstream  sh0 {
        server                    127.0.0.1:1967;
    }
}

Add entries to the map and an upstream for each server you want to connect. Once the multiple instances are running and nginx starts up you can hitt the different capsules based on their domain name.

Multiple Gemini to HTTP Proxies

I use Kineto for proxying my capsule to HTTP. Again with multiple instances means a service setup that supports multiple instances. `kineto@.service`

[Unit]
Description=Kineto Gemini to HTTP Proxy
After=network.target

[Service]
Type=simple
Restart=always
User=molly
EnvironmentFile=/etc/kineto-%i.conf
ExecStart=/usr/local/bin/kineto-wrapper

[Install]
WantedBy=multi-user.target

The wrapper script handles the optional arguments supplied in the config file.

#!/bin/sh
#
# /usr/local/bin/kineto-wrapper

if [ -z "${GEMINI_URL}" ]; then
  printf "Missing config GEMINI_URL\n" 1>&2
  exit 1
fi

ARGS=""

if [ ! -z "${BIND}" ]; then
  ARGS="${ARGS} -b ${BIND}"
fi

if [ ! -z "${CSS_FILE}" ]; then
  ARGS="${ARGS} -s ${CSS_FILE}"
fi

if [ ! -z "${CSS_URL}" ]; then
  ARGS="${ARGS} -e ${CSS_URL}"
fi

eval "/usr/local/bin/kineto ${ARGS} ${GEMINI_URL}"

The last step is setting up the per site configuration in nginx to proxy all https reqeusts to kineto. Make sure the proxy_pass path points to the right kineto instance.

server {

  server_name gemini.sh0.xyz;

  location / {
    proxy_pass http://localhost:8081/;
    proxy_buffering off;
    proxy_set_header X-Real-IP $remote_addr;
  }

  access_log /var/log/nginx/gemini.sh0.xyz.access.log;
  error_log /var/log/nginx/gemini.sh0.xyz.error.log error;

  listen [::]:443 ssl ipv6only=on; # managed by Certbot
  listen 443 ssl; # managed by Certbot
  ssl_certificate /usr/local/etc/letsencrypt/live/sh0.xyz/fullchain.pem; # managed by Certbot
  ssl_certificate_key /usr/local/etc/letsencrypt/live/sh0.xyz/privkey.pem; # managed by Certbot
  include /usr/local/etc/letsencrypt/options-ssl-nginx.conf; # managed by Certbot
  ssl_dhparam /usr/local/etc/letsencrypt/ssl-dhparams.pem; # managed by Certbot
}

server {
  if ($host = gemini.sh0.xyz) {
      return 301 https://$host$request_uri;
  } # managed by Certbot

  listen 80;
  listen [::]:80;

  server_name gemini.sh0.xyz;
    return 404; # managed by Certbot
}

I wrote a simple atom.xml cgi script that just expects the first header to be the title and a `Published:` and `Updated:` line towards the top of the file to define its date and time of publishing.

# Title of my post

Published: 2022-10-01 08:42
Updated: 2022-10-01 11:30

Lorem ipsem...

Molly Brown (Project Site)

Kineto (Project Site)

My atom.xml script (Python3)

Atom Feed

I've written a cgi script but I'm a little worried about how much it depends on the server side to be working correctly. Change time stamps on files and magically they get republished. Will be looking into being able to store off the current file and checking if anything is new, maybe then moving to a dynamic method. But for the time being I just maintain an atom.xml file by hand.

back