Hosting
Where the deployables actually run — one Hetzner box, Docker Compose behind Caddy, and no cloud platform in the path.
Everything StatsHub serves runs on a single Hetzner server, and that same box is
the machine the repo is developed on. There is no PaaS account, no build farm
and no deploy webhook. Knowing that explains most of what follows: the URLs, why
a deploy works while git push does not, and why nothing here asks you to log
in to a dashboard.
One box, every service
bun run deploy:preview builds each service into a Docker image and runs the
lot as one Compose project, statshub-preview, on one network. Containers bind
loopback only; Caddy is the sole public listener.
| Service | URL | Container | Loopback |
|---|---|---|---|
web | web.5.9.14.80.sslip.io | statshub-preview-web | 127.0.0.1:4105 |
api | api.5.9.14.80.sslip.io | statshub-preview-api | 127.0.0.1:4101 |
admin | admin.5.9.14.80.sslip.io | statshub-preview-admin | 127.0.0.1:4100 |
docs | docs.5.9.14.80.sslip.io | statshub-preview-docs | 127.0.0.1:4103 |
csv-deduplicator | csv-deduplicator.5.9.14.80.sslip.io | statshub-preview-csv-deduplicator | 127.0.0.1:4102 |
postgres | none | statshub-preview-postgres | none |
Ports are handed out in sorted service order from PREVIEW_PORTS, which is why
there is a gap at 4104: postgres takes its place in that order but publishes
nothing. It serves no HTTP and gets no hostname — the apps reach it in-network
as postgres:5432, and web and admin reach the API as http://api:8080.
Those names are why the whole stack has to be one Compose project.
The link table in the root README.md is regenerated from the running
containers on every deploy, so it describes what is up rather than what was
intended.
A deploy never leaves the machine
scripts/deploy/preview.sh takes git archive of a commit, builds images from
that tarball, and starts them. Nothing is uploaded anywhere and no remote is
contacted.
That has a consequence worth stating plainly, because it looks like a problem
and is not: this box has no GitHub credentials, so git push fails — and
deploying still works. The commit you deploy only has to exist locally.
Unpushed work is deployable work.
A commit that exists is not a commit that builds
The default ref is HEAD, and HEAD is whatever six agents last landed. If
bun.lock is stale for that commit, every Next app dies at bun install --frozen-lockfile about forty seconds into its Docker build. Deploy a ref you
chose — --ref <sha> — when it matters.
Caddy owns the edge
scripts/deploy/preview.sh writes /srv/caddy/preview-statshub.caddy and
reloads Caddy. The file is generated on every deploy and carries a header
saying so; hand-edits are overwritten. Each entry is a one-line
reverse_proxy from a hostname to a loopback port.
Hostnames are service plus the domain, which defaults to this box's IPv4
under sslip.io. sslip.io resolves any name containing an IP address to that
address, so web.5.9.14.80.sslip.io points here without anyone creating a DNS
record — and because it is a real public name, Caddy can get a real certificate
for it. Set PREVIEW_DOMAIN to serve a wildcard you own instead.
These URLs are on the public internet
Caddy serves them to anyone who asks. Within minutes of the API going up its
log fills with scanners probing /login.action and /v2/_catalog. Do not
host a stack holding real credentials or client data without auth in front of
it, and take it down with bun run deploy:preview:down when you are done.
Healthy does not mean correct
Every container ships a healthcheck, and each one asks a single question:
does /api/health answer. A Next app whose static assets are missing answers
it perfectly — the route is server-rendered, the container reports healthy,
Caddy proxies happily, and the site renders as unstyled HTML because every
/_next/static/* request 404s.
That is not hypothetical. The statshub-* directory rename left
scripts/prepare-standalone.mjs copying public/ and .next/static/ into
.next/standalone/apps/web while the server ran from apps/statshub-web, and
the stack reported five healthy services while web served no CSS at all. The
scripts now derive that path from the package directory so a rename cannot
repeat it.
When you check a deploy, fetch a stylesheet, not just the health route.
There is no platform to configure
Railway was removed from this repo: the scripts, the project id and the status command are gone, and nothing deploys there. The Dockerfiles still carry a comment block naming Coolify build-pack fields (base directory, Dockerfile location, exposed port). That describes how an image can be built by a Dockerfile build pack, and it is accurate — but no Coolify instance runs here, and no deploy in this repo goes through one.
If StatsHub ever moves to a managed host, the images are already the interface:
each app has a Dockerfile taking the repository root as its build context, and
docker-compose.yml is the contract between them.