8.3 KiB
Production Deployment
This guide covers installing nuntius on a Linux server with Caddy, systemd, and rsync already in place — the same setup used by petrbalvin.org. The same rsync -avz workflow that ships the static website works for nuntius with no changes.
Architecture in Production
graph LR
Browser[Visitor's browser]:::accent6
Internet((Internet)):::accent2
Caddy[Caddy :443]:::accent0
Nuntius[nuntius :8080]:::accent1
Systemd[systemd unit]:::accent7
SMTP[(SMTP provider)]:::accent2
Disk[(/var/lib/nuntius/data/*.jsonl)]:::accent2
Browser --> Internet
Internet --> Caddy
Caddy -->|reverse_proxy /api/nuntius/| Nuntius
Systemd -.->|manages| Nuntius
Nuntius -->|SMTP PLAIN| SMTP
Nuntius -->|append JSONL| Disk
nuntius binds [::]:8080 by default — the dual-stack IPv6 wildcard that accepts both IPv4 and IPv6 connections on Linux and FreeBSD. Caddy should be configured to proxy to [::1]:8080 or 127.0.0.1:8080 for loopback-only access.
1. Build and Upload (Local Machine)
cd nuntius
just build
# → ./bin/nuntius
rsync -avz \
bin/nuntius \
.env.example \
user@your-server:/tmp/nuntius/
install.pylives in the repository root. The systemd unit is included inline in Option A below (you do not need to rsync it from the repo)..env.exampleis a template for the secrets file. Adjust thersyncsource list to match what you keep checked in.
2. Install on the Server (SSH Session)
Option A — Manual (the README quick start)
ssh user@your-server
# Create the nuntius system user (one time)
sudo useradd --system --shell /usr/sbin/nologin --home-dir /var/lib/nuntius nuntius
sudo mkdir -p /var/lib/nuntius
sudo chown nuntius:nuntius /var/lib/nuntius
# Install the binary
sudo install -m 0755 /tmp/nuntius/nuntius /usr/local/bin/nuntius
# First run auto-creates /etc/nuntius/config.toml with the 3-form template
sudo /usr/local/bin/nuntius &
sleep 2
sudo kill %1
# Install the systemd service (created inline from this guide)
sudo tee /etc/systemd/system/nuntius.service > /dev/null <<'EOF'
[Unit]
Description=nuntius contact form backend
After=network.target
[Service]
Type=simple
User=nuntius
Group=nuntius
WorkingDirectory=/var/lib/nuntius
EnvironmentFile=/etc/nuntius/.env
ExecStart=/usr/local/bin/nuntius
Restart=on-failure
RestartSec=5
[Install]
WantedBy=multi-user.target
EOF
sudo systemctl daemon-reload
sudo systemctl enable --now nuntius
# Set up secrets
sudo cp /tmp/nuntius/.env.example /etc/nuntius/.env
sudo chmod 600 /etc/nuntius/.env
sudo chown root:nuntius /etc/nuntius/.env
sudo $EDITOR /etc/nuntius/.env # set NUNTIUS_SMTP_PASSWORD
# Edit the auto-generated config to your real SMTP + allowed origins
sudo $EDITOR /etc/nuntius/config.toml
# Restart and watch logs
sudo systemctl restart nuntius
sudo journalctl -u nuntius -f
The service file expects the binary at /usr/local/bin/nuntius, the env file at /etc/nuntius/.env, and the data dir at /var/lib/nuntius (the config's data_dir: "./data" resolves to /var/lib/nuntius/data because the unit sets WorkingDirectory=/var/lib/nuntius). If you'd rather use /opt/nuntius/... or any other layout, edit the systemd unit block above before installing.
Option B — install.py (idempotent)
Before running the script, copy the systemd unit from the block in Option A above into /tmp/nuntius/nuntius.service (the script reads it from the current working directory):
ssh user@your-server
cd /tmp/nuntius
# Paste the [Unit]…[Install] block from Option A into this file:
sudo $EDITOR /tmp/nuntius/nuntius.service
sudo python3 install.py
The script is idempotent: re-running on an already-installed host is safe. It:
- Creates the
nuntiussystem user if missing. - Creates
/var/lib/nuntiuswith the right ownership. - Installs the binary to
/usr/local/bin/nuntius. - Installs the systemd unit to
/etc/systemd/system/nuntius.service. - Runs nuntius briefly to auto-generate
/etc/nuntius/config.toml. - Copies
.env.exampleto/etc/nuntius/.env(mode 0600) if missing. - Reloads systemd and
enables the service — but does not start it, so you can edit/etc/nuntius/.envand/etc/nuntius/config.tomlfirst.
The script ends with the exact next steps you need to run by hand.
3. Caddy 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:
sudo caddy validate
sudo systemctl reload caddy
Caddy forwards X-Forwarded-For automatically — so the real client IP is always passed to nuntius without any extra configuration.
4. Wire It Up to Your Web
If you proxy /api/nuntius/* through Caddy (recommended, step 3 above), VITE_NUNTIUS_URL can stay empty and the form will post to the same origin as the page. Otherwise, set it to the bare upstream URL:
# .env (in the web project, only needed if NOT proxying through Caddy)
VITE_NUNTIUS_URL=https://your-server:8080
For a feedback or newsletter endpoint on the same site, just call the matching path:
await fetch(`${import.meta.env.VITE_NUNTIUS_URL || ''}/api/nuntius/feedback`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ name, email, message }),
});
await fetch(`${import.meta.env.VITE_NUNTIUS_URL || ''}/api/nuntius/newsletter`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ email }),
});
CORS is enforced per form by the allowed_origins list in config.toml. Add every frontend origin that should be able to post to each form.
5. Updating Later
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'
For schema changes, also ship the new config.toml and a migration note in your changelog.
6. Updating the Config
The service does not auto-reload config.toml. After editing it, restart:
sudo $EDITOR /etc/nuntius/config.toml
sudo systemctl restart nuntius
sudo journalctl -u nuntius -f
The process exits 1 on a broken config (unknown field, unknown form type, duplicate path, missing SMTP host, …) and systemd will not keep it up. Fix the reported line and retry.
Files This Guide Creates on the Server
| File | Owner | Mode | Purpose |
|---|---|---|---|
/usr/local/bin/nuntius |
root |
0755 |
The static binary |
/var/lib/nuntius/ |
nuntius |
0750 |
Working directory + data dir |
/var/lib/nuntius/data/ |
nuntius |
0750 |
Newsletter JSONL logs (created at runtime) |
/etc/nuntius/config.toml |
root |
0644 |
nuntius config (auto-generated, then edited) |
/etc/nuntius/.env |
root:nuntius |
0600 |
Secrets (SMTP password) |
/etc/systemd/system/nuntius.service |
root |
0644 |
systemd unit |
/var/lib/nuntius/data/newsletter-*.jsonl is created on the first newsletter form submission, by the nuntius user.
Verifying the Deploy
After systemctl restart nuntius, check:
# Service is up
systemctl status nuntius
# Binary version
/usr/local/bin/nuntius --version
# Listener is bound
ss -tlnp | grep 8080
# Health endpoint responds
curl -s http://127.0.0.1:8080/health | jq .
# Recent log lines
journalctl -u nuntius -n 50 --no-pager
# Caddy proxy works
curl -i https://your-domain.example/api/nuntius/health
# End-to-end POST through Caddy
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"}'
If the end-to-end POST returns {"ok": true} and journalctl -u nuntius -f shows a request line with status=200, the deploy is live.
Backing Up
/etc/nuntius/config.tomland/etc/nuntius/.env— back up alongside your other config files./var/lib/nuntius/data/*.jsonl— back up the JSONL log with your usual file backup job. It is append-only and triviallytar'able.
Uninstalling
sudo systemctl disable --now nuntius
sudo rm /etc/systemd/system/nuntius.service /usr/local/bin/nuntius
sudo rm -rf /etc/nuntius /var/lib/nuntius
sudo userdel nuntius