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`** - service key is always **`svc`**
- named volumes use **bare names** (`cache`, not `myapp_cache`) - 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 Naming things after your repo used to cause **container-name collisions** at
fleet scale — two repos shipping the same name clashed. The generic compose 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 3. `--system` installs into the image's Python (no venv needed — the container
*is* the isolation); reads `pyproject.toml`, no `uv.lock` required. *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` ## Your `compose.yaml`
Copy this verbatim. It names nothing repo-specific — the deploy layer fills in Copy this verbatim. It names nothing repo-specific — the deploy layer fills in
@@ -95,7 +134,6 @@ services:
volumes: volumes:
- ${LOGS_DIR:-./logs}:/app/logs # (4)! - ${LOGS_DIR:-./logs}:/app/logs # (4)!
- ${CONFIG_DIR:-./config}:/app/config # (5)! - ${CONFIG_DIR:-./config}:/app/config # (5)!
- ${MOUNTS_DIR:-./mounts}:/app/mounts
- cache:/app/cache # (6)! - cache:/app/cache # (6)!
volumes: volumes:
@@ -112,16 +150,21 @@ volumes:
what recovers a crashed container. what recovers a crashed container.
4. Logs → host, path **injected** by the deploy layer. The `:-./logs` default lets 4. Logs → host, path **injected** by the deploy layer. The `:-./logs` default lets
you `docker compose up` locally with nothing set and still work. 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 6. Named volume with a **bare name** — ephemeral, auto-namespaced per service at
deploy time so it can't collide. deploy time so it can't collide.
7. Declare bare (`cache`, not `myapp_cache`). Deploy auto-prefixes it. 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" !!! tip "The `${VAR:-./default}` pattern"
Every host path uses a variable with a local fallback. Locally, `docker compose Both host paths use a variable with a local fallback. Locally, `docker compose
up` needs nothing set — it uses `./logs`, `./config`, `./mounts`. On the fleet, up` needs nothing set — it uses `./logs` and `./config`. On the fleet, the
the deploy layer sets the real values (below), so the same file works both deploy layer sets the real values (below), so the same file works both places.
places.
## How deploy fills it in ## 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> COMPOSE_PROJECT_NAME=<workspace>-<name>
LOGS_DIR=/srv/logs/<workspace>/<name> LOGS_DIR=/srv/logs/<workspace>/<name>
CONFIG_DIR=/srv/config/<workspace>/<name> CONFIG_DIR=/srv/config/<workspace>/<name>
MOUNTS_DIR=/srv/mounts/<workspace>/<name>
``` ```
Which makes ownership obvious and collisions impossible: 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 - **`<name>`** — the git repo name, lowercased, by default. Overridable at
deploy time. deploy time.
Host paths live under `/srv/<kind>/<workspace>/<name>/` (config, logs, mounts), all Host paths live under `/srv/<kind>/<workspace>/<name>/` (config, logs), all owned by
owned by the `services` user (uid/gid **1337**). You never write these paths in your the `services` user (uid/gid **1337**). You never write these paths in your compose —
compose — you read the injected `${...}` variables. you read the injected `${...}` variables.
## Onboarding a service ## Onboarding a service
@@ -227,7 +269,7 @@ services:
- service key `svc` — **no `container_name`**, no repo-specific names - service key `svc` — **no `container_name`**, no repo-specific names
- `user: "1337:1337"` — the shared `services` account - `user: "1337:1337"` — the shared `services` account
- `restart: unless-stopped` - `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`) - named volumes with **bare names** (`cache`, not `myapp_cache`)
- for browser/subprocess workloads: `init: true`, `shm_size`, `mem_limit` - for browser/subprocess workloads: `init: true`, `shm_size`, `mem_limit`
+2 -2
View File
@@ -68,8 +68,8 @@ happens, depending on where the project runs:
``` ```
This is how things run in production — see the [Deploy guide](deploy.md) for the This is how things run in production — see the [Deploy guide](deploy.md) for the
full container standard (uid 1337, mounts, layer caching, and the uv image full container standard (uid 1337, the compose convention, layer caching, and the
setup). uv image setup).
!!! tip "Which one?" !!! tip "Which one?"
**Local `.venv`** for quick iteration, **Makefile** when you want repeatable **Local `.venv`** for quick iteration, **Makefile** when you want repeatable
+2 -2
View File
@@ -70,7 +70,7 @@ out of it; examples use placeholders like `<workspace>`, `<project>`, and
--- ---
How to get a project running on **rethink-net**containers, paths and How to get a project running on **rethink-net**the compose convention,
mounts, permissions, and secrets. the one-command deploy, and secrets.
</div> </div>