central-proxy/README.md
Waldson Patrício 217efdace1 [002] Add Syncthing stack behind Caddy at sync.waldson.com.br
Run an always-on Syncthing peer on the droplet as an off-site copy for the
owner's folders. New syncthing/ compose stack on proxy_net: the sync protocol
is published on host 22000 (tcp+udp) for direct device connections, while the
GUI (8384) is unpublished and reached only through the central Caddy, which
gains a sync.waldson.com.br reverse-proxy block. Config and folder data live on
a host bind mount for SSH inspection and are git-ignored. README documents the
deploy, upgrade, and interactive pairing runbook.

Binding the GUI to a non-loopback address (STGUIADDRESS) also disables
Syncthing's DNS-rebinding host check, so the proxy needs no Host rewrite.
2026-07-23 13:34:10 -03:00

214 lines
7.8 KiB
Markdown

# central-proxy
The shared edge reverse proxy for the DigitalOcean droplet (`174.138.36.232`).
A single **Caddy** container terminates TLS for every site on the box and routes
each hostname to the right upstream container. It is the only thing published on
ports 80 and 443; all the application stacks (bigode, nazareonline, ...) sit
behind it on the external `proxy_net` Docker network and speak plain HTTP to it.
Caddy manages Let's Encrypt certificates automatically - it obtains and renews a
cert for each hostname listed in the `Caddyfile` on first request. Issued certs
and ACME account keys persist in the `caddy_data` Docker volume (mounted at
`/data`), not in this repo.
## Layout
```
compose.yaml the Caddy service: publishes 80/443, joins proxy_net,
bind-mounts the Caddyfile, persists certs in caddy_data
Caddyfile one site block per app; each reverse_proxies to a container
reachable by name on proxy_net
syncthing/ the Syncthing stack (own compose file); GUI behind Caddy,
sync protocol published on 22000
```
## Prerequisites
- The external network exists: `docker network create proxy_net`.
- Each upstream stack attaches its public-facing container to `proxy_net`, so
Caddy can reach it by container name (e.g. `nazareonline-web-1:80`).
- DNS A records for every hostname in the `Caddyfile` point at the droplet.
## Adding a site
Add a block to the `Caddyfile` naming the hostname(s) and the upstream:
```
www.example.com, example.com {
reverse_proxy example-web-1:80
}
```
The upstream container must be on `proxy_net`. Caddy forwards the original `Host`
and sets `X-Forwarded-Proto: https`, so upstreams behind it can rebuild HTTPS
URLs and do their own host canonicalisation.
Then reload without downtime (does not disturb the other sites):
```sh
docker exec central-proxy-caddy-1 caddy reload --config /etc/caddy/Caddyfile
```
Validate a change before reloading:
```sh
docker run --rm -v "$PWD/Caddyfile":/etc/caddy/Caddyfile:ro caddy:2-alpine \
caddy validate --config /etc/caddy/Caddyfile
```
## Deploy
The compose stack lives at `/home/waldson/central-proxy` on the droplet. To
apply a change, copy the edited `Caddyfile` up and reload:
```sh
scp -i ~/.ssh/deploy_key ./Caddyfile waldson@174.138.36.232:/home/waldson/central-proxy/Caddyfile
ssh -i ~/.ssh/deploy_key waldson@174.138.36.232 \
'docker exec central-proxy-caddy-1 caddy reload --config /etc/caddy/Caddyfile'
```
Bring the proxy up (or recreate it after a compose change) from that directory:
```sh
docker compose up -d
```
## Syncthing stack
An always-on Syncthing peer for the owner's folders, giving devices somewhere
to sync when no two of them are online together and keeping an off-site copy on
the droplet. It lives in `syncthing/` and deploys to
`/home/waldson/central-proxy/syncthing` on the droplet.
Only the sync protocol is published on the host (`22000/tcp` and `22000/udp`),
so devices connect directly instead of through community relays. The web GUI
(`8384`) is **not** published; it is reached only through Caddy at
`sync.waldson.com.br`, guarded by Syncthing's own username/password. The
central `Caddyfile` already carries the `sync.waldson.com.br` block. Binding the
GUI to a non-loopback address (`STGUIADDRESS=0.0.0.0:8384`, required so Caddy in
its own container can reach it) also turns off Syncthing's DNS-rebinding host
check, so a plain `reverse_proxy` needs no `Host`-header rewrite.
### Where data lives
Config (device keys, the folder list) and every synced folder live under the
host bind mount `syncthing/data` (mounted at `/var/syncthing` in the container),
so state survives container recreation and is inspectable over plain SSH. The
directory is git-ignored. `PUID`/`PGID` default to `1000`; if the droplet's
deploy user has a different id, adjust them in `compose.yaml` so the synced
files stay owned by that user.
### Deploy
From `syncthing/` on the droplet:
```sh
docker compose up -d
```
The container comes up as `syncthing-syncthing-1` on `proxy_net`, which is the
name Caddy reverse-proxies to. A restart preserves config and pairings:
```sh
docker compose restart
```
### Upgrade
The image is `syncthing/syncthing:latest` (Syncthing publishes no rolling major
tag; pin a specific version here if you want reproducible upgrades). To update:
```sh
docker compose pull && docker compose up -d
```
### First-run setup (interactive, in the GUI)
These steps are done once by the owner at `https://sync.waldson.com.br`:
1. Set the GUI username and password (Actions -> Settings -> GUI).
2. Add the droplet as a device on each personal device (and vice versa), using
the device IDs shown under Actions -> Show ID.
3. Share all five folders (Sync, phone-photos, comprovantes, notes,
torque-data) with the droplet and accept them on the droplet, pointing each
folder path under `/var/syncthing` so the data lands in the bind mount.
## Droplet groundwork
Baseline host configuration for the droplet (`wloud`, 174.138.36.232,
2 GB RAM, 2 vCPUs, 58 GB disk). Everything below was set up once and only
needs revisiting when the host changes.
### Swap
The droplet has 2 GB RAM and no swap by default; a 2 GB swapfile prevents
Out Of Memory (OOM) kills during on-droplet builds and daemon memory spikes.
It is created once with (requires sudo):
```sh
sudo fallocate -l 2G /swapfile
sudo chmod 600 /swapfile
sudo mkswap /swapfile
sudo swapon /swapfile
echo '/swapfile none swap sw 0 0' | sudo tee -a /etc/fstab
echo 'vm.swappiness=10' | sudo tee /etc/sysctl.d/99-swappiness.conf
sudo sysctl -p /etc/sysctl.d/99-swappiness.conf
```
The fstab entry makes it survive reboots; swappiness 10 keeps the kernel from
swapping eagerly. Verify with `free -m` (~2048 total swap) and, after a
reboot, `swapon --show`.
### Firewall
`ufw` is installed and active on the host. Probing from outside the droplet
(2026-07-23) showed no filtering in the path for the service ports: 2222/tcp
and 22000/tcp answered "connection refused" while nothing listened, which
means packets reach the host, so neither ufw nor a DigitalOcean cloud
firewall blocks them. Ports in use:
| Port | Proto | Service |
|-----------|---------|--------------------------------|
| 22 | tcp | host sshd |
| 80, 443 | tcp | Caddy (all HTTP/HTTPS traffic) |
| 2222 | tcp | Forgejo git-over-SSH |
| 22000 | tcp+udp | Syncthing sync protocol |
If ufw rules are ever tightened, keep the four rows above allowed. To
re-verify reachability from any machine outside the droplet:
```sh
nc -vz 174.138.36.232 2222
nc -vz 174.138.36.232 22000
nc -vzu 174.138.36.232 22000
```
"Connection refused" means the port is reachable but nothing listens yet;
only a timeout indicates a filter.
### DNS
All records live on the DigitalOcean nameservers (ns1-3.digitalocean.com)
and point at the droplet:
| Record | Type | Value | Serves |
|--------------------|------|-----------------|----------------------|
| waldson.com.br | A | 174.138.36.232 | personal site |
| www.waldson.com.br | A | 174.138.36.232 | redirect to apex |
| git.waldson.com.br | A | 174.138.36.232 | Forgejo |
| sync.waldson.com.br| A | 174.138.36.232 | Syncthing GUI |
personaledson.com.br and nazareonline.com.br also resolve here for the
existing stacks. Check resolution with:
```sh
dig +short waldson.com.br www.waldson.com.br git.waldson.com.br sync.waldson.com.br
```
## Notes
- `letsencrypt/acme.json` is leftover state from an earlier Traefik-based proxy
and is not used by Caddy. It is git-ignored (it contains private keys); it can
be deleted from the droplet once you are sure nothing references it.
- Never `docker volume rm caddy_data` - that discards every issued certificate
and forces re-issuance, which can hit Let's Encrypt rate limits.