5451a591ad
Validate / validate (push) Successful in 32s
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
256 lines
8.3 KiB
Markdown
256 lines
8.3 KiB
Markdown
# Deployment
|
|
|
|
Self-hosted on a single Linux box (Proxmox LXC / VM / bare metal). Four layers, innermost to outermost:
|
|
|
|
1. **Node app** (`adapter-node`) — listens on `127.0.0.1:3000`
|
|
2. **Caddy** (internal) — listens on LAN IP, Yggdrasil IPv6, NetBird IP, and `127.0.0.1` (for Tor). HTTP only — no TLS.
|
|
3. **Tor / Yggdrasil / NetBird** — overlay transports that connect peers to Caddy
|
|
4. **External reverse proxy** (off-box, e.g. at the network edge) — does TLS termination for the public hostname; forwards to LAN IP:8080
|
|
|
|
```
|
|
┌─ Internet ──→ TLS proxy ──┐
|
|
│ │
|
|
Tor peer ───┼─ onion ──→ tor daemon ──┤
|
|
│ ├──→ Caddy:8080 ──→ Node:3000
|
|
Yggdrasil peer ─┼─ ygg ──→ ygg IPv6 ──┤
|
|
│ │
|
|
NetBird peer ──┼─ WG ──→ netbird IP ──┘
|
|
```
|
|
|
|
All non-TLS: the external proxy terminates TLS, Tor encrypts its own path, Yggdrasil/NetBird are encrypted overlays. Caddy and the node app speak plain HTTP.
|
|
|
|
---
|
|
|
|
## 1. Node app
|
|
|
|
`systemd` unit at `/etc/systemd/system/buildfor-life-budget.service`:
|
|
|
|
```ini
|
|
[Unit]
|
|
Description=B4L Budget (SvelteKit adapter-node)
|
|
After=network-online.target postgresql.service
|
|
Wants=network-online.target
|
|
|
|
[Service]
|
|
Type=simple
|
|
User=budget
|
|
WorkingDirectory=/opt/buildfor-life-budget
|
|
EnvironmentFile=/opt/buildfor-life-budget/.env
|
|
ExecStart=/usr/bin/node build/index.js
|
|
Restart=on-failure
|
|
RestartSec=5
|
|
|
|
# Hardening
|
|
NoNewPrivileges=true
|
|
ProtectSystem=strict
|
|
ProtectHome=true
|
|
ReadWritePaths=/var/lib/buildfor-life-budget/uploads
|
|
PrivateTmp=true
|
|
|
|
[Install]
|
|
WantedBy=multi-user.target
|
|
```
|
|
|
|
`/opt/buildfor-life-budget/.env` must contain:
|
|
|
|
```
|
|
PORT=3000
|
|
HOST=127.0.0.1
|
|
ORIGIN=https://budget.example.com
|
|
PROTOCOL_HEADER=x-forwarded-proto
|
|
HOST_HEADER=x-forwarded-host
|
|
BODY_SIZE_LIMIT=26214400
|
|
DATABASE_URL=postgresql://budget_app:...@127.0.0.1:5432/buildfor_life_budget
|
|
UPLOADS_DIR=/var/lib/buildfor-life-budget/uploads
|
|
FAVICON_FETCH_ENABLED=true
|
|
```
|
|
|
|
`ORIGIN` stays the public HTTPS URL — SvelteKit uses this for CSRF and cookie checks. If you hit the app via Tor/Yggdrasil/NetBird with a different Host header, either:
|
|
- set `csrf.checkOrigin: false` in `svelte.config.js`, or
|
|
- access each transport via a Host that matches `ORIGIN` (point DNS / hosts-file entries at the transport IPs).
|
|
|
|
The second approach is cleaner — all transports see the same `Host: budget.example.com` and SvelteKit doesn't care which IP the connection came from.
|
|
|
|
```bash
|
|
sudo systemctl daemon-reload
|
|
sudo systemctl enable --now buildfor-life-budget.service
|
|
```
|
|
|
|
---
|
|
|
|
## 2. Internal Caddy
|
|
|
|
Package: `sudo apt install caddy` (or use the official repo for latest).
|
|
|
|
`/etc/caddy/Caddyfile`:
|
|
|
|
```caddyfile
|
|
{
|
|
# Off — TLS is handled by the upstream external proxy.
|
|
auto_https off
|
|
admin off
|
|
}
|
|
|
|
# Listen on every transport-facing IP. Same backend.
|
|
# LAN IP · Yggdrasil IPv6 · NetBird IP · localhost (for Tor)
|
|
http://192.168.10.5:8080,
|
|
http://[200:abcd:1234:...]:8080,
|
|
http://100.64.0.5:8080,
|
|
http://127.0.0.1:8080 {
|
|
|
|
# Forward real client info so SvelteKit's PROTOCOL_HEADER/HOST_HEADER work.
|
|
# The external TLS proxy should set X-Forwarded-Proto=https; default to http for
|
|
# the overlays (Tor/Ygg/NetBird) which do their own encryption.
|
|
@fromTls header X-Forwarded-Proto https
|
|
header @fromTls X-Forwarded-Proto https
|
|
|
|
reverse_proxy 127.0.0.1:3000 {
|
|
header_up Host {host}
|
|
header_up X-Real-IP {remote_host}
|
|
header_up X-Forwarded-For {remote_host}
|
|
header_up X-Forwarded-Proto {scheme}
|
|
header_up X-Forwarded-Host {host}
|
|
}
|
|
|
|
encode zstd gzip
|
|
request_body {
|
|
max_size 26MB
|
|
}
|
|
|
|
log {
|
|
output file /var/log/caddy/budget.log {
|
|
roll_size 50mb
|
|
roll_keep 10
|
|
}
|
|
format console
|
|
}
|
|
}
|
|
```
|
|
|
|
Replace the three IPs with the real values:
|
|
- **LAN IP** — `ip -4 -br addr show | grep -v lo` on the host.
|
|
- **Yggdrasil IPv6** — `yggdrasilctl getSelf | grep -i address` (starts with `200:` or `201:`).
|
|
- **NetBird IP** — `netbird status | grep 'NetBird IP'` (usually `100.64.0.0/10` range).
|
|
|
|
```bash
|
|
sudo systemctl enable --now caddy
|
|
sudo systemctl reload caddy # after edits
|
|
```
|
|
|
|
**Firewall** — the box should accept `:8080` inbound only on those four interfaces. With `ufw`:
|
|
|
|
```bash
|
|
sudo ufw allow in on eth0 from 192.168.10.0/24 to any port 8080 proto tcp # LAN
|
|
sudo ufw allow in on tun-ygg to any port 8080 proto tcp # Yggdrasil
|
|
sudo ufw allow in on wt0 to any port 8080 proto tcp # NetBird (interface name varies)
|
|
# Tor connects via 127.0.0.1 — no firewall rule needed
|
|
sudo ufw deny 8080/tcp # block the rest
|
|
```
|
|
|
|
---
|
|
|
|
## 3. Tor onion service
|
|
|
|
Package: `sudo apt install tor`. Append to `/etc/tor/torrc`:
|
|
|
|
```
|
|
HiddenServiceDir /var/lib/tor/budget/
|
|
HiddenServicePort 80 127.0.0.1:8080
|
|
HiddenServiceVersion 3
|
|
```
|
|
|
|
```bash
|
|
sudo systemctl restart tor
|
|
sudo cat /var/lib/tor/budget/hostname
|
|
```
|
|
|
|
The printed `<random>.onion` is the address. Protect the contents of `/var/lib/tor/budget/` — anyone with the private key can impersonate this service. Back them up offline.
|
|
|
|
---
|
|
|
|
## 4. Yggdrasil
|
|
|
|
Package: official `.deb` from [yggdrasil-network.github.io](https://yggdrasil-network.github.io/installation-linux.html).
|
|
|
|
After install, Yggdrasil picks a stable IPv6 address from the `200::/7` range. To see it:
|
|
|
|
```bash
|
|
sudo yggdrasilctl getSelf
|
|
```
|
|
|
|
No Yggdrasil-side config needed for this app — peers who have your Yggdrasil address can reach `http://[200:...]:8080` once they're on the network.
|
|
|
|
Add trusted peers in `/etc/yggdrasil.conf`'s `Peers` block, or rely on public peers listed at [publicpeers.neilalexander.dev](https://publicpeers.neilalexander.dev/).
|
|
|
|
---
|
|
|
|
## 5. NetBird
|
|
|
|
Package: official from `pkgs.netbird.io`. Set up:
|
|
|
|
```bash
|
|
sudo netbird up --setup-key <your-netbird-setup-key>
|
|
sudo netbird status
|
|
```
|
|
|
|
Note the IP under `NetBird IP` — that's what Caddy binds to. NetBird manages WireGuard tunnels; any peer in the same NetBird network can hit that IP.
|
|
|
|
Keep the daemon running via the included `netbird.service`.
|
|
|
|
---
|
|
|
|
## 6. External reverse proxy (TLS termination)
|
|
|
|
This sits on your network edge, not on the app box. Example Caddyfile on the edge:
|
|
|
|
```caddyfile
|
|
budget.example.com {
|
|
reverse_proxy 192.168.10.5:8080 {
|
|
header_up X-Forwarded-Proto https
|
|
header_up X-Forwarded-Host {host}
|
|
header_up Host {host}
|
|
}
|
|
}
|
|
```
|
|
|
|
Caddy auto-provisions a Let's Encrypt cert. If you use nginx/Traefik instead, forward the same headers so the internal Caddy and the SvelteKit app see the correct `https` protocol and public host.
|
|
|
|
---
|
|
|
|
## Update / redeploy
|
|
|
|
On the app box:
|
|
|
|
```bash
|
|
cd /opt/buildfor-life-budget
|
|
sudo -u budget git pull
|
|
sudo -u budget npm ci --omit=dev=false
|
|
sudo -u budget npm run build
|
|
sudo -u budget npm run db:push # only if schema changed
|
|
sudo systemctl restart buildfor-life-budget
|
|
```
|
|
|
|
Caddy config does not need reloading unless IPs change.
|
|
|
|
---
|
|
|
|
## Troubleshooting
|
|
|
|
| Symptom | Likely cause |
|
|
|---|---|
|
|
| External URL 502 from the edge proxy | Internal Caddy not up, or LAN firewall blocking `:8080` from the edge. |
|
|
| `.onion` loads but forms return 403 | CSRF origin mismatch — either configure a hosts-file entry for the onion pointing to `budget.example.com`, or disable `csrf.checkOrigin` in `svelte.config.js`. |
|
|
| Yggdrasil peer can't reach the address | Both sides must be on Yggdrasil; check `yggdrasilctl getPeers` on both ends. |
|
|
| NetBird IP changes after reconnect | Setup key vs reusable key — use a fixed/dedicated peer config so the IP stays stable. |
|
|
| SvelteKit sees HTTP for TLS-terminated traffic | External proxy not sending `X-Forwarded-Proto: https` header. |
|
|
| Scheduler doesn't tick | Systemd unit restart killed the in-process interval; check logs for `[scheduler] recurring bills started`. |
|
|
|
|
---
|
|
|
|
## Security notes
|
|
|
|
- The internal Caddy MUST NOT be reachable from the internet directly. The four-IP bind list keeps it on private transports only; verify with `ss -tlnp | grep :8080` that no `0.0.0.0:8080` shows up.
|
|
- `.onion` key files and NetBird setup keys are secrets — back them up to an offline medium, not to the repo.
|
|
- PostgreSQL should bind to `127.0.0.1` only (`listen_addresses = 'localhost'` in `postgresql.conf`).
|
|
- File uploads under `UPLOADS_DIR` are served via auth-checked endpoints only — never expose that directory statically.
|