Skip to content

Docker/self-host: backup volume name, atomic site-build, tag-after-gates in docker.yml, guide and compose fixes #166

Closed

Description

@Madraka

Findings from the adversarial review of the 0.6.3 Docker image, compose stack, self-host guide and docker.yml (#143, #148). The CLI/API side (setup token, Better Auth origins, --no-auth, rate-limit IP) is #165.

High

  1. The documented backup archives an empty volume. docs/guides/self-host.md:88 runs docker run --rm -v cloudhal-data:/data … tar czf …, but deploy/docker/compose.yml:46-47 declares volumes: cloudhal-data: without name:, so the stack's volume is <project>_cloudhal-data (cloudhal_cloudhal-data after the quick start's mkdir cloudhal). docker run -v auto-creates an empty cloudhal-data, tar archives nothing, exit 0; the restore lands in a volume the stack never mounts. This is the guide's only pre-upgrade rollback path. Fix: name: cloudhal-data on the volume (and say so), or make the backup command use docker compose run/$(docker compose ... ).

Medium

  1. site-build empties /data/dist before rendering (deploy/docker/bin/site-build:13): the Caddy-served site is a bare 404 for the length of each build, and the guide's 15-minute cron makes that recurring; a failed build leaves the site empty. Build into /data/dist.next, then swap atomically (rename), keep the previous output on failure.
  2. Release tags are pushed and attested before the smoke test and Trivy (.github/workflows/docker.yml:111): a failing gate leaves a broken latest/X.Y live. Also latest is skipped for a release started via Actions › Release › Run workflow, and a dispatch backfill of an older patch moves X.Y backwards (docker.yml:93-94). Fix: build and push by digest (or sha- tag) first, smoke + Trivy against that digest, then add the version tags with docker buildx imagetools create; compute latest/X.Y from the highest released version, not from the event.

Low (docs and compose riders)

  1. BETTER_AUTH_SECRET from .env never reaches the container: compose.yml:14 does not pass it, while the guide (self-host.md:62) tells operators to set it.
  2. Guide (self-host.md:21) and .env.example say invites and reset links are printed to the logs; the container prints nothing unless cloudhal.json has mail — document the mail setting or make the terminal sender the default when no mail transport is configured.
  3. compose.yml:38: :? interpolation in the inactive caddy profile fails docker compose up unless both Caddy variables are set; use :- with a comment, or move the check into the Caddy entrypoint.
  4. .env.example:1 says "No secrets live here" while asking for the Workers AI token; the guide says it is "never stored". Say what is a secret and point at Docker secrets / an env file outside the repo.
  5. .env.example:9: changing CLOUDHAL_PORT without CLOUDHAL_BASE_URL leaves Studio loading but sign-up/sign-in failing with 403 (Better Auth origin). Couple the two in the comments, and after Self-host: auth hardening after the 0.6.3 review (setup token, Better Auth origins, --no-auth guard, rate-limit IP) #165 lands, say CLOUDHAL_BASE_URL is required for any non-localhost origin.
  6. self-host.md:68: nothing says how the static build learns the public site origin, so canonical links, sitemap and feeds say localhost. Document CLOUDHAL_SITE_URL/the site's baseUrl (whichever the build reads) and pass it in site-build.
  7. Dockerfile:6: the build example names 0.6.2, whose CLI rejects the entrypoint's --init-if-empty; use 0.6.3/latest or a placeholder.
  8. Critic gaps worth a line each: site-build before the first publish builds the latest or a live snapshot (drafts) — until RFC-0043 PR 2's --published mode, the guide must not claim "published only"; the Caddyfile's handle_errors → /404.html is dead for the default starter (no /404 page) and _headers is ignored by Caddy (add header /media/* Cache-Control …); the guide's "previews are visible" claim is wrong on Node (no ApiDeps.preview).
  9. Smoke coverage (scripts/docker-smoke.ts): add GET / → Studio index.html, POST /mcp → 401, and an arm64 run under QEMU before tags move.

Done when

  • Items 1–3 fixed with the workflow verified by a dispatch run against an existing version (re-tag only, no new release).
  • Guide, .env.example, compose and Dockerfile updated for 4–11; smoke test extended for 12.
  • pnpm check green (docker-smoke needs Docker; run it locally once).

Source: adversarial review run after the 0.6.3 release (178 agents, 47 confirmed findings).

Activity

  1. Madraka commented on Nov 8, 2026

    @Madraka
    ContributorAuthor

    Wording for docs/guides/self-host.md, .env.example and the compose file now that the #165 fix is in #169. That PR does not touch these files; they belong to this issue.

    Quick start, replacing "the first account that signs up is the Owner, after which sign-up closes":

    The first start prints a one-time setup link to the container's logs (docker compose logs cloudhal), http://localhost:8787/login#setup=<token>. Open it to create the Owner account. Without it sign-up is refused, so nobody who reaches the port first can claim the installation. After that sign-up closes and everyone else joins by invite. To choose the token yourself (for example from a secrets manager), set CLOUDHAL_SETUP_TOKEN to a random value of 32 characters or more, using only letters, digits and - . _ ~ (openssl rand -hex 32). The log then shows the link with <CLOUDHAL_SETUP_TOKEN> in place of the value.

    Beyond localhost, the CLOUDHAL_BASE_URL row:

    The origin browsers open Studio at, e.g. https://studio.example.com. Required behind a reverse proxy, and recommended whenever Studio is reached from another machine. It is Better Auth's base URL (its cookies become Secure for https), the only origin sign-in accepts besides CLOUDHAL_TRUSTED_ORIGINS, the origin of invite and password-reset links, and the URL the log prints. Without it, sign-in accepts whatever address the browser used, and the log warns. Origin only, no path.

    Beyond localhost, the CLOUDHAL_BEHIND_PROXY row: add "and the client's address for rate limits from the last X-Forwarded-For hop".

    Environment table: add a row for CLOUDHAL_SETUP_TOKEN. Default empty: a token is generated at each start while there is no account, and printed to the logs.

    compose.yml: the cloudhal service lists its variables one by one, so a CLOUDHAL_SETUP_TOKEN in .env never reaches the container. Add CLOUDHAL_SETUP_TOKEN: ${CLOUDHAL_SETUP_TOKEN:-} to its environment:; an empty value means a token is generated. Add a commented # CLOUDHAL_SETUP_TOKEN= to .env.example with the rule above. Also adjust its "No secrets live here" header, since this variable is a secret.

    .env.example (item 8): changing CLOUDHAL_PORT or CLOUDHAL_BIND means CLOUDHAL_BASE_URL must name the new origin. With the compose default http://localhost:8787, sign-in from http://<lan-ip>:8787 fails with 403, because a configured base URL is the only origin accepted. Either set it to the LAN origin or leave it empty.

    Limits / --no-auth: cloudhal studio --no-auth is now refused unless --host is loopback and --behind-proxy is off, so it cannot be used in the container, which binds 0.0.0.0.

    scripts/docker-smoke.ts: the #165 PR already passes CLOUDHAL_SETUP_TOKEN to the container and checks that sign-up without it is refused. The PR body lists exactly what changed; item 12's additions go on top of that.

  2. added a commit that references this issue on Nov 8, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions