# 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 forgejo/ the Forgejo stack (own compose file); web UI behind Caddy, git-over-SSH published on 2222 ``` ## 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. ## Forgejo stack A single-user Forgejo instance (the owner's self-hosted git server) at `git.waldson.com.br`. It lives in `forgejo/` and deploys to `/home/waldson/central-proxy/forgejo` on the droplet. Only git-over-SSH is published on the host (`2222/tcp`), so the host's admin sshd keeps port 22. The web UI (`3000`) is **not** published; it is reached only through Caddy at `git.waldson.com.br`. The central `Caddyfile` carries the `git.waldson.com.br` block. SQLite is the database, and the whole instance is configured from environment variables in `compose.yaml` (registration disabled, Forgejo Actions disabled, correct clone-URL host/port), so the security posture is reproduced on every recreate rather than depending on installer clicks. ### Where data lives Everything - git repositories, the SQLite database (`/data/gitea/gitea.db`), the generated `app.ini`, and the container's SSH host keys - lives in the `forgejo_data` named Docker volume mounted at `/data`. That one volume is the entire instance, so `docker compose up -d --force-recreate` preserves all users and repositories. Never `docker volume rm forgejo_data`: it discards every repo and account. (Off-site backup of this volume is a separate slice: a nightly `forgejo dump` into a Syncthing-shared folder.) ### Deploy From `forgejo/` on the droplet: ```sh docker compose up -d ``` The container comes up as `forgejo-forgejo-1` on `proxy_net`, which is the name Caddy reverse-proxies to. Recreating it preserves users and repositories: ```sh docker compose up -d --force-recreate ``` ### First-run setup (interactive) The web installer is locked (`INSTALL_LOCK=true`), so the single admin account is created from the CLI once, after the container is first up: ```sh docker exec -u git forgejo-forgejo-1 \ forgejo admin user create \ --admin --username --email \ --random-password --must-change-password ``` Log in at `https://git.waldson.com.br`, change the password, and add your SSH public key under Settings -> SSH / GPG Keys. Registration is already disabled, so there is no sign-up path for anyone else. ### Cloning over SSH (port 2222) Forgejo advertises `ssh://git@git.waldson.com.br:2222//.git`. To keep remotes short (`git@git.waldson.com.br:/.git`) on a personal machine, add this to `~/.ssh/config`: ``` Host git.waldson.com.br HostName git.waldson.com.br Port 2222 User git IdentityFile ~/.ssh/id_ed25519 ``` With that entry, `git clone git@git.waldson.com.br:/.git` works without spelling out the port. HTTPS clone/pull works through Caddy with no SSH setup at all: `git clone https://git.waldson.com.br//.git`. ### Upgrade The image is pinned to a Forgejo major version (`codeberg.org/forgejo/forgejo:11`). Patch and minor updates within that major are safe: ```sh docker compose pull && docker compose up -d ``` A major bump (editing the tag to the next number) runs one-way database migrations. Do it one major at a time, back up `forgejo_data` first, and read the Forgejo release notes before changing the tag. ## 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.