new compose + deploy standard: generic compose, deploy injects identity
Supersedes the prior 'name everything after the repo' guidance, which caused
container-name collisions at fleet scale. The dev's compose now names nothing
repo-specific; the deploy layer injects identity and host paths.
deploy.md:
- replace the 'One name everywhere' danger callout with 'Your compose names
nothing repo-specific' (no container_name, no hardcoded names/paths)
- rewrite the compose example to the standard: svc service key,
restart: unless-stopped (required — host unit is oneshot), host paths via
${LOGS_DIR}/${CONFIG_DIR}/${MOUNTS_DIR} with :-./ local fallbacks, bare
'cache' volume. Keep user: "1337:1337" + HOME=/tmp (the services account
identity — not repo-specific)
- add 'How deploy fills it in' (injected env vars a dev can rely on + the
resulting docker ps names) and 'Onboarding a service' (deploy <host>
<workspace> <git-url> [name], deploy key handled via Gitea API)
- update paths to <name>, subprocess example to svc, checklist to the new
rules; drop docker CLI from secret rotation (host-side edit, we restart)
standards.md:
- add a 'Service compose' section: short convention + Right/Wrong tabs
(the nova before/after), linking to the Deploy guide for the full detail
Kept the services-account (1337) section and uid-agnostic Dockerfile notes.
Verified in-browser; mkdocs build --strict clean (cross-ref anchor resolves).
Signed-off-by: disqualifier <dev@disqualifier.me>
This commit is contained in:
+102
-61
@@ -7,17 +7,18 @@
|
||||
!!! tip "What can run here"
|
||||
APIs, websites, applets, bots, monitors.
|
||||
|
||||
!!! danger "One name everywhere — repo = container = volumes"
|
||||
The **repo name is the name.** Your service (the compose service key), the
|
||||
container, and every named volume **must all use that same name**. If your repo
|
||||
is `myrepo`:
|
||||
!!! danger "Your compose names nothing repo-specific"
|
||||
**No `container_name`. No hardcoded names, workspace, or `/srv` paths.** The
|
||||
deploy layer injects identity and host paths at deploy time — your compose stays
|
||||
generic so it can't collide with any other service on the fleet.
|
||||
|
||||
- service → `myrepo`
|
||||
- container → `myrepo`
|
||||
- volumes → `myrepo-data`, `myrepo-cache`, … (the repo name, then a suffix)
|
||||
- service key is always **`svc`**
|
||||
- named volumes use **bare names** (`cache`, not `myapp_cache`)
|
||||
- host paths come from **`${LOGS_DIR}` / `${CONFIG_DIR}` / `${MOUNTS_DIR}`**
|
||||
|
||||
Mismatched names break our deploy, monitoring, and log scraping — they key off
|
||||
the repo name. **Do not** name the service one thing and the repo another.
|
||||
Naming things after your repo used to cause **container-name collisions** at
|
||||
fleet scale — two repos shipping the same name clashed. The generic compose
|
||||
below fixes that; copy it verbatim.
|
||||
|
||||
## Docker — the services account
|
||||
|
||||
@@ -78,62 +79,99 @@ 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.
|
||||
|
||||
The service key, `container_name`, and volume names below are all **`myrepo`** —
|
||||
the repo name. Match yours to your repo.
|
||||
## Your `compose.yaml`
|
||||
|
||||
Copy this verbatim. It names nothing repo-specific — the deploy layer fills in
|
||||
identity and host paths.
|
||||
|
||||
```yaml
|
||||
services:
|
||||
myrepo: # (1)!
|
||||
svc: # (1)!
|
||||
build: .
|
||||
container_name: myrepo # (2)!
|
||||
user: "1337:1337" # (3)!
|
||||
user: "1337:1337" # (2)!
|
||||
restart: unless-stopped # (3)!
|
||||
environment:
|
||||
HOME: /tmp
|
||||
volumes:
|
||||
- /srv/config/<workspace>/<project>:/app/config:ro # (4)!
|
||||
- /srv/logs/<workspace>/<project>:/app/logs # (5)!
|
||||
- myrepo-data:/app/data # (6)!
|
||||
- ${LOGS_DIR:-./logs}:/app/logs # (4)!
|
||||
- ${CONFIG_DIR:-./config}:/app/config # (5)!
|
||||
- ${MOUNTS_DIR:-./mounts}:/app/mounts
|
||||
- cache:/app/cache # (6)!
|
||||
|
||||
volumes:
|
||||
myrepo-data: # (7)!
|
||||
cache: # (7)!
|
||||
```
|
||||
|
||||
1. Service key = **the repo name**.
|
||||
2. Container name = **the repo name** — same as the service.
|
||||
3. Run as the shared account. **No** in-container `user`/`useradd` — don't bake a
|
||||
user into the image; set it here.
|
||||
4. Config: host-managed bind mount, mounted **read-only**.
|
||||
5. Logs: bind mount — live and rolled, scraped for monitoring.
|
||||
6. Named volume = **repo name + suffix** (`myrepo-data`). Docker owns it, so there
|
||||
are no host permissions to fiddle with.
|
||||
7. Declare the volume under the same **repo-name-prefixed** key.
|
||||
1. The service key is always **`svc`** — never a repo-specific name, never a
|
||||
`container_name`. Our tooling keys off `svc` (the `svc` command, the
|
||||
`svc-<workspace>-<name>` unit).
|
||||
2. Run as the shared **`services`** account (**uid/gid 1337**, fixed fleet-wide).
|
||||
**No** in-container `user`/`useradd` — set it here. This is the one identity
|
||||
line that stays; it's not repo-specific.
|
||||
3. **Required.** The host-side service is oneshot; Docker's own restart policy is
|
||||
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}`.
|
||||
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.
|
||||
|
||||
## Paths and mounts
|
||||
!!! 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.
|
||||
|
||||
Everything host-side follows one shape: `/srv/<kind>/<workspace>/<project>/`.
|
||||
## How deploy fills it in
|
||||
|
||||
!!! warning "What `<workspace>` is"
|
||||
`<workspace>` is the owning bucket for a project.
|
||||
At deploy time we set the variables your compose reads, so identity and host paths
|
||||
are injected — not baked into your file. You can rely on these being present:
|
||||
|
||||
- **An individual dev?** It's your **lowercase username** — `ricky`, `xattam`, …
|
||||
- **A shared / official project?** It's the **workspace** it belongs to —
|
||||
`bots`, `web`, `apis`, …
|
||||
```
|
||||
COMPOSE_PROJECT_NAME=<workspace>-<name>
|
||||
LOGS_DIR=/srv/logs/<workspace>/<name>
|
||||
CONFIG_DIR=/srv/config/<workspace>/<name>
|
||||
MOUNTS_DIR=/srv/mounts/<workspace>/<name>
|
||||
```
|
||||
|
||||
| What | Where | How |
|
||||
| --- | --- | --- |
|
||||
| Repo + compose | `/srv/docker/<workspace>/<project>/` | created by the git clone, not pre-provisioned |
|
||||
| Config | `/srv/config/<workspace>/<project>/` | bind mount, host-managed, read-only |
|
||||
| Logs | `/srv/logs/<workspace>/<project>/` | bind mount; live + rolled, scraped |
|
||||
| Mounts (other host-visible data) | `/srv/mounts/<workspace>/<project>/` | bind mount, host-managed |
|
||||
| Caches, profiles, scratch | named volume | Docker manages ownership |
|
||||
Which makes ownership obvious and collisions impossible:
|
||||
|
||||
All `/srv/...` paths are owned by the `services` user (uid/gid **1337**).
|
||||
| item | value |
|
||||
| --- | --- |
|
||||
| container | `<workspace>-<name>-svc-1` |
|
||||
| volume | `<workspace>-<name>_cache` |
|
||||
| network | `<workspace>-<name>_default` |
|
||||
| logs | `/srv/logs/<workspace>/<name>/` |
|
||||
|
||||
!!! warning "What `<workspace>` and `<name>` are"
|
||||
- **`<workspace>`** — who owns it: an individual dev (`ricky`, `xattam`, …) or a
|
||||
category (`tpv`, `web`, `bots`, …).
|
||||
- **`<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.
|
||||
|
||||
## Onboarding a service
|
||||
|
||||
One command registers and starts a service:
|
||||
|
||||
```
|
||||
deploy <host> <workspace> <git-url> [name]
|
||||
```
|
||||
|
||||
It registers the service, generates a per-repo **read-only deploy key on the host**
|
||||
(the private key never leaves the box), pushes the public key to the repo's Gitea
|
||||
deploy keys automatically, clones the code, and installs and starts the service's
|
||||
unit. No manual key paste, no host setup on your end.
|
||||
|
||||
!!! note "If your service won't start or its logs aren't persisting"
|
||||
That's usually a host-side bind-mount **ownership** thing — the kind of detail
|
||||
**we sort out at deploy time**, not something you need to chown or provision.
|
||||
If a bot won't come up or logs/caches keep vanishing, flag it and we'll fix
|
||||
the mount perms. Stick to a clean `compose.yaml` and let us handle the host.
|
||||
If a bot won't come up or logs/caches keep vanishing, flag it and we'll fix it.
|
||||
Stick to the generic `compose.yaml` above and let us handle the host.
|
||||
|
||||
## Layer caching
|
||||
|
||||
@@ -161,9 +199,10 @@ knobs in compose:
|
||||
|
||||
```yaml
|
||||
services:
|
||||
yourbot:
|
||||
svc:
|
||||
build: .
|
||||
user: "1337:1337"
|
||||
restart: unless-stopped
|
||||
init: true # (1)!
|
||||
shm_size: "2gb" # (2)!
|
||||
mem_limit: "4g" # (3)!
|
||||
@@ -183,28 +222,30 @@ services:
|
||||
|
||||
## What your compose / Dockerfile needs
|
||||
|
||||
- `user: "1337:1337"`
|
||||
- bind mounts for **config + logs**
|
||||
- named volumes for **the rest**
|
||||
- secrets bind-mounted **`:ro`**
|
||||
**Compose**
|
||||
|
||||
- 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
|
||||
- named volumes with **bare names** (`cache`, not `myapp_cache`)
|
||||
- for browser/subprocess workloads: `init: true`, `shm_size`, `mem_limit`
|
||||
|
||||
**Dockerfile**
|
||||
|
||||
- `HOME=/tmp`
|
||||
- `chmod -R a+rwX /app`
|
||||
- `chmod -R a+rwX /app` (uid-agnostic; runs as the `services` account, 1337)
|
||||
- deps installed **before** the code copy (layer caching) — pip or `uv pip install`
|
||||
- `git` in the image **if the container needs it**
|
||||
- for browser/subprocess workloads: `init: true`, `shm_size`, `mem_limit`
|
||||
- using uv? add `ENV UV_COMPILE_BYTECODE=1`
|
||||
|
||||
## Secrets
|
||||
|
||||
!!! warning "Secrets never go in the image"
|
||||
We do **not** commit secrets (usually, lol). They stay **gitignored**, live on
|
||||
the host at `/srv/config/<workspace>/<project>/`, and are bind-mounted
|
||||
**read-only** at runtime. Add them to `.dockerignore` so a `COPY . .` can't
|
||||
sweep them into a layer.
|
||||
We do **not** commit secrets (usually, lol). They stay **gitignored** and live
|
||||
on the host under the config dir (`/srv/config/<workspace>/<name>/`), reaching
|
||||
your container read-only via `${CONFIG_DIR}`. Add them to `.dockerignore` so a
|
||||
`COPY . .` can't sweep them into a layer.
|
||||
|
||||
Rotating a secret = edit the host file and restart. No rebuild.
|
||||
|
||||
```bash
|
||||
vim /srv/config/<workspace>/<project>/secrets.env # edit on the host
|
||||
docker compose restart yourapp # pick up the change — no rebuild
|
||||
```
|
||||
Rotating a secret is a host-side edit — update the file and the service picks it up
|
||||
on restart. No rebuild, and nothing you run: flag it and we handle the restart.
|
||||
|
||||
Reference in New Issue
Block a user