StatsHub Docs
Guides

Host a preview

Run the whole stack in Docker on the Hetzner box, on real URLs, from a commit or the working tree.

bun run deploy:preview builds every service into a Docker image and hosts the lot on the Hetzner box, behind Caddy, on URLs you can send someone. It is the same deploy path the push workflow uses — see Hosting for what the box runs and why there is no external build platform.

Every push to main deploys automatically when the local systemd timer sees the new remote SHA. Pushing to dev — the default branch, and where the work goes — does not, so the commands below are how you look at unreleased work: an explicit redeploy, a partial service build, or an uncommitted snapshot.

PREVIEW_DB_URL='postgresql://…' bun run deploy:preview
PREVIEW_DB_URL='postgresql://…' bun run deploy:preview -- --ref main
PREVIEW_DB_URL='postgresql://…' bun run deploy:preview -- web docs
PREVIEW_DB_URL='postgresql://…' bun run deploy:preview -- --worktree
bun run deploy:preview:status
bun run deploy:preview:down

The link table in the root README.md is generated from the running containers on every deploy, so it describes what is actually up rather than what was intended.

One project, one network

Every service goes into a single compose project. That is load-bearing: docker-compose.yml points web and admin at http://api:8080. Per-app projects would put each service on its own network and break that name.

Only the published port, the container name and the labels are overridden. The image is built from the same docker-compose.yml that ships to production, so a green preview is evidence the real deploy will be green too.

Ports are an implementation detail

Services publish on 127.0.0.1 in the 4100-4199 range and are reachable only through Caddy. The hostname is the stable address; the port is assigned in sorted service order and may shift when a service is added.

The local postgres Compose service never comes up in a public preview. Set PREVIEW_DB_URL to a populated Postgres database. If its hostname is available only on a Docker network, set PREVIEW_DB_NETWORK too. The deploy stops before building when the URL is missing, and refuses to publish the API unless its country list and full tournament catalog contain real product data.

The API also loads its server-side credentials from apps/statshub-api/.env by default. Set PREVIEW_API_ENV_FILE when they live elsewhere. The file is required, is attached only at container runtime, and is never copied into an image. Public product reads remain public; user and admin routes keep their normal authentication, and the preview explicitly disables the development-only admin bypass.

It builds a commit, not your desk

The default input is git archive of a real commit: this repo is routinely mid-refactor with several agents writing to it, and a source tree that changes halfway through a build produces an image matching no revision.

--worktree opts into the uncommitted tree, and even then the tree is snapshotted to a temp directory first, so an edit landing mid-build cannot reach the image.

HEAD does not always build

A commit that exists is not a commit that builds. If bun.lock is stale for the commit you picked, every Next app dies at bun install --frozen-lockfile about 40 seconds in. The build loop carries on to the next service and the summary names what failed — four broken apps should not cost you the two that work.

URLs

Hostnames are <service>.<domain>, where the domain defaults to this box's IPv4 under sslip.io — it resolves without anyone touching DNS, and Caddy can get a real certificate for it. Set PREVIEW_DOMAIN to use a wildcard you own.

VariableDefault
PREVIEW_DOMAIN<ipv4>.sslip.ioHost suffix.
PREVIEW_SCHEMEhttpshttp (or --http) skips the certificate.
PREVIEW_PORTS4100-4199Loopback range to allocate from.
PREVIEW_DB_URLnone; requiredPopulated Postgres connection used at runtime.
PREVIEW_DB_NETWORKnoneExternal Docker network needed to resolve the database host.
PREVIEW_API_ENV_FILEapps/statshub-api/.envServer-side API credentials mounted at runtime.

NEXT_PUBLIC_BASE_URL, NEXT_PUBLIC_DOCS_URL and NEXT_PUBLIC_DASHBOARD_URL are pointed at the preview's own hostnames, as build args and runtime environment, so the hosted copy links to itself instead of sending anyone who clicks a link back to production. API_ORIGIN stays the in-network api:8080, because that call is made server-side.

These URLs are on the public internet

Caddy serves them to anyone. Within minutes of the API going up its log fills with scanners probing /login.action and /v2/_catalog. Product data is intentionally public; authenticated user and admin operations still require valid credentials. Take the preview down when you are finished.

Taking it down

bun run deploy:preview:down removes the containers and the network, deletes the Caddy site file, reloads, and rewrites the README table to match. Services inherit restart: unless-stopped from the compose anchor, so a stack you forget survives a reboot and keeps holding its ports and certificates.

On this page