Files

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.py lives 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.example is a template for the secrets file. Adjust the rsync source 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:

  1. Creates the nuntius system user if missing.
  2. Creates /var/lib/nuntius with the right ownership.
  3. Installs the binary to /usr/local/bin/nuntius.
  4. Installs the systemd unit to /etc/systemd/system/nuntius.service.
  5. Runs nuntius briefly to auto-generate /etc/nuntius/config.toml.
  6. Copies .env.example to /etc/nuntius/.env (mode 0600) if missing.
  7. Reloads systemd and enables the service — but does not start it, so you can edit /etc/nuntius/.env and /etc/nuntius/config.toml first.

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.toml and /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 trivially tar'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