StatsHub Docs

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.

ServiceURLContainerLoopback
webweb.5.9.14.80.sslip.iostatshub-preview-web127.0.0.1:4105
apiapi.5.9.14.80.sslip.iostatshub-preview-api127.0.0.1:4101
adminadmin.5.9.14.80.sslip.iostatshub-preview-admin127.0.0.1:4100
docsdocs.5.9.14.80.sslip.iostatshub-preview-docs127.0.0.1:4103
csv-deduplicatorcsv-deduplicator.5.9.14.80.sslip.iostatshub-preview-csv-deduplicator127.0.0.1:4102
postgresnonestatshub-preview-postgresnone

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.

On this page