# Deploy hu13 with Docker

## Build the image

From the repo root (build context is sources only — see `.dockerignore`):

```sh
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

```sh
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`):

1. `hu13 build /data/repo -o /srv/www` (+ `--branch` / `--clone-url` /
   `--public-origin` only when `HU13_BRANCH` / `HU13_CLONE_URL` /
   `HU13_PUBLIC_ORIGIN` are set). Any failure — bad branch, unreadable
   repo — aborts LOUD before serving starts (`set -eu`; serve never runs
   on a stale/empty site). The build emits `repo.git` (a dumb-HTTP bare
   repo: `git clone <origin>/repo.git`, no smart server needed),
   `repo.bundle` and `repo.tar.gz`.
2. `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`, `--quota` only when the matching
   `HU13_*` env var is set; otherwise the serve defaults apply).
   `exec` keeps `serve` on 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 derived `repo.git` directory never looks tracked.
- Static hosting (Cloudflare Pages/nginx): publish the whole `dist/`
  directory. `dist/repo.git` is plain files + `update-server-info`, and
  `dist/_headers` carries the `/raw/**` and artifact
  `/releases/<version>/**` download policy for Pages. On nginx, add the
  equivalent `add_header Content-Disposition attachment` for `/raw/` and
  release artifact directories — committed HTML/SVG must never render.
- Pages clean-URL redirects: Pages answers `.html` paths 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. `_redirects` status 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 in `functions/inbox/[kind].js` + R2 binding `INBOX` and
  secret `INBOX_SALT` implement the same contract. See
  `docs/cloudflare.md` for the bucket, binding, secret and WAF rate-rule
  recipe. `dist/_routes.json` limits 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-order `api/*.json` emitter instead
  of `serde_json`, enable `strip = true` + `opt-level = "z"` + LTO in the
  release profile. Do not gold-plate before then.
- `HU13_WWW` / `HU13_INBOX_DIR` also work for bare-metal
  `hu13 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` (or `docker exec` into the volume). Never served over HTTP.
- Mount a plain checkout, not a git worktree: a worktree's `.git` is a
  pointer file (`gitdir: ...`) whose target lives outside the mount, so
  git inside the container cannot resolve anything. `git clone` the
  branch (or copy the repo) and mount that. (Host/container uid mismatch
  on the mount is already handled via `safe.directory` in entrypoint.sh.)
