182 lines
6.3 KiB
Markdown
182 lines
6.3 KiB
Markdown
# 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.
|