Deploy hu13 with Docker
deploy · wiki index
Deploy hu13 with Docker
Build the image
From the repo root (build context is sources only — see .dockerignore):
docker build -t hu13 .
Multi-stage: rust:1.98-bookworm compiles with
cargo build --locked --release (only Cargo.toml, Cargo.lock, src/,
static/ are copied, so edits elsewhere don't bust the cache);
debian:bookworm-slim runtime adds ca-certificates, a nonroot user
hu13 (uid 10000), the binary, and entrypoint.sh. No toolchain, no
sources, no .git in the final image.
Run it
docker run --rm -p 39313:39313 \
-v "$PWD:/data/repo:ro" \
-v hu13-inbox:/data/inbox \
-e HU13_BRANCH=main \
-e HU13_PUBLIC_ORIGIN=http://localhost:39313 \
-e HU13_CLONE_URL=https://example.com/mirror.git \
hu13
Then open http://localhost:39313/.
What happens inside (entrypoint.sh):
hu13 build /data/repo -o /srv/www(+--branch/--clone-url/--public-originonly whenHU13_BRANCH/HU13_CLONE_URL/HU13_PUBLIC_ORIGINare set). Any failure — bad branch, unreadable repo — aborts LOUD before serving starts (set -eu; serve never runs on a stale/empty site). The build emitsrepo.git(a dumb-HTTP bare repo:git clone <origin>/repo.git, no smart server needed),repo.bundleandrepo.tar.gz.exec hu13 serve --www /srv/www --inbox-dir /data/inbox --listen 0.0.0.0:39313(+--max-title,--max-body,--max-bytes,--rate-per-hour,--quotaonly when the matchingHU13_*env var is set; otherwise the serve defaults apply).execkeepsserveon PID 1.
Volumes / env reference
| Mount / var | Default | Notes |
|---|---|---|
/data/repo (ro) | — | repo checkout to render (required) |
/data/inbox | — | inbox storage; MUST sit outside --www (serve refuses otherwise) |
HU13_BRANCH | main, else master | branch to build; wrong name fails the container |
HU13_PUBLIC_ORIGIN | unset | absolute base URL; enables git clone <origin>/repo.git + absolute bundle/tarball links |
HU13_CLONE_URL | git origin remote | fallback/mirror label shown as "mirror source" |
HU13_LISTEN | 0.0.0.0:39313 | serve address |
HU13_MAX_TITLE | 300 | inbox title cap (chars) |
HU13_MAX_BODY | 20000 | inbox body/prompt cap (chars) |
HU13_MAX_BYTES | 32768 | whole-POST cap (bytes) |
HU13_RATE_PER_HOUR | 2 | per-IP POSTs/hour |
HU13_QUOTA | 500 | global inbox file quota |
Notes
-
dist/in the repo is gitignored; build outside the checkout (or to/srv/www) so the derivedrepo.gitdirectory never looks tracked. -
Static hosting (Cloudflare Pages/nginx): publish the whole
dist/directory.dist/repo.gitis plain files +update-server-info, anddist/_headerscarries the/raw/**and artifact/releases/<version>/**download policy for Pages. On nginx, add the equivalentadd_header Content-Disposition attachmentfor/raw/and release artifact directories — committed HTML/SVG must never render. -
Pages clean-URL redirects: Pages answers
.htmlpaths with a 308 (/issues/index.html→/issues/,/files/a.md.html→/files/a.md); the links still resolve, and Docker/nginx serve the exact path._redirectsstatus 200 cannot override this. If exact URL shape matters, use a proxy/Worker or accept the Pages form. -
Cloudflare Pages inbox: the static
dist/alone has no inbox. The Pages Function infunctions/inbox/[kind].js+ R2 bindingINBOXand secretINBOX_SALTimplement the same contract. Seedocs/cloudflare.mdfor the bucket, binding, secret and WAF rate-rule recipe.dist/_routes.jsonlimits Functions to/inbox/*. -
Binary size: release build is ~2.1MB unstripped, ~1.7MB stripped. If it ever tops 2MB stripped, the cut list is: replace
yaml_serde(libyaml) with the hand-rolled frontmatter split already used for parsing shapes, hand-roll the fixed-orderapi/*.jsonemitter instead ofserde_json, enablestrip = true+opt-level = "z"+ LTO in the release profile. Do not gold-plate before then. -
HU13_WWW/HU13_INBOX_DIRalso work for bare-metalhu13 serve(flags win over env); the container always passes explicit--www/--inbox-dir. -
Inbox files are read locally only:
hu13 inbox list|show --inbox-dir /data/inbox(ordocker execinto the volume). Never served over HTTP. -
Mount a plain checkout, not a git worktree: a worktree's
.gitis a pointer file (gitdir: ...) whose target lives outside the mount, so git inside the container cannot resolve anything.git clonethe branch (or copy the repo) and mount that. (Host/container uid mismatch on the mount is already handled viasafe.directoryin entrypoint.sh.)