feat: contact form backend for linux and freebsd servers
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

Assisted-by: GLM 5.3 Flash
This commit is contained in:
2026-09-29 00:32:56 +02:00
commit 3a38f00dc0
49 changed files with 10769 additions and 0 deletions
+181
View File
@@ -0,0 +1,181 @@
# Deployment
How nuntius runs in production: a single static binary behind Caddy, under
systemd, with its state in one data directory.
## Topology
```mermaid
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`):
```caddyfile
handle_path /api/nuntius/* {
reverse_proxy 127.0.0.1:8080
}
```
Then validate and reload:
```sh
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
```sh
just build
```
Copy the artefacts to the server:
```sh
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:
```sh
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:
```sh
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
```sh
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:
```sh
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:
```sh
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:
```sh
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.