Files
handbook/docs/deploy/compose.md
T
dsql f9e5e4f24d split Deploy into a hub + 3 sub-pages; inline comments over annotations
The Deploy page got long and the code annotations rendered inconsistently
(some as (n) numbers, some as clickable +, and a marker on a fully-commented
line made that line vanish). Fix both:

- Deploy is now a HUB (docs/deploy/index.md): the intro + collision rule +
  central-deploy note, then card links to three focused sub-pages. Deploy
  stays the single global-nav entry; sub-pages are not_in_nav, reached from
  the hub cards.
  - deploy/compose.md — compose convention, storage tiers, how deploy fills
    it in, subprocess/browser knobs, checklist
  - deploy/dockerfile.md — services-account image, COPY, layer caching, uv
  - deploy/secrets.md — keeping secrets out of the image
- Replace code annotations with INLINE COMMENTS on the compose/Dockerfile
  examples: everything visible at once, no + to click, and the commented
  MOUNTS_DIR line no longer disappears.
- Update inbound links (index card, standards, workflow, environments) to
  deploy/ and deploy/compose.md; nav Deploy -> deploy/index.md with
  not_in_nav for the sub-pages.

Verified in-browser: hub cards link correctly, sub-pages render with visible
inline comments (0 annotation markers), left nav shows only Deploy;
mkdocs build --strict clean (validates not_in_nav + all cross-links).

Signed-off-by: disqualifier <dev@disqualifier.me>
2026-07-01 02:32:16 -04:00

4.8 KiB

Compose convention

The compose.yaml your repo ships. Copy it verbatim — it names nothing repo-specific, so the deploy layer can inject identity and host paths without collisions.

services:
  svc:                                     # generic service key — ALWAYS svc, no container_name
    build: .
    user: "1337:1337"                      # the shared services account (required)
    restart: unless-stopped                # required — the host unit is oneshot; this recovers crashes
    environment:
      HOME: /tmp
    volumes:
      - ${LOGS_DIR:-./logs}:/app/logs        # output   — host log dir, injected at deploy
      - ${CONFIG_DIR:-./config}:/app/config  # input     — host config dir, injected at deploy
      # - ${MOUNTS_DIR:-./mounts}:/app/data  # optional  — arbitrary host data (opt-in, see below)
      - cache:/app/cache                     # your data — ephemeral named volume

volumes:
  cache:                                   # bare name — deploy auto-prefixes it per service

The rules

  • Service key is always svc. Never a repo-specific name, never a container_name — the ops tooling keys off svc. This is what makes services collision-proof on the fleet.
  • user: "1337:1337" and restart: unless-stopped are required. The first runs as the shared services account (uid/gid 1337, fixed fleet-wide); the second lets Docker recover a crashed container (the host-side unit is oneshot).
  • Host paths come from ${...} variables, never hardcoded. Write to ${LOGS_DIR} and ${CONFIG_DIR}; ${MOUNTS_DIR} is optional.
  • Named volumes use bare names (cache, not myapp_cache) — deploy auto-prefixes them per service so they can't collide.

!!! tip "The container path is where your code reads and writes" WORKDIR is /app, so the right-hand side of each volume line is the path your code targets. If your app writes to ./cache (i.e. /app/cache), that's the cache mount; logs go to /app/logs, config is read from /app/config. Match the container path to what your code actually uses.

Three tiers of storage

  • logs (output) and config (input you edit) — we handle these: injected and managed at deploy time.
  • mounts (optional) — a host dir for arbitrary read/write data. The deploy layer injects MOUNTS_DIR and auto-creates the dir, but the mount line is opt-in: uncomment it and choose the container path yourself (it's app-specific, so it can't be auto-mounted).
  • Named volumes (cache, …) — yours: anything else your service persists. Docker owns them, no host paths to manage.

!!! tip "The ${VAR:-./default} pattern" Host paths use a variable with a local fallback. Locally, docker compose up needs nothing set — it uses ./logs, ./config (and ./mounts if you enable it). When deployed, the ops tooling sets the real values, so the same file works both places.

How deploy fills it in

At deploy time the tooling generates the environment your compose reads, so the ${...} variables resolve without anything from you:

LOGS_DIR      # your host log dir      -> ${LOGS_DIR}
CONFIG_DIR    # your host config dir   -> ${CONFIG_DIR}
MOUNTS_DIR    # optional host data dir -> ${MOUNTS_DIR} (if you enable the mount)

Write your logs and config to those mount points and you're set. If a service won't come up or its logs aren't persisting, that's a host-side detail on our end — flag it and we'll sort it.

Subprocess and browser workloads

Bots that spawn Chrome, Xvfb, ffmpeg, or other child processes need three extra knobs:

services:
  svc:
    build: .
    user: "1337:1337"
    restart: unless-stopped
    init: true              # tini as PID 1 — reaps zombie subprocesses, forwards signals
    shm_size: "2gb"         # Chrome/headless browsers crash on Docker's default 64 MB /dev/shm
    mem_limit: "4g"         # bound memory — raise as worker/browser count grows

!!! warning "The PID-1 gotcha with shell-wrapper CMDs" If your CMD is a shell-script wrapper (e.g. xvfb-run ...), it must not be PID 1, or the real process dies on startup. init: true is exactly what fixes this — tini takes PID 1, and your wrapper runs as a normal child.

Checklist

  • service key svcno container_name, no repo-specific names
  • user: "1337:1337" — the shared services account
  • restart: unless-stopped
  • host paths via ${LOGS_DIR} / ${CONFIG_DIR} (and optional ${MOUNTS_DIR}) — never hardcoded
  • named volumes with bare names (cache, not myapp_cache)
  • for browser/subprocess workloads: init: true, shm_size, mem_limit

See Dockerfile & build for the image, and Secrets for keeping credentials out of it.