Repository navigation
fix(docker): backup volume, atomic site-build, tag after gates, guide fixes (#166) - #170
Merged
Merged
Conversation
… fixes (#166) - compose.yml names the volume `cloudhal-data` (CLOUDHAL_VOLUME keeps a 0.6.3 stack's `<project>_cloudhal-data`); the guide's backup, restore and upgrade steps match it for compose and plain `docker run`. - site-build renders into /data/dist, then publishes the tree under /data/site/<id> and swaps /data/site/current with one rename; a failed build changes nothing, the previous build is kept, one build at a time. CLOUDHAL_SITE_URL becomes the site's domain (cloudhal.json domains.site) so canonical links, the sitemap and feeds use it. - docker.yml pushes `sha-<short>` only, runs the smoke test (amd64 and a native ubuntu-24.04-arm job) and Trivy on that digest, then a `tag` job adds X.Y.Z, X.Y and latest with `imagetools create`, computed from the highest v* tag, and attests the final digest. - compose passes BETTER_AUTH_SECRET, CLOUDHAL_AI_TOKEN and CLOUDFLARE_ACCOUNT_ID only when set; the caddy profile checks its origins when Caddy starts instead of `:?`; the Caddyfile serves site/current and marks existing /media/*, /_astro/*, /_cloudhal/* files immutable. - Guide, .env.example, Dockerfile: invites and resets are links (the `mail` setting documented), secrets and COMPOSE_ENV_FILES, CLOUDHAL_PORT with CLOUDHAL_BASE_URL, the site's origin, latest-snapshot builds before the first publish, the 404 page, no preview links on Node, 0.6.3 example. - docker-smoke.ts checks GET / (Studio), POST /mcp without credentials (401) and two site-build runs. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Signed-off-by: Madraka <8898640+Madraka@users.noreply.github.com>
… a proxy (#166) Wording for the #165 hardening (PR #169): - Quick start: the first owner signs up through the one-time setup link printed to the logs; CLOUDHAL_SETUP_TOKEN to choose it. compose.yml and .env.example pass and document CLOUDHAL_SETUP_TOKEN. - CLOUDHAL_BASE_URL is required with CLOUDHAL_BEHIND_PROXY=1; compose now keeps an explicitly empty value (`${VAR-default}`), so a LAN install can accept the browser's own address instead of a localhost-only 403. - Own reverse proxy: overwrite or append X-Forwarded-For (its last hop is the rate-limit address), with an nginx example; `--no-auth` cannot run in the container. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Signed-off-by: Madraka <8898640+Madraka@users.noreply.github.com>
#166) `cloudhal studio` now refuses --behind-proxy without --base-url (#169), but compose's `${CLOUDHAL_BASE_URL:-http://localhost:8787}` default hid that: a proxied stack that forgot the base URL silently got localhost and failed every sign-in with 403. - compose passes CLOUDHAL_BASE_URL with no default and .env.example ships it empty: unset, sign-in accepts the browser's own address and the log warns; behind a proxy the CLI refuses to start. - The entrypoint refuses CLOUDHAL_BEHIND_PROXY=1 with a localhost base URL (a .env kept from 0.6.3), for `studio` only. - The guide's base-URL and proxy rows say it is required behind a proxy. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Signed-off-by: Madraka <8898640+Madraka@users.noreply.github.com>
…s, upgrade steps (#166) Review of #170 (project manager): - site-build no longer moves /data/dist away. `cloudhal build` has no output option and empties <project>/dist, so the build runs in a staging copy of the project (/data/.site-build: files copied, node_modules and the data folder linked, so the same store is read). The finished build goes to /data/site/<id>; `current`, `previous` and /data/dist (a symlink to site/current) are each replaced with `ln -sfn` + `mv -T`. A 0.6.3 real dist moves aside once. An old Caddyfile, an nginx root of /data/dist and the 0.6.3 image with the new Caddyfile keep working. - The Caddyfile's root is /srv/cloudhal/dist again; hashed assets the current build lacks come from site/previous. - No 0.6.3 re-tag: the smoke test needs 0.6.4's setup token. Guide, CONTRIBUTING, Dockerfile and README say 0.6.4 and later. - COMPOSE_ENV_FILES with full paths (relative ones follow the current directory); the cron line runs `cd … && docker compose exec`, which needs no env files. - The caddy-profile quick start and the upgrade download the Caddyfile; the upgrade checks the pulled image with `docker run --entrypoint grep` (no volume mounted) after `docker compose pull`. - docker-smoke.ts reads the site through /data/dist and checks the current/previous links. Verified locally: four site-build runs on a scaffolded project (a 0.6.3 real dist first, two swaps, a failing build that changed nothing) and the Caddyfile served from that tree with Caddy 2.11. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Signed-off-by: Madraka <8898640+Madraka@users.noreply.github.com>
…ade order (#166) Last review round of #170 (project manager): - site-build: a real /data/dist (0.6.3) moves to site/legacy-0.6.3 and becomes site/previous, so the prune keeps it for one build. A mount point at /data/dist is refused before anything is written (mountpoint -q, else stat -c %d). Stale `dist.*`, `site/current.*` and `site/previous.*` links from an interrupted run are removed first. - Caddyfile: assets served from site/previous are immutable too. docker.yml validates the Caddyfile in caddy:2.11 before building. - docker-smoke.ts: a real /data/dist with a marker before the first build; afterwards /data/dist resolves under /data/site/ and site/previous holds the marker. - Guide and README: the one moment without /data/dist on the first build after 0.6.3; the setup link needs 0.6.4 (until then keep Studio on 127.0.0.1 and create the Owner first); upgrade steps in order (fix the base and site URLs before starting, download .env.example to compare, check the image named by `docker compose config --images`, what a 0 means, the fallback needs the new Caddyfile, `rmdir Caddyfile`); no Astro build (`cloudhal build`, `cloudhal deploy`) inside /data. Verified locally: the migration run (legacy folder kept as previous with its marker, a stale link removed), the next run (legacy pruned), the mount-point refusal (nothing written), and Caddy 2.11.9 serving a previous-only asset with the immutable header. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Signed-off-by: Madraka <8898640+Madraka@users.noreply.github.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Fixes #166. Every item of the issue is in this PR, rebased on #169 (#165), whose setup-token flow it keeps. Nothing under
packages/changes, and no changeset is needed:deploy/docker, docs andscripts/are not published packages.Nothing here was built or run as an image locally, by agreement (the build machine freezes under load). The PR's own Docker workflow builds the amd64 image and runs the smoke test and Trivy on it. The full workflow (digest-first push, both smoke tests, the
tagjob) is verified by the 0.6.4 release run right after this merges. There is no 0.6.3 re-tag: the smoke test asserts #169's setup-token refusal, which only 0.6.4 has, so a 0.6.3 image can never pass the gate. Until 0.6.4 is out,latestis the old 0.6.3 image, andmain's compose file and Caddyfile work with it: the served path is/data/distin both layouts.Items and how each was verified
1. Backup volume (high). The compose volume is now
name: ${CLOUDHAL_VOLUME:-cloudhal-data}, so the stack,docker run -v cloudhal-data:/dataand the guide's backup commands share one volume.<project>_cloudhal-data. The guide,.env.exampleand compose tell operators to find it withdocker volume lsand setCLOUDHAL_VOLUMEto it, and that a second stack on the same host needs a name of its own.tar tzf.data.db-walbeside the olddata.db.docker compose configwith no.env, with.env.example, withCLOUDHAL_VOLUMEset, and with the caddy profile.2. Atomic
site-build, with/data/distkept as the served path.cloudhal buildhas no output option: it always writes<project>/distand empties it first (packages/cli/src/commands/astro.ts). Sosite-buildnever builds in/data. It copies the project's own files to/data/.site-build, linksnode_modules(its real target) and the data folder (cloudhal.jsondataDir) so the build reads the same store, and builds there./data/site/<build id>. Thensite/current,site/previousand/data/dist(a symlink tosite/current) are each replaced withln -sfn+mv -T, which is atomic. A failed build changes nothing, and the stage is removed on exit.flockallows one build at a time.distfolder moves tosite/legacy-0.6.3on the first build and becomessite/previous, so the prune keeps it, and its pages' assets, for one more build. That run has one moment without/data/dist, between two renames; bookworm's coreutils has nomv --exchange, and the docs say so. A mount point at/data/dist(a host bind mount) is refused before anything is written, usingmountpoint -q, else astat -c %dcomparison. Staledist.*,site/current.*andsite/previous.*links from an interrupted run are removed first. An old Caddyfile (root /srv/cloudhal/dist), an nginx root of/data/dist, and the 0.6.3 image withmain's Caddyfile all keep working.site/previous; older builds are removed.node_modulesstanding in for the image's and GNUmv/flockfrom Homebrew.distand replaced it with the symlink.currentandpreviousand kept exactly two builds.astro.config.mjsand leftcurrent,previousanddistunchanged.*.cloudhal(for archives) matched the data folder.cloudhalin the stage rules, and a chain of pnpm-style relative links broke Tailwind's resolution.previouswith its marker, and a staledist.123link was removed), the next build (the legacy folder was pruned), and the mount-point refusal with a stubmountpoint(nothing written, no stage left).bash -nand shellcheck. The smoke test creates a real/data/distwith a marker before the first build. It then checks that/data/distresolves under/data/site/, thatsite/previousholds the marker, the second swap, and the kept builds.3. Tag after gates (
docker.yml).buildpushes linux/amd64 + linux/arm64 with SBOM and provenance undersha-<short>only, then runs the amd64 smoke test and Trivy against$IMAGE@digest.smoke-arm64runs the smoke test on the nativeubuntu-24.04-armrunner. QEMU was not used: a Node start plus an Astro build under emulation would take well over the ~10 minutes the brief allows, while the native runner is free for public repositories.tagruns only after both. It adds the tags withdocker buildx imagetools create --tag …from the single tested digest. One source and no annotations means buildx copies the index as is, so the digest and its attestation manifests stay the same; the step reads the digest back and warns if it ever differs. It then writesattest-build-provenancefor that final digest.X.Y.Zalways;X.Ywhen the version is the highest patch of that minor among the repository'sv*tags plus itself;latestwhen it is the highest release of all. A release finished by hand still getslatest, and a backfill of an older patch moves nothing.edgebuilds getedgeonly. Pull requests build amd64, smoke-test and scan, with no push, arm64 or tag.4.
BETTER_AUTH_SECRETreaches the container as a bare name, so it is passed only when set, as areCLOUDHAL_AI_TOKENandCLOUDFLARE_ACCOUNT_ID. The entrypoint treats an empty value as unset, so the secret on the volume stays in use. Verified:docker compose configshows the names absent when unset and present when set.5. Mail claim. Invites and resets are links made on Studio's Team page. The guide's limits now document
cloudhal.jsonmail: with it, the container prints each mail to its logs. Verified againststudio.ts,mail-local.tsandroutes/invites.ts.6. No
:?in the caddy profile. Caddy'scommandchecks both origins when it starts and exits with a message.docker compose upwithout the profile no longer needs them. Verified:docker compose configwith and without the profile, and with empty Caddy variables.7. Secrets wording.
.env.exampleand the guide name the secrets (BETTER_AUTH_SECRET,CLOUDHAL_AI_TOKEN,CLOUDHAL_SETUP_TOKEN), saychmod 600and keep the file out of git. To keep them out of a checkout: a second env file named inCOMPOSE_ENV_FILESfor every compose command. A one-off--env-fileis not enough, because the nextupwould drop the secrets.8.
CLOUDHAL_PORT/CLOUDHAL_BASE_URL.CLOUDHAL_BASE_URLmust be exactly the origin browsers open Studio at, and is required behind a proxy.${CLOUDHAL_BASE_URL:-http://localhost:8787}default hid the CLI's refusal of--behind-proxywithout a base URL: a proxied stack silently got localhost and a 403 on every sign-in. Compose now passes it with no default, and.env.exampleships it empty, so unset means the CLI's own behaviour (sign-in from the browser's address, with a warning).CLOUDHAL_BEHIND_PROXY=1with a localhost base URL, as kept in a.envfrom 0.6.3. Verified: compose config, and the entrypoint run with a stubcloudhalin five cases.9. Site origin for the static build.
cloudhal.jsondomains.site,cloudhal domain set).site-buildsets it fromCLOUDHAL_SITE_URL, which compose now passes to the cloudhal service.localhost, an IP address or anexample.*placeholder is skipped with a message, and the build goes on.10. Dockerfile example is 0.6.4, the first version the gates accept; the guide and README say so too.
11. Critic gaps.
cloudhal build --publishedis RFC-0043 PR 2./media/*,/_astro/*and/_cloudhal/*immutable, but only for files that exist, so a miss is never cached for a year./404.htmlexists only when the site has a/404page; the guide and Caddyfile note this._headersreally contains./srv/cloudhal/distagain. A hashed asset (/_astro/*,/_cloudhal/*,/media/*) that the current build lacks is served fromsite/previous, with the same immutableCache-Control, so a page opened before a swap still loads.docker.ymlvalidates the Caddyfile incaddy:2.11before building.caddy validateandcaddy fmtwith Caddy 2.11.9 from Homebrew, because the Docker daemon is stopped on this machine. That Caddy served the four-run build tree:/returned 200, a current asset 200 with the immutable header, an asset only inprevious200, a missing asset 404 without a cache header, and/.cloudhal/auth-secret,/../.cloudhal/data.dband/site/previous/…404.12. Smoke test. It keeps #169's flow: the script makes its own token, passes
-e CLOUDHAL_SETUP_TOKEN, expects 403 without thex-cloudhal-setup-tokenheader and 200 with it. After the sign-up it checksGET /(200,text/html,<title>Cloudhal Studio</title>) and an anonymousPOST /mcp(401). It also runssite-buildtwice, and--platformselects the architecture. Verified: Biome andnode --check. The script runs in CI only.From #169's review, in the guide:
docker compose logs cloudhal, orCLOUDHAL_SETUP_TOKEN: 32+ characters of letters, digits and- . _ ~.CLOUDHAL_SETUP_TOKENis in the environment table, compose and.env.example.X-Forwarded-For, whose last hop is the rate-limit address, with an nginx example; Caddy does this by default.--no-authis listed as unusable in the container.Also changed:
CONTRIBUTING.md's Docker paragraph (the tag rule) anddeploy/docker/README.md.Review
Four read-only adversarial reviewers covered the workflow,
site-build, compose/Caddy and the docs/smoke test. Their nine confirmed findings are fixed in the first commit: the Caddy miss caching, the one-off--env-file, a second stack on one volume,site-buildstopping on alocalhostorigin, deleting the previous build too early, the restore order, the 0.6.3 upgrade withoutpull, and the_headerswording. The completeness critic did not finish.Checks
pnpm check: green (exit 0) on the final commit. Biome (2792 files), size check, font fallbacks, MCP tools doc, demo archive and typecheck (20/20) passed.deploy/,docs/,scripts/,.github/), so turbo replayed passing results for identical inputs instead of running them again. An earlier full run of this diff, before the rebase, stopped on two MiniflareECONNRESETerrors in@cloudhal/analytics, a package not touched here; it was a load flake.docker compose config(no.env,.env.example, the caddy profile with variables set and empty,CLOUDHAL_VOLUMEset);bash -nand shellcheck onbin/site-buildandbin/entrypoint; actionlint 1.7.12 ondocker.yml;caddy validateandcaddy fmt; the localsite-buildand Caddy runs above.scripts/docker-smoke.ts. The PR's Docker workflow and the 0.6.4 release run cover them.Upgrading from 0.6.3 (in the guide)
CLOUDHAL_VOLUME=<project>_cloudhal-data..env'sCLOUDHAL_BASE_URL(behind a proxy the new entrypoint refuses a localhost one) and its placeholderCLOUDHAL_SITE_URLbefore anything starts.compose.yml, theCaddyfile(rmdir Caddyfileif Docker made a folder; the previous-build fallback needs the new file) and.env.exampleto compare, never over.env.docker compose pull, then check the image named bydocker compose config --imageswithdocker run --rm --entrypoint grep … -c site/current /opt/cloudhal/bin/site-build. That check never touches the named volume. A 0 means the image is still 0.6.3: the new files work with it, and the layout changes with the first 0.6.4 build.The site stays at
/data/distthroughout. The quick start notes that the setup link needs 0.6.4: until then, keep Studio on 127.0.0.1 and create the Owner first.Review of #170 (second round)
The project manager's review (24 agents, each finding checked twice) upheld eight findings; all are fixed in
66d19724:/data/diststays the served path (item 2).COMPOSE_ENV_FILEStakes full paths (relative ones resolve against the current directory). The cron line iscd /path/to/cloudhal && docker compose exec -T cloudhal site-build, which needs no env files.Caddyfile.pull, without a volume.Review of #170 (third round)
Fixed in
31251ae6:site/legacy-0.6.3=previous. There is a mount-point preflight and stale-link cleanup, and the docs say "except for a moment on the first build after upgrading from 0.6.3".previousassets are immutable, and the Caddyfile is validated in CI.config --imagescheck, what a 0 means, the.env.exampledownload, thermdir Caddyfilehint, and the widened build warning.🤖 Generated with Claude Code