Files
petrbalvin 3a38f00dc0
Test / test (push) Successful in 2m1s
Release / gates (push) Successful in 1m57s
Release / build (amd64, freebsd) (push) Successful in 1m26s
Release / build (amd64, linux) (push) Successful in 1m30s
Release / build (arm64, freebsd) (push) Successful in 1m28s
Release / build (arm64, linux) (push) Successful in 1m49s
Release / build (loong64, linux) (push) Successful in 1m30s
Release / build (riscv64, linux) (push) Successful in 1m29s
Release / release (push) Successful in 41s
feat: contact form backend for linux and freebsd servers
Assisted-by: GLM 5.3 Flash
2026-09-29 00:32:56 +02:00

6.3 KiB

Deployment

How nuntius runs in production: a single static binary behind Caddy, under systemd, with its state in one data directory.

Topology

flowchart LR
    Browser[Visitor's browser] --> Caddy[Caddy :443]
    Caddy -->|reverse_proxy /api/nuntius/*| Nuntius[nuntius :8080]
    Systemd[Systemd unit] -.->|manages| Nuntius
    Nuntius -->|SMTP PLAIN| SMTP[(SMTP provider)]
    Nuntius -->|append JSONL| Disk[(/var/lib/nuntius/data)]

nuntius binds [::]:8080 by default: the dual-stack wildcard that accepts both IPv4 and IPv6 connections. The host part is the server.bind key in config.toml; set it to 127.0.0.1 for a loopback-only listener, which is the recommended shape behind a proxy.

Reverse proxy

Add this directive to your Caddyfile (most likely /etc/caddy/Caddyfile):

handle_path /api/nuntius/* {
    reverse_proxy 127.0.0.1:8080
}

Then validate and reload:

sudo caddy validate
sudo systemctl reload caddy

Caddy overwrites X-Forwarded-For for untrusted clients, which is what the server.trust_proxy_headers opt-in expects. Frontends on the same site post to the same origin and need no extra URL; the per-form allowed_origins list carries every origin that may post.

Requirements

  • A Linux or FreeBSD host with systemd and a C compiler-free runtime: the binary is static, nothing else ships with it.
  • The nuntius system user and group, the data directory /var/lib/nuntius, the configuration at /etc/nuntius/config.toml, and the environment file /etc/nuntius/.env (all created by the installer or the manual steps below).
  • Port 8080 locally, 443 publicly through Caddy.
  • An SMTP account (any standards-compliant provider, such as Proton Mail or Thundermail).
File Owner Mode Purpose
/usr/local/bin/nuntius root 0755 The static binary
/var/lib/nuntius/ nuntius 0750 Working directory and data root
/var/lib/nuntius/data/ nuntius 0750 JSONL logs and the rate-limit snapshot (created at runtime)
/etc/nuntius/config.toml root 0644 The configuration (auto-generated, then edited)
/etc/nuntius/.env root:nuntius 0600 Secrets, loaded via EnvironmentFile=
/etc/systemd/system/nuntius.service root 0644 The systemd unit

Build

just build

Copy the artefacts to the server:

rsync -avz bin/nuntius nuntius.service .env.example scripts/install.pl user@your-server:/tmp/nuntius/

Run

The installer does the whole sequence and is idempotent; re-running is safe and it never starts the service, so you review the configuration first:

ssh user@your-server
cd /tmp/nuntius
sudo perl install.pl

It creates the nuntius system user, /var/lib/nuntius, installs the binary and the unit, generates the starter configuration, seeds /etc/nuntius/.env from .env.example, and enables the service. Then review and start:

sudo $EDITOR /etc/nuntius/.env          # set NUNTIUS_SMTP_PASSWORD
sudo $EDITOR /etc/nuntius/config.toml   # real SMTP settings and allowed origins
sudo systemctl start nuntius
sudo journalctl -u nuntius -f

Without the installer, the same steps by hand: create the user and directory as above, install -m 0755 the binary, install -m 0644 the unit, start the binary once to generate /etc/nuntius/config.toml, systemctl daemon-reload && systemctl enable --now nuntius.

Service unit

The unit lives at nuntius.service in the repository root and installs to /etc/systemd/system/nuntius.service; the copy in this document would drift, the pointer does not. It runs as the nuntius user with WorkingDirectory=/var/lib/nuntius (so the default data_dir = "./data" resolves to /var/lib/nuntius/data), loads /etc/nuntius/.env, validates the configuration through ExecStartPre=/usr/local/bin/nuntius --check-config, and restarts on failure. The service is enabled, not started, after installation.

Production configuration

The keys that differ from the defaults in production: allowed_origins carries the real frontend origins, the [forms.smtp] block carries the real host and credentials, server.metrics_token guards the metrics endpoint when it is exposed, and server.bind stays :: or moves to a loopback address behind the proxy. Secrets live in /etc/nuntius/.env and reach the configuration through ${VAR} references; no secret value belongs in config.toml or in this repository.

Upgrade

cd nuntius
just build
rsync -avz bin/nuntius user@your-server:/tmp/nuntius/
ssh user@your-server 'sudo install -m 0755 /tmp/nuntius/nuntius /usr/local/bin/nuntius && sudo systemctl restart nuntius'

The service does not reload config.toml; after editing it, restart. A broken configuration exits 1 and systemd does not keep it up, so fix the reported key and start again. just gates before the new binary ships.

Rollback

Reinstall the previous release's binary from the releases page and restart; the configuration, the JSONL logs and the rate-limit snapshot carry over untouched. Rehearsed exactly this far, and no further: there is no automated rollback path. The reverse of the whole installation is systemctl disable --now nuntius, removing the unit, the binary, /etc/nuntius and /var/lib/nuntius, and deleting the user.

Monitoring

A healthy instance answers the health endpoint and writes one JSON request line per submission:

systemctl status nuntius
/usr/local/bin/nuntius --version
curl -s http://127.0.0.1:8080/health
journalctl -u nuntius -n 50 --no-pager
curl -i https://your-domain.example/api/nuntius/health

An end-to-end check through the proxy:

curl -X POST https://your-domain.example/api/nuntius/contact \
  -H "Content-Type: application/json" \
  -H "Origin: https://your-domain.example" \
  -d '{"name":"Test","email":"test@example.com","message":"hello there, this is a test message"}'

{"ok": true} plus a request line with status=200 in the journal means the deploy is live. Subscriber counts come from the log:

wc -l /var/lib/nuntius/data/newsletter-newsletter.jsonl
jq -r .email /var/lib/nuntius/data/newsletter-newsletter.jsonl | sort -u

Back up /etc/nuntius/config.toml, /etc/nuntius/.env and /var/lib/nuntius/data/*.jsonl with the usual jobs; the log is append-only and trivially archivable.