deploy: add COPY-into-image tabs, clarify volume ownership, drop mounts

- deploy.md: new 'Getting your files into the image' subsection — a
  multi-tab annotated block (single file / directory / deps-first) showing
  COPY <src> <dest> patterns, plus a warning not to COPY secrets.
- clarify volume ownership: 'Logs and config are ours; the rest is yours' —
  we inject/manage the logs + config host mounts; devs put everything else
  in named volumes Docker owns.
- remove all /srv/mounts: dropped the MOUNTS_DIR volume + env var and every
  mounts reference from the compose block, injected-vars, paths line, and
  checklist. environments.md + index.md mounts mentions cleaned too.

Verified in-browser: COPY tabs switch, volume tip renders, zero mounts refs
remain; mkdocs build --strict clean.

Signed-off-by: disqualifier <dev@disqualifier.me>
This commit is contained in:
2026-07-01 01:36:25 -04:00
parent 7ed388c69c
commit 8c56fd98a8
3 changed files with 58 additions and 16 deletions
+54 -12
View File
@@ -14,7 +14,7 @@
- service key is always **`svc`**
- named volumes use **bare names** (`cache`, not `myapp_cache`)
- host paths come from **`${LOGS_DIR}` / `${CONFIG_DIR}` / `${MOUNTS_DIR}`**
- host paths come from **`${LOGS_DIR}` / `${CONFIG_DIR}`**
Naming things after your repo used to cause **container-name collisions** at
fleet scale — two repos shipping the same name clashed. The generic compose
@@ -79,6 +79,45 @@ CMD ["python", "-m", "yourapp"]
3. `--system` installs into the image's Python (no venv needed — the container
*is* the isolation); reads `pyproject.toml`, no `uv.lock` required.
### Getting your files into the image
`COPY . .` grabs the whole repo, but be explicit about anything that needs its own
place — assets, templates, a config the app reads at runtime. `COPY <src> <dest>`:
`<src>` is relative to the build context (your repo), `<dest>` is a path in the
image.
=== "A single file"
```dockerfile
COPY ./config.toml /app/config.toml # (1)!
```
1. One file into a specific path. The app reads it at `/app/config.toml`.
=== "A directory"
```dockerfile
COPY ./assets /app/assets # (1)!
```
1. A whole tree. Trailing paths are dirs — `/app/assets/` mirrors `./assets/`.
=== "Deps first (layer caching)"
```dockerfile
COPY requirements.txt . # (1)!
RUN pip install --no-cache-dir -r requirements.txt
COPY . . # (2)!
```
1. Copy just the deps file first, install, **then** copy the code.
2. See [layer caching](#layer-caching) — this ordering is why.
!!! warning "Don't `COPY` secrets into the image"
Anything sensitive stays **out** of the image — no `COPY ./secrets.env`. Secrets
live on the host and are injected read-only at runtime (see [Secrets](#secrets)).
Add them to `.dockerignore` so a blanket `COPY . .` can't sweep them in.
## Your `compose.yaml`
Copy this verbatim. It names nothing repo-specific — the deploy layer fills in
@@ -95,7 +134,6 @@ services:
volumes:
- ${LOGS_DIR:-./logs}:/app/logs # (4)!
- ${CONFIG_DIR:-./config}:/app/config # (5)!
- ${MOUNTS_DIR:-./mounts}:/app/mounts
- cache:/app/cache # (6)!
volumes:
@@ -112,16 +150,21 @@ volumes:
what recovers a crashed container.
4. Logs → host, path **injected** by the deploy layer. The `:-./logs` default lets
you `docker compose up` locally with nothing set and still work.
5. Config → host, likewise injected. Same pattern for `${MOUNTS_DIR}`.
5. Config → host, likewise injected.
6. Named volume with a **bare name** — ephemeral, auto-namespaced per service at
deploy time so it can't collide.
7. Declare bare (`cache`, not `myapp_cache`). Deploy auto-prefixes it.
!!! tip "Logs and config are ours; the rest is yours"
**We handle `logs` and `config`** — the two `${...}` host mounts above are
injected and managed at deploy time. **Everything else is yours:** anything
your service needs to persist (caches, profiles, scratch) goes in a **named
volume**, which Docker owns — no host paths for you to manage.
!!! tip "The `${VAR:-./default}` pattern"
Every host path uses a variable with a local fallback. Locally, `docker compose
up` needs nothing set — it uses `./logs`, `./config`, `./mounts`. On the fleet,
the deploy layer sets the real values (below), so the same file works both
places.
Both host paths use a variable with a local fallback. Locally, `docker compose
up` needs nothing set — it uses `./logs` and `./config`. On the fleet, the
deploy layer sets the real values (below), so the same file works both places.
## How deploy fills it in
@@ -132,7 +175,6 @@ are injected — not baked into your file. You can rely on these being present:
COMPOSE_PROJECT_NAME=<workspace>-<name>
LOGS_DIR=/srv/logs/<workspace>/<name>
CONFIG_DIR=/srv/config/<workspace>/<name>
MOUNTS_DIR=/srv/mounts/<workspace>/<name>
```
Which makes ownership obvious and collisions impossible:
@@ -150,9 +192,9 @@ Which makes ownership obvious and collisions impossible:
- **`<name>`** — the git repo name, lowercased, by default. Overridable at
deploy time.
Host paths live under `/srv/<kind>/<workspace>/<name>/` (config, logs, mounts), all
owned by the `services` user (uid/gid **1337**). You never write these paths in your
compose — you read the injected `${...}` variables.
Host paths live under `/srv/<kind>/<workspace>/<name>/` (config, logs), all owned by
the `services` user (uid/gid **1337**). You never write these paths in your compose —
you read the injected `${...}` variables.
## Onboarding a service
@@ -227,7 +269,7 @@ services:
- 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}` / `${MOUNTS_DIR}` — never hardcoded
- host paths via `${LOGS_DIR}` / `${CONFIG_DIR}` — never hardcoded
- named volumes with **bare names** (`cache`, not `myapp_cache`)
- for browser/subprocess workloads: `init: true`, `shm_size`, `mem_limit`