diff --git a/docs/deploy.md b/docs/deploy.md index 0894916..9180d84 100644 --- a/docs/deploy.md +++ b/docs/deploy.md @@ -61,14 +61,17 @@ set to `https://crate.mokkenstorm.dev/api/discogs/callback`. ## Build, push, deploy -A push to `main` runs [`.tangled/workflows/deploy.yaml`](../.tangled/workflows/deploy.yaml) -on Tangled's hosted spindle (`spindle.tangled.sh`), which builds the image and -SSHes it onto the droplet: no manual scp/ssh round-trip anymore. Build -**natively, still**: an emulated amd64 build of the Erlang toolchain crashes -under QEMU (OTP #10355), so the workflow uses spindle's `nixery` engine, which -runs steps as plain Docker containers on the runner's own arch (no emulation -layer at all, unlike the `microvm` engine's cross-arch case) rather than -cross-building. +Deploys are manual: run `deploy/publish.sh` from the repo root. It archives +`HEAD`, scp's it to the droplet, builds the image **on the droplet** (which has +a real Docker daemon), then flips `AT_RECORD_TAG` and `docker compose up -d`. +Building on the droplet sidesteps both the QEMU/OTP#10355 crash from +cross-building amd64 on an arm Mac and the fact that no CI runner available can +build the image at all (see below). + +```sh +deploy/publish.sh +curl -sf https://crate.mokkenstorm.dev/healthz +``` The build bundles the Lustre frontend with **Bun**. To avoid a mid-build download from GitHub (why early spindle runs failed), the build stage copies a @@ -76,32 +79,20 @@ pinned Bun from `oven/bun` and `web/gleam.toml` sets `tools.lustre.bin.bun = "system"`; keep the Dockerfile's `oven/bun` tag matching `lustre_dev_tools`' pin. No Tailwind entry exists, so Tailwind is never fetched. -One-time setup (already done for `crate.mokkenstorm.dev` as of 2026-07-12): - -1. Attach this repo to the hosted spindle from its Tangled settings page - (`/settings/spindles` -> verify). -2. On the droplet, `/opt/at-record/deploy.sh` runs the actual redeploy - (`docker compose pull app && docker compose up -d app`), and the - droplet's `.env` pins `AT_RECORD_TAG=latest` so that pull always picks up - the newest pushed image. -3. A dedicated ed25519 deploy key is pinned in root's `authorized_keys` with - a forced command so it can ONLY ever run `deploy.sh`, regardless of what - command the client sends (`no-pty`, `no-port-forwarding`, etc. also set): - ``` - command="/opt/at-record/deploy.sh",no-port-forwarding,no-X11-forwarding,no-agent-forwarding,no-pty,no-user-rc ssh-ed25519 AAAA... at-record-ci-deploy - ``` - A leaked `DEPLOY_SSH_KEY` secret can redeploy the current `:latest` image - and nothing else. -4. Under the repo's **Settings -> Secrets**, set: - - `GHCR_TOKEN`: a GitHub PAT with `write:packages` scope, for - `docker login ghcr.io`. - - `DEPLOY_SSH_KEY`: the private half of the deploy key above. - - `DEPLOY_HOST`: `root@`. - -Redeploys happen automatically on every push to `main`. First run is also the -test: watch the pipeline in the repo's CI view, and confirm -`docker compose ps` on the droplet shows the new tag if you want to -double-check. +### Autopublish: dropped (2026-07-13) + +A Tangled-spindle autopublish (`.tangled/workflows/deploy.yaml`) was tried and +removed: the `nixery` runner cannot build a container image. `docker build` has +no daemon; `buildah` cannot create the user namespaces it needs; `kaniko` cannot +set the `security.capability` xattrs required to unpack the Debian base image. +All three come down to the runner's lack of privileges. + +Two ways to revisit (the deleted workflow is in git history): a privileged or +native-amd64 Tangled runner, or **Alpine base images**. Alpine ships no +`security.capability` xattrs, so kaniko's unpack would not trip on them; the cost +is moving the production image from Debian/glibc to Alpine/musl (`apt-get` -> +`apk`, and the `gleam`/`erlang`/`oven/bun` tags to their `-alpine` variants), +which needs its own verification. Until then `publish.sh` is the path. ## Smoke test (also the first confidential-client test)