update deploy guide per spec + add licensing standard

deploy.md:
- fix logs path to /srv/logs/<dev>/<project>
- reframe permissions as a deployer-side heads-up (bind-mount ownership is
  handled at deploy time; 'if your bot won't start or logs vanish, flag us')
  instead of a dev task / heavy footgun
- git in the image only when the container needs it (host always has git)
- NEW: layer caching (requirements before code copy)
- NEW: subprocess/browser workloads — init:true (tini + PID-1 shell-wrapper
  gotcha), shm_size 2gb, mem_limit; with code annotations and a warning
- refresh the compose-needs checklist accordingly

standards.md:
- NEW: licensing — no per-file headers; single top-level LICENSE only when a
  repo is for outside use

Verified: mkdocs build --strict clean; new deploy sections rendered.
Signed-off-by: disqualifier <dev@disqualifier.me>
This commit is contained in:
disqualifier 2026-06-29 20:51:07 -04:00
parent 4fd19bf620
commit e684ac2853
2 changed files with 66 additions and 26 deletions

View File

@ -18,23 +18,29 @@ runs cleanly as that account.
```dockerfile ```dockerfile
FROM python:3.12-slim FROM python:3.12-slim
ENV HOME=/tmp # (1)! ENV HOME=/tmp # (1)!
WORKDIR /app WORKDIR /app
RUN apt-get update \ RUN apt-get update \
&& apt-get install -y --no-install-recommends git \ && apt-get install -y --no-install-recommends git \
&& rm -rf /var/lib/apt/lists/* # (2)! && rm -rf /var/lib/apt/lists/* # (2)!
COPY requirements.txt .
RUN pip install --no-cache-dir -r requirements.txt # (3)!
COPY . . COPY . .
RUN pip install --no-cache-dir . \ RUN chmod -R a+rwX /app # (4)!
&& chmod -R a+rwX /app # (3)!
CMD ["python", "-m", "yourapp"] CMD ["python", "-m", "yourapp"]
``` ```
1. `HOME=/tmp` — the `services` account has no home dir; anything writing to 1. `HOME=/tmp` — the `services` account has no home dir; anything writing to
`$HOME` (caches, configs) needs a writable target. `$HOME` (caches, configs) needs a writable target.
2. Install `git` **only if** you `pip install` from git, then clean the apt lists 2. Include `git` **only if the container itself needs it** — e.g. you
to keep the image small. `pip install` from git, or the app shells out to git at runtime. The build and
3. `chmod -R a+rwX /app` makes the app tree writable by **any** uid — this is host always have git; this line is about what's *inside* the image.
what "uid-agnostic" means. Note it only covers **non-mounted** dirs (see the 3. Install deps **before** copying the code (see [layer
[footgun](#permissions-the-bind-mount-footgun)). caching](#layer-caching)).
4. `chmod -R a+rwX /app` makes the app tree writable by **any** uid — that's what
"uid-agnostic" means.
```yaml ```yaml
services: services:
@ -45,7 +51,7 @@ services:
HOME: /tmp HOME: /tmp
volumes: volumes:
- /srv/configs/<project>:/app/config:ro # (2)! - /srv/configs/<project>:/app/config:ro # (2)!
- /srv/<dev>/<project>:/app/logs # (3)! - /srv/logs/<dev>/<project>:/app/logs # (3)!
- yourapp-data:/app/data # (4)! - yourapp-data:/app/data # (4)!
volumes: volumes:
@ -63,29 +69,55 @@ volumes:
| What | Where | How | | What | Where | How |
| --- | --- | --- | | --- | --- | --- |
| Configs | `/srv/configs/<project>/` | bind mount, host-managed | | Configs | `/srv/configs/<project>/` | bind mount, host-managed, read-only |
| Logs | `/srv/<dev>/<project>/` | bind mount; live + rolled, scraped | | Logs | `/srv/logs/<dev>/<project>/` | bind mount; live + rolled, scraped |
| Caches, profiles, scratch | named volume | Docker manages ownership | | Caches, profiles, scratch | named volume | Docker manages ownership |
## Permissions — the bind-mount footgun !!! 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.
!!! danger "Bind-mount sources must exist and be 1337-owned *before* `up`" ## Layer caching
`docker compose up` does **not** create bind-mount directories as you. If a
bind-mount source is missing, the Docker daemon (**root**) creates it **as
root** — and your container (**1337**) then can't write it. Logs silently fall
back to console-only; caches re-download every run.
So: Copy `requirements.txt` and `pip install` **before** `COPY . .`. Docker caches
layers in order, so deps only reinstall when `requirements.txt` changes — not on
every code edit. Get this backwards and every one-line change triggers a full
dependency reinstall.
- **Bind-mount sources (configs, logs) must EXIST and be 1337-owned before `up`** ```dockerfile
— handled at provisioning, not a per-deploy chown hook. COPY requirements.txt .
- **Named volumes avoid this entirely** — use them for anything that doesn't need RUN pip install --no-cache-dir -r requirements.txt # cached until deps change
host visibility. COPY . . # changes every build
```
!!! note "Why `chmod a+rwX /app` doesn't save you here" ## Subprocess and browser workloads
The Dockerfile `chmod` only covers **non-mounted** dirs. A bind mount
overrides the image directory with the host directory, so for mounted paths Bots that spawn Chrome, Xvfb, ffmpeg, or other child processes need three extra
the **host-side ownership wins** — the image's permissions are irrelevant. knobs in compose:
```yaml
services:
yourbot:
build: .
user: "1337:1337"
init: true # (1)!
shm_size: "2gb" # (2)!
mem_limit: "4g" # (3)!
```
1. Runs **tini** as PID 1 to reap zombie subprocesses and forward signals.
Without it, spawned Chrome/Xvfb processes leak as zombies.
2. Chrome and most headless browsers crash on Docker's default **64 MB**
`/dev/shm`. Bump it for any browser workload.
3. Bound memory — especially when each worker spawns a browser. Raise it as
worker 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.
## What your compose / Dockerfile needs ## What your compose / Dockerfile needs
@ -95,7 +127,9 @@ So:
- secrets bind-mounted **`:ro`** - secrets bind-mounted **`:ro`**
- `HOME=/tmp` - `HOME=/tmp`
- `chmod -R a+rwX /app` - `chmod -R a+rwX /app`
- `git` in the build if you `pip install` from git - deps installed **before** the code copy (layer caching)
- `git` in the image **if the container needs it**
- for browser/subprocess workloads: `init: true`, `shm_size`, `mem_limit`
## Secrets ## Secrets

View File

@ -51,6 +51,12 @@ $ flake8
- Every library and project has a **README** with the install line, what it does, - Every library and project has a **README** with the install line, what it does,
and a usage example. and a usage example.
!!! note "Licensing — no per-file headers"
Don't prepend license/copyright boilerplate to source files. If a repo needs a
license, it's a single top-level **`LICENSE`** file — never repeated per file.
Most internal repos don't carry one; add it only when a repo is meant for
outside use and the terms are decided.
=== "Do" === "Do"
```python ```python