# Production Deployment This guide covers installing nuntius on a Linux server with `nginx`, `systemd`, and `rsync` already in place — the same setup used by [petrbalvin.org](https://www.petrbalvin.org). The same `rsync -avz` workflow that ships the static website works for nuntius with no changes. ## Architecture in Production ```mermaid graph LR Browser[Visitor's browser]:::accent6 Internet((Internet)):::accent2 Nginx[nginx :443]:::accent0 Nuntius[nuntius :8080]:::accent1 Systemd[systemd unit]:::accent7 SMTP[(SMTP provider)]:::accent2 Disk[(/var/lib/nuntius/data/*.jsonl)]:::accent2 Browser --> Internet Internet --> Nginx Nginx -->|proxy_pass /api/nuntius/| Nuntius Systemd -.->|manages| Nuntius Nuntius -->|SMTP PLAIN| SMTP Nuntius -->|append JSONL| Disk ``` nuntius listens on `127.0.0.1:8080` (default). nginx terminates TLS on `:443` and proxies `/api/nuntius/*` to the upstream. systemd restarts the process on failure. ## 1. Build and Upload (Local Machine) ```bash cd nuntius just build # → ./bin/nuntius rsync -avz \ bin/nuntius \ .env.example \ user@your-server:/tmp/nuntius/ ``` > `install.sh` 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) ```bash 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.sh` (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): ```bash 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 bash install.sh ``` 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 `enable`s 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. nginx Reverse Proxy Add this location block to the same nginx server that already serves your site (most likely `/etc/nginx/sites-available/petrbalvin.org`): ```nginx location /api/nuntius/ { proxy_pass http://127.0.0.1:8080/api/nuntius/; proxy_set_header Host $host; proxy_set_header X-Real-IP $remote_addr; proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; proxy_set_header X-Forwarded-Proto $scheme; } ``` Then: ```bash sudo nginx -t sudo systemctl reload nginx ``` Without `X-Forwarded-For`, the rate limiter will see every request as coming from `127.0.0.1` and effectively become useless once the form is live. The `Host` header also matters for vhost-aware upstreams; the default is fine for a single nuntius instance. ## 4. Wire It Up to Your Web If you proxy `/api/nuntius/*` through nginx (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: ```bash # .env (in the web project, only needed if NOT proxying through nginx) VITE_NUNTIUS_URL=https://your-server:8080 ``` For a feedback or newsletter endpoint on the same site, just call the matching path: ```js 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 ```bash 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: ```bash 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: ```bash # 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 # nginx proxy works curl -i https://your-domain.example/api/nuntius/health # End-to-end POST through nginx 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 ```bash 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 ```