Appearance
Multiple projects on one box
A 4 GB box has plenty of room left over. DEPLOY.md measures gammonchain at 71 MB idle and about 231 MB under 200 concurrent long matches. The deploy/ layout lets other projects share the box without editing any existing project's config. That property decides whether a shared box stays pleasant or turns into a tangle.
What is shared, and what is not
Caddy is the only component that really has to be shared. It holds the certificate store and is the only thing bound to ports 80 and 443. Everything else belongs to one project:
deploy/Caddyfile shared; names no project, just `import sites/*.caddy`
deploy/sites/*.caddy one file per project
docker network `edge` created once by setup.sh; Caddy reaches each project's container by name
projects/<name>/ each extra project's own compose stack, volumes and .envdeploy/Caddyfile contains nothing project-specific:
import sites/*.caddyAdding a project
bash
./deploy/new-project.sh myapp myapp.com 3000Arguments: <name> <domain> [container-port]. The port defaults to 8080. The script:
- refuses to run if
deploy/sites/<name>.caddyalready exists; - writes
deploy/sites/<name>.caddy, a site block for<domain>, www.<domain>that reverse-proxies to<name>:<port>; - writes
projects/<name>/docker-compose.yml, one service named<name>, built from.,restart: unless-stopped,exposeon the port, joined to the externaledgenetwork, reading.env; - creates an empty
projects/<name>/.envand addsprojects/*/.envto.gitignoreif it is missing.
Then:
- put the app, with a Dockerfile, in
projects/<name>/ - point the domain at the box
cd projects/<name> && docker compose up -d --build- reload Caddy:
docker compose -f deploy/docker-compose.prod.yml exec caddy caddy reload --config /etc/caddy/Caddyfile(push.shalso does this on every deploy)
Two ways to be reachable
A domain, which is almost always the right choice. Caddy already holds 80/443 and routes by hostname, so another website is another file, not another hole in the firewall. The project publishes no host port at all. It sits on the edge network, so only Caddy can reach it, and nothing can reach it without TLS.
A raw port, for something that is not HTTP. Add it to deploy/ports.conf, run sudo ./deploy/firewall.sh, and open it in the Hetzner Cloud Firewall too. Publishing the port in compose alone is neither enough nor safe. See Firewall.
The generated site file matches gammonchain.caddy: it uses tls internal for a domain proxied by Cloudflare with SSL mode Full, and it does not set X-Forwarded-For or X-Forwarded-Proto. Caddy's default appends the peer, so Cloudflare's real client address stays the left-most entry; replacing the header with {remote_host} would collapse every client into a few Cloudflare edge addresses and rate-limit buckets. If the new domain is not behind Cloudflare, delete the tls internal line and Caddy obtains a Let's Encrypt certificate instead. See Deploy to a VPS.
push.sh and other projects
push.sh rsyncs the whole working directory to /opt/gammonchain with --delete, but that deletion is fenced in:
- it never acts outside
APP_DIR, and the script refuses anAPP_DIRthat is/, a top-level directory or a home directory; - it never deletes anything under
projects/or anydeploy/sites/*.caddy, so a project created on the box withnew-project.shsurvives a gammonchain deploy from a laptop that does not have it (files you do have locally are still sent and updated); - it never copies or deletes any
.env(at any depth),projects/*/data/or gammonchain's own runtime data.