Skip to content

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 .env

deploy/Caddyfile contains nothing project-specific:

import sites/*.caddy

Adding a project

bash
./deploy/new-project.sh myapp myapp.com 3000

Arguments: <name> <domain> [container-port]. The port defaults to 8080. The script:

  • refuses to run if deploy/sites/<name>.caddy already 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, expose on the port, joined to the external edge network, reading .env;
  • creates an empty projects/<name>/.env and adds projects/*/.env to .gitignore if it is missing.

Then:

  1. put the app, with a Dockerfile, in projects/<name>/
  2. point the domain at the box
  3. cd projects/<name> && docker compose up -d --build
  4. reload Caddy: docker compose -f deploy/docker-compose.prod.yml exec caddy caddy reload --config /etc/caddy/Caddyfile (push.sh also 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 an APP_DIR that is /, a top-level directory or a home directory;
  • it never deletes anything under projects/ or any deploy/sites/*.caddy, so a project created on the box with new-project.sh survives 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.

Rules engine, fairness protocol, verifier, analysis and worker: MIT. Server and client: AGPL-3.0-or-later.