Skip to content

Self-host: official Docker image on ghcr.io (versioned, multi-arch, released with npm) #143

Closed

Description

@Madraka

Goal

Cloudhal is "self-hostable" (README), but the only documented self-host path is npm create cloudhal + cloudhal studio on a developer machine. There is no container image, no Dockerfile, no compose file and no self-host guide. Publish an official, versioned image on the GitHub Container Registry so a server owner runs Cloudhal (Studio + API + MCP + the site) with one docker compose up.

Deliverables

1. Image ghcr.io/cloudhal-dev/cloudhal

  • Built from the npm release, not from the repository tree: npm install -g @cloudhal/cli@X.Y.Z (+ create-cloudhal for first-run scaffolding) in a multi-stage build on node:24-bookworm-slim, non-root user, tini/--init, HEALTHCHECK on /api/instance (or an equivalent public probe). The image is exactly what npm users get, so the npm smoke test already covers it.
  • Multi-arch: linux/amd64 + linux/arm64 (better-sqlite3 prebuilds; verify on both, no in-image compilation).
  • Tags: X.Y.Z, X.Y, latest (from release tags), edge (main, nightly), sha-<short>; OCI labels (org.opencontainers.image.*), SBOM + provenance attestations (docker/build-push-action sbom: true, provenance: mode=max), Trivy scan gate, cosign keyless signing (optional, phase 2).
  • Variant decision for render_page (needs Chrome): default image without Chrome; -chrome variant later if there is demand.

2. Runtime changes in @cloudhal/cli / @cloudhal/api (the parts a container cannot do without):

  • cloudhal studio --host 0.0.0.0 / CLOUDHAL_HOST (today serveNode binds 127.0.0.1).
  • --base-url https://studio.example.com / CLOUDHAL_BASE_URL: Better Auth baseURL and trusted origins are hard-coded to http://localhost:<port> in packages/cli/src/commands/studio.ts; behind a reverse proxy sign-in and cookies break. Trust X-Forwarded-* only when told (--behind-proxy).
  • Data on one volume: /data holds the project (cloudhal.json, dataDir SQLite + blobs, media). First run with an empty volume scaffolds a project (cloudhal init equivalent, starter chosen by env) instead of failing.
  • Serving the public site: cloudhal dev is Astro's dev server (drafts visible) and cloudhal build is static output. Decide and document the self-host site path: (a) cloudhal build on publish → static files served by the same container (or Caddy/nginx sidecar), or (b) a Node adapter for starters/blog (@astrojs/node) run as a second process. (a) is the smaller, honest first step; (b) is an RFC. The site must redeploy on publish (hook on the scheduler/publish path).
  • Mail: cfg.mail prints invites to stdout; document SMTP as a follow-up.
  • CLOUDHAL_AUTOMATIONS, CLOUDHAL_TRUSTED_ORIGINS, CLOUDHAL_AI_TOKEN, CLOUDFLARE_ACCOUNT_ID documented as container env.

3. Compose + docs

  • deploy/docker/compose.yml (studio + optional Caddy with automatic TLS), .env.example, deploy/docker/README.md and docs/guides/self-host.md (requirements, upgrade = pull new tag, backup = the volume or cloudhal export, restore, reverse proxy, health).
  • Upgrade path: cloudhal migrations run on start (they do today on openLocalStore); state the rule "latest follows patch releases; pin X.Y in production".

4. CI/CD .github/workflows/docker.yml

  • Triggered by release.yml after a successful npm publish (workflow_call with the version, like template-sync.yml), by workflow_dispatch with a version (re-publish / backfill), and nightly from main for edge (built from the repo, tagged edge only, never latest).
  • permissions: packages: write, id-token: write, attestations: write; docker/login-action to ghcr with GITHUB_TOKEN; docker/metadata-action for tags/labels; docker/setup-qemu-action + buildx; cache type=gha.
  • Smoke after build: run the image, curl the health endpoint, sign up the first owner through the API, create a site, publish, fetch the site (the same loop npm-smoke.yml runs, inside the container).
  • Dependabot: add the docker ecosystem for the base image and github-actions already covers the actions.
  • Package visibility public on ghcr; link the package to the repository (org.opencontainers.image.source).

5. Versioning rule

One version everywhere: the changesets fixed group → vX.Y.Z tag → npm → ghcr X.Y.Z. The image never carries its own version; a rebuild of the same version (base-image security fix) is X.Y.Z-r2 / re-tag, documented in CONTRIBUTING.md "Releasing" step 5.

Order

  1. Runtime changes (--host, --base-url, /data first run, site serving decision) with tests on the Node server.
  2. Dockerfile + compose + local build on amd64/arm64; manual smoke.
  3. docker.yml + release hook + Dependabot docker; first publish of the current release by workflow_dispatch.
  4. docs/guides/self-host.md, README "Self-host" section, CONTRIBUTING "Releasing" step.

Follow-ups (separate issues): SMTP mail, Chrome variant for render_page, Node adapter for live site serving (RFC), Helm chart / Kubernetes manifests if asked for.

Activity

  1. Madraka commented on Nov 10, 2026

    @Madraka
    ContributorAuthor

    Done. The official image ghcr.io/cloudhal-dev/cloudhal ships with every release since 0.6.3. It is multi-arch (amd64/arm64), carries an SBOM, provenance and attestations, and passes a smoke test and a Trivy scan before the version tags move (#148, #170). The current release is 0.7.0 (tags 0.7.0, 0.7, latest). The guide is docs/guides/self-host.md. The remaining follow-up, rebuilding the static site on publish, is tracked in #150.

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

    enhancementNew feature or request

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions