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:

```shell
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:

```shell
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](https://github.com/zoolutions/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.
- `headers` — `X-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-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.

```yaml
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.

> **Warning:** 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

| 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

> **Warning:** 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.