StatsHub Docs

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 only deploy path this repo has — see Hosting for what the box runs and why there is nowhere else to push.

bun run deploy:preview                 # the whole stack, from HEAD
bun run deploy:preview -- --ref main   # from a specific commit
bun run deploy:preview -- web docs     # only these, into the same stack
bun run deploy:preview -- --worktree   # the uncommitted working tree
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, and the apps reach the database as postgres:5432. Per-app projects would put each service on its own network and break every one of those names.

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.

postgres comes up with the stack but gets no hostname — it serves no HTTP, and docker-compose.yml says outright it is not part of any deployment. Its compose default of 5432 is dropped, because the dev database already holds that port.

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.

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. Do not host a stack holding real credentials or production data without putting auth in front of it, and take it 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