Reference

Deploy

One reusable workflow deploys every docs-kit site to dash + GHCR.

Scaffolded for you#

The CLI writes the whole deploy: config/deploy.yml, .dash/secrets, a Dockerfile, and a .github/workflows/deploy-docs.yml that calls the shared reusable workflow. Point it at your repo and you have a deployable app:

docs-kit new my-docs --image OWNER/REPO --service my-repo

The Docker image#

Lean, multi-stage, and upgradable.

The scaffolded Dockerfile is a multi-stage build: a throwaway build stage carries the toolchain (build-essential, git, bun) and compiles the gems + assets, and the final stage copies only the installed bundle and the app — no compilers, no node_modules. A shipped .dockerignore keeps the build context small (no .git, node_modules, logs, specs, or coverage). When the site bundles thruster (a Rails 8 default), bin/thrust fronts Puma with HTTP caching, compression, and X-Sendfile — Thruster listens on the routed port (3000) and proxies to Puma.

The .dockerignore is gem-owned — every docs_kit:install run refreshes it. The Dockerfile is yours to tune, so the generator never clobbers it; it stamps a version marker (# docs-kit Dockerfile vX.Y.Z) so --sync warns you when a newer, leaner template ships. Diff and adopt:

bin/rails g docs_kit:install --sync   # warns if your Dockerfile is stale
diff Dockerfile "$(bundle show docs-kit)/lib/generators/docs_kit/install/templates/Dockerfile.tt"

dash-proxy, switched on#

The scaffolded deploy.yml uses the proxy, not just the router.

Every site deploys with dash 4 (minimum_version: 4.0.7) and turns on the per-app dash-proxy features a docs site benefits from — no per-site tuning, the template writes them:

  • compress: true — zstd / brotli / gzip negotiated at the edge; Thruster-encoded responses pass through.
  • cache: { enabled: true, max_ttl: 300 } — an RFC 9111 shared cache. It stores only responses marked Cache-Control: public (Propshaft assets, /llms.txt); HTML carrying a session cookie is refused by design. dash proxy cache stats shows what it holds.
  • headersX-Content-Type-Options / Referrer-Policy set once at the proxy; Server and X-Powered-By stripped.
  • intercept_errors: [502, 503, 504] + error_pages_path: public — the site's own status pages during a container swap, not a bare "Bad Gateway".
  • exclude_metrics_paths: [/up] — the health probe stays out of the request histograms.

Deliberately left alone: proxy.run is host-wide (every site on the shared host boots the same proxy; a differing run: block reboots it on each alternate deploy), and rate_limit / deny_ips need client_ip.trusted_proxies pinned to the tunnel's address to key on visitors rather than on cloudflared. dash docs proxy is the always-current reference.

The first dash 4 deploy on a host renames the proxy (kamal-proxydash-proxy) and copies its config volume — one short outage on that host while ports 80/443 change hands, paid once by whichever site deploys first.

The reusable workflow#

Build and deploy live once in zoolutions/docs-kit/.github/workflows/deploy.yml. Each site's .github/workflows/deploy-docs.yml is a thin caller — no build logic is copied per site.

.github/workflows/deploy-docs.yml
on:
  release: { types: [published] }
  workflow_dispatch:

permissions:
  contents: read
  packages: write

jobs:
  deploy:
    uses: zoolutions/docs-kit/.github/workflows/deploy.yml@main
    with:
      image: OWNER/REPO
      service: my-repo
    secrets: inherit

Naming#

Use the repo name.

Set image and service to the repo's OWNER/REPO. The pushed GHCR package then auto-links to the repo, so GITHUB_TOKEN can push and pull it — no PAT required.

A name that doesn't match the repo becomes an unlinked package that GITHUB_TOKEN can't pull — the deploy fails when dash tries to fetch the image. Before deploying, the workflow runs dash doctor, a pre-flight of host, registry, proxy, ports and readiness gates that fails the job early with one report instead of one failure at a time.

Secrets#

SecretPurpose
SSH_PRIVATE_KEYDeploy key for the dash SSH user.
DEPLOY_HOSTThe deploy host (IP or DNS).
DEPLOY_DOMAINThe public host dash-proxy routes.

Add these to a docs GitHub Environment. The registry password is the auto-provided GITHUB_TOKEN, so secrets: inherit passes everything the reusable workflow needs.

Requirements the caller must set#

The caller workflow MUST grant permissions: packages: write itself — a reusable workflow can't escalate its caller's permissions. Without it the deploy fails at startup, before any dash step runs.