395 lines
14 KiB
Markdown
395 lines
14 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
|
|
forgejo/ the Forgejo stack (own compose file); web UI behind Caddy,
|
|
git-over-SSH published on 2222
|
|
ntfy/ the ntfy stack (own compose file); push server behind Caddy,
|
|
nothing published (pure HTTP through the proxy)
|
|
```
|
|
|
|
## 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 <admin> --email <you@example.com> \
|
|
--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/<user>/<repo>.git`. To keep
|
|
remotes short (`git@git.waldson.com.br:<user>/<repo>.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:<user>/<repo>.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/<user>/<repo>.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.
|
|
|
|
## ntfy stack
|
|
|
|
A self-hosted [ntfy](https://ntfy.sh) push-notification server (the owner's
|
|
alerts endpoint for scripts, cron, and the Android app) at
|
|
`push.waldson.com.br`. It lives in `ntfy/` and deploys to
|
|
`/home/waldson/central-proxy/ntfy` on the droplet.
|
|
|
|
Nothing is published on the host: ntfy is pure HTTP and reached only through
|
|
Caddy at `push.waldson.com.br`, so it rides Caddy's existing 80/443 and opens no
|
|
new firewall port. Caddy transparently upgrades the long-lived WebSocket
|
|
connections the Android app and web UI use for instant delivery. The central
|
|
`Caddyfile` carries the `push.waldson.com.br` block.
|
|
|
|
Access is **deny-all**: `NTFY_AUTH_DEFAULT_ACCESS=deny-all` means no topic can be
|
|
published or subscribed without an authenticated user or token, so there is no
|
|
open surface for strangers even though the URL is public. The whole instance is
|
|
configured from `NTFY_*` environment variables in `compose.yaml` (no
|
|
`server.yml`), so the posture is reproduced on every recreate. The container runs
|
|
as UID 1000, which cannot bind port 80, so ntfy listens on `:8080` inside the
|
|
container and Caddy reverse-proxies to `ntfy-ntfy-1:8080` - the same high-port
|
|
shape as Syncthing (`:8384`) and Forgejo (`:3000`).
|
|
|
|
### Where data lives
|
|
|
|
The auth database (`user.db`), the message cache (`cache.db`), and uploaded
|
|
attachments all live under the host bind mount `ntfy/data` (mounted at
|
|
`/var/lib/ntfy` in the container), so state survives container recreation and is
|
|
inspectable over plain SSH. The directory is git-ignored via the repo's `data/`
|
|
rule. The container runs as `1000:1000`; if the droplet's deploy user has a
|
|
different id, adjust `user:` in `compose.yaml` so the db files stay owned by that
|
|
user.
|
|
|
|
### Deploy
|
|
|
|
From `ntfy/` on the droplet:
|
|
|
|
```sh
|
|
docker compose up -d
|
|
```
|
|
|
|
The container comes up as `ntfy-ntfy-1` on `proxy_net`, which is the name Caddy
|
|
reverse-proxies to. A restart preserves users, tokens, and cached messages:
|
|
|
|
```sh
|
|
docker compose restart
|
|
```
|
|
|
|
### First-run setup (CLI)
|
|
|
|
There is no web installer and no sign-up path. After the container is first up,
|
|
create the admin user and (for scripts) a publish token via the CLI:
|
|
|
|
```sh
|
|
# Admin can read/write every topic (prompts for a password).
|
|
docker exec -it ntfy-ntfy-1 ntfy user add --role=admin <admin>
|
|
|
|
# A token for unattended publishing from scripts/cron.
|
|
docker exec -it ntfy-ntfy-1 ntfy token add <admin>
|
|
```
|
|
|
|
For a non-admin user, grant per-topic access explicitly (deny-all denies
|
|
everything else):
|
|
|
|
```sh
|
|
docker exec -it ntfy-ntfy-1 ntfy user add <name>
|
|
docker exec -it ntfy-ntfy-1 ntfy access <name> <topic> rw
|
|
```
|
|
|
|
Publish with the token from anywhere:
|
|
|
|
```sh
|
|
curl -H "Authorization: Bearer <token>" -d "backup finished" \
|
|
https://push.waldson.com.br/<topic>
|
|
```
|
|
|
|
In the Android app, add the server `https://push.waldson.com.br`, sign in with
|
|
the user (or set the token), and subscribe to the topic.
|
|
|
|
### Upgrade
|
|
|
|
The image is pinned to the current ntfy major (`binwiederhier/ntfy:v2`). Patch
|
|
and minor updates within that major are safe:
|
|
|
|
```sh
|
|
docker compose pull && docker compose up -d
|
|
```
|
|
|
|
A major bump is a deliberate tag edit; read the ntfy release notes first.
|
|
|
|
## 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 |
|
|
|
|
ntfy adds no row here: it is reached only through Caddy on 80/443. 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 |
|
|
| push.waldson.com.br| A | 174.138.36.232 | ntfy push server |
|
|
|
|
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.
|