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-repoThe 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 markedCache-Control: public(Propshaft assets,/llms.txt); HTML carrying a session cookie is refused by design.dash proxy cache statsshows what it holds.headers—X-Content-Type-Options/Referrer-Policyset once at the proxy;ServerandX-Powered-Bystripped.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-proxy → dash-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.
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: inheritNaming#
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.
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#
| Secret | Purpose |
|---|---|
SSH_PRIVATE_KEY | Deploy key for the dash SSH user. |
DEPLOY_HOST | The deploy host (IP or DNS). |
DEPLOY_DOMAIN | The 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#
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.