diff --git a/.dockerignore b/.dockerignore index 3bb30d8..8075c64 100644 --- a/.dockerignore +++ b/.dockerignore @@ -4,19 +4,29 @@ # this workspace once, and every byte of it is uploaded to the daemon before the first # instruction runs. The build produces its own artefacts inside the image; the ones lying here # are from another toolchain, another architecture, or both. -target/ -node_modules/ +# **Anchored at the context root unless they start with `**/`, and that is the whole point of this +# block.** Docker matches these against paths relative to the context, so a bare `node_modules/` +# excludes the one at the top and copies `apps/web/node_modules` — 683 MiB of it — straight into +# the image. Measured, not feared: the build context carried 683 files where the source tree has +# 457. +# +# `apps/web/dist` was the one that cost an afternoon. Copied in, Tailwind scanned the previous +# bundle for class names and emitted a different stylesheet, so the chunk hashes moved and the +# deployment stopped matching the manifest `.github/workflows/release.yml` publishes — the exact +# property `scripts/verify-web.sh` exists to check. A build whose output depends on whether +# somebody ran `pnpm run build` beforehand is not reproducible, and nothing said so. +**/target/ +**/node_modules/ .git/ .worktrees/ # Secrets. `.env` is the development one and would be baked into a layer. -.env -.env.local -deploy/.env +**/.env +**/.env.local # Build outputs that the image rebuilds itself. -dist/ -pkg/ -*.tsbuildinfo +**/dist/ +**/pkg/ +**/*.tsbuildinfo apps/desktop/gen/ release/artefacts/ diff --git a/deploy/Dockerfile.web b/deploy/Dockerfile.web index a6c398e..1665d85 100644 --- a/deploy/Dockerfile.web +++ b/deploy/Dockerfile.web @@ -39,7 +39,7 @@ FROM node:22.23.2-bookworm-slim AS build # The media server's origin, for calls. Empty by default, and it must stay that way for a # deployment that runs none: an empty value keeps its host out of `connect-src` instead of # widening the policy for a service that will never be contacted. -ARG VITE_MEDIA_URL= +ARG VITE_MEDIA_URL # The version is pinned rather than left to corepack's default, for the reason # `rust-toolchain.toml` gives at length about the compiler: "recent" is not a version. @@ -75,7 +75,24 @@ COPY apps/web ./ # toolchain and no `wasm-pack` are needed here. That artefact is the one `scripts/verify-wasm.sh` # checks against `crates/crypto-wasm` on every pull request; serving it is serving the bytes that # check covers. -RUN pnpm run build +# **`unset` when empty, and this is not a formality.** Vite substitutes `import.meta.env.VITE_*` +# statically, and it distinguishes a variable that is *absent* from one that is *defined and +# empty*. Absent, the bundle keeps `const e={}, x=!!e?.VITE_MEDIA_URL`. Defined-and-empty, Vite +# folds it to a constant and minification deletes the branch — different bytes, different chunk +# hashes, different `index.html`. +# +# `ARG VITE_MEDIA_URL=` put every deployment in the second case, so a deployment that runs **no** +# media server still produced a bundle that did not match the manifest published by +# `.github/workflows/release.yml`. That is backwards: `docs/THREAT-MODEL.md` § 4quinquies says the +# trade is "verifiable **or** calls", and a deployment configuring no calls was paying it anyway. +# +# Measured rather than reasoned, on node 22.23.2 and pnpm 11.22.0, `dist/index.html`: +# +# variable absent, as CI builds it 2a26c3f24aff3326 ← what the manifest lists +# ARG VITE_MEDIA_URL= (before) 9b4c1178cc297802 +# unset when empty (after) 2a26c3f24aff3326 +# unset when empty, calls configured a13e1dcae6b3cbae ← differs, and should +RUN if [ -z "${VITE_MEDIA_URL:-}" ]; then unset VITE_MEDIA_URL; fi; pnpm run build FROM caddy:2-alpine