handbook/docs/deploy.md
disqualifier dafc1dcacd fix path: /srv/configs -> /srv/config (singular); fix broken anchor
- deploy.md: /srv/configs -> /srv/config (singular is canonical) across the
  compose mount, paths table, secrets path, and rotation snippet. Add the
  repo+compose row (/srv/docker/<workspace>/<project>, created by the clone,
  not pre-provisioned) and a note that all /srv paths are owned by the
  services user (1337) — rounding out the canonical layout.
- workflow.md: the first 'per-project git identity' link pointed at a
  nonexistent #per-project-git-identity anchor; point it at #handy-shell-setup
  (where the gitsetup alias lives), matching the other link to the same spot.

Did not touch pip/requirements, chmod a+rwX + user 1337, HOME=/tmp, init,
layer caching, or the git-install caveat — all intentional standards.

Verified: mkdocs build --strict clean (validates anchors); table renders.
Signed-off-by: disqualifier <dev@disqualifier.me>
2026-06-30 00:57:24 -04:00

159 lines
5.8 KiB
Markdown

# Deployment Guide
> How a project gets onto **rethink-net** — our Ubuntu 26.x servers. Get your
> container to follow a few consistent rules and deploying is mostly handing us a
> `compose.yaml`.
!!! tip "What can run here"
APIs, websites, applets, bots, monitors.
## Docker — the services account
Every service runs containerized as the shared **`services`** account:
**uid/gid 1337**, fixed fleet-wide. Build your image to be **uid-agnostic** so it
runs cleanly as that account.
```dockerfile
FROM python:3.12-slim
ENV HOME=/tmp # (1)!
WORKDIR /app
RUN apt-get update \
&& apt-get install -y --no-install-recommends git \
&& rm -rf /var/lib/apt/lists/* # (2)!
COPY requirements.txt .
RUN pip install --no-cache-dir -r requirements.txt # (3)!
COPY . .
RUN chmod -R a+rwX /app # (4)!
CMD ["python", "-m", "yourapp"]
```
1. `HOME=/tmp` — the `services` account has no home dir; anything writing to
`$HOME` (caches, configs) needs a writable target.
2. Include `git` **only if the container itself needs it** — e.g. you
`pip install` from git, or the app shells out to git at runtime. The build and
host always have git; this line is about what's *inside* the image.
3. Install deps **before** copying the code (see [layer
caching](#layer-caching)).
4. `chmod -R a+rwX /app` makes the app tree writable by **any** uid — that's what
"uid-agnostic" means.
```yaml
services:
yourapp:
build: .
user: "1337:1337" # (1)!
environment:
HOME: /tmp
volumes:
- /srv/config/<workspace>/<project>:/app/config:ro # (2)!
- /srv/logs/<workspace>/<project>:/app/logs # (3)!
- yourapp-data:/app/data # (4)!
volumes:
yourapp-data:
```
1. Run as the shared account. **No** in-container `user`/`useradd` — don't bake a
user into the image; set it here.
2. Configs: host-managed bind mount, mounted **read-only**.
3. Logs: bind mount — live and rolled, scraped for monitoring.
4. Everything else: a **named volume**. Docker owns it, so there are no host
permissions to fiddle with.
## Paths and mounts
Everything host-side follows one shape: `/srv/<kind>/<workspace>/<project>/`.
!!! warning "What `<workspace>` is"
`<workspace>` is the owning bucket for a project.
- **An individual dev?** It's your **lowercase username**`ricky`, `xattam`, …
- **A shared / official project?** It's the **workspace** it belongs to —
`bots`, `web`, `apis`, …
| 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 |
All `/srv/...` paths are owned by the `services` user (uid/gid **1337**).
!!! 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.
## Layer caching
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.
```dockerfile
COPY requirements.txt .
RUN pip install --no-cache-dir -r requirements.txt # cached until deps change
COPY . . # changes every build
```
## Subprocess and browser workloads
Bots that spawn Chrome, Xvfb, ffmpeg, or other child processes need three extra
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
- `user: "1337:1337"`
- bind mounts for **configs + logs**
- named volumes for **the rest**
- secrets bind-mounted **`:ro`**
- `HOME=/tmp`
- `chmod -R a+rwX /app`
- 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
!!! 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.
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
```