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>
This commit is contained in:
2026-07-01 02:32:16 -04:00
parent c335887dd6
commit f9e5e4f24d
10 changed files with 281 additions and 285 deletions
+106
View File
@@ -0,0 +1,106 @@
# 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.
```yaml
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:
```yaml
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 `svc`**no `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](dockerfile.md)** for the image, and **[Secrets](secrets.md)**
for keeping credentials out of it.