Deploy: Docker
Multi-stage Dockerfile pattern for any Kumiko app. Used by all production deploys (single-VM, K3s, anywhere a container runtime exists).
Reference implementation: publicstatus/deploy/Dockerfile.
Anatomy
Section titled “Anatomy”Build stage: Bun produces dist/ + dist-server/. Runtime: Bun-only alpine image.
ARG BUN_VERSION=1.3.14
# ---------- build: produces dist/ + dist-server/ ----------FROM oven/bun:${BUN_VERSION}-alpine AS buildWORKDIR /appCOPY . .RUN bun install --frozen-lockfile
WORKDIR /appRUN bun run build
# ---------- runtime: bun + dist-server + dist + migrations ----------FROM oven/bun:${BUN_VERSION}-alpine AS runtimeWORKDIR /appCOPY --from=build /app/dist-server ./RUN bun install --productionCOPY --from=build /app/dist ./distCOPY --from=build /app/kumiko/migrations ./kumiko/migrations
ENV KUMIKO_REPO_ROOT=/appENV INIT_CWD=/app
CMD ["sh", "-c", "exec bun run server.js"]Image size: ~270 MB total (Bun + native externals + your app). Self-contained.
docker run --rm \ -e DATABASE_URL="postgresql://user:pass@host:5432/db" \ -e REDIS_URL="redis://host:6379" \ -p 3000:3000 \ ghcr.io/your-org/your-app:latestMigrate (pre-deploy step)
Section titled “Migrate (pre-deploy step)”The image includes a bundled kumiko.js CLI. Run it as an ephemeral container before starting your app:
docker run --rm \ -e DATABASE_URL="postgresql://user:pass@host:5432/db" \ ghcr.io/your-org/your-app:latest \ bun /app/kumiko.js migrate applyLegacy apps use migrate apply. New apps (kumiko/migrations/): bun /app/kumiko.js schema apply.
Required before every deploy. Idempotent — fast no-op if nothing pending. Boot-gate refuses to start the app if the schema doesn’t match.
Build args (for CI)
Section titled “Build args (for CI)”docker build \ --build-arg BUILD_VERSION="$(git describe --tags --always)" \ --build-arg BUILD_TIME="$(date -u +%Y-%m-%dT%H:%M:%SZ)" \ -t ghcr.io/your-org/your-app:${SHA} \ -t ghcr.io/your-org/your-app:latest \ -f deploy/Dockerfile .Tag both :latest and :${SHA} — :latest for rolling deploys, :${SHA} for rollbacks.
Multi-arch (arm64)
Section titled “Multi-arch (arm64)”For Hetzner CAX (arm) or Apple Silicon servers, build with QEMU emulation:
docker buildx build --platform linux/arm64 --push -t ... .In GitHub Actions: docker/setup-qemu-action@v3 + platforms: linux/arm64 on docker/build-push-action. See kumiko-platform build-image.yml for a working example.
Update awareness (default)
Section titled “Update awareness (default)”A single-page app that’s already open in a browser tab never learns about a new deploy — the tab keeps the old bundle until a hard reload. Kumiko handles this out of the box, no service worker, no config.
bun run build derives a self-contained build ID (a hash over the content-hashed asset URLs — changes whenever any asset changes) and emits it two ways:
-
baked into
index.htmlas the build that’s currently loaded:window.__KUMIKO_BUILD__ // { id: "678e3629d887", builtAt: "2026-06-18T17:18:03.260Z" } -
written to
dist/build-info.json, servedno-cache:{ "id": "678e3629d887", "builtAt": "2026-06-18T17:18:03.260Z" }
Every renderer-web app mounts an <UpdateChecker/> automatically. When the tab regains focus it fetches build-info.json and compares the server’s id against the loaded one; on a mismatch it shows a “new version available” banner with a reload button. It’s fail-safe — no baked build ID (dev, older bundle) or a fetch error means no banner, never a false positive.
window.__KUMIKO_BUILD__.builtAt is a readable timestamp you can surface as the app’s version instead of a raw git sha. It comes from the static build-info.json, not from an env-var-backed /api/version endpoint, so it’s reliable across every deploy target.