# Dockerfile & build 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 # services account has no home dir; $HOME must be writable WORKDIR /app RUN apt-get update \ && apt-get install -y --no-install-recommends git \ && rm -rf /var/lib/apt/lists/* # include git ONLY if the container itself needs it COPY requirements.txt . RUN pip install --no-cache-dir -r requirements.txt # deps before code — see layer caching COPY . . RUN chmod -R a+rwX /app # writable by any uid — this is "uid-agnostic" CMD ["python", "-m", "yourapp"] ``` - **`HOME=/tmp`** — the `services` account has no home dir; anything writing to `$HOME` (caches, configs) needs a writable target. - **`git`** — include it **only if the container itself needs it** (e.g. you `pip install` from git, or the app shells out to git). The build and host always have git; this is about what's *inside* the image. - **`chmod -R a+rwX /app`** — makes the app tree writable by any uid, so it runs as 1337. ## Layer caching Copy the deps file and install **before** `COPY . .`. Docker caches layers in order, so deps only reinstall when the deps file changes — not on every code edit. Get this backwards and every one-line change triggers a full dependency reinstall. ```dockerfile COPY requirements.txt . # pip baseline RUN pip install --no-cache-dir -r requirements.txt # cached until deps change COPY . . # changes every build ``` ## 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. In `COPY `, `` is relative to the build context (your repo) and `` is a path in the image. ```dockerfile COPY ./config.toml /app/config.toml # a single file into a specific path COPY ./assets /app/assets # a whole directory (tree mirrors ./assets/) ``` !!! 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.md)**). Add them to `.dockerignore` so a blanket `COPY . .` can't sweep them in. ## Faster builds with uv (optional) [uv](https://docs.astral.sh/uv/) is a drop-in for pip that reads the same `pyproject.toml` — no lockfile needed in the image. Swap the deps layer and add `UV_COMPILE_BYTECODE` so containers don't pay the first-import `.pyc` compile cost: ```dockerfile FROM python:3.12-slim COPY --from=ghcr.io/astral-sh/uv:latest /uv /uvx /bin/ # pull the uv binary from its image ENV HOME=/tmp ENV UV_COMPILE_BYTECODE=1 # compile bytecode at build, not first cold start WORKDIR /app COPY pyproject.toml . RUN uv pip install --system . # --system: into the image's Python, no venv/lock COPY . . RUN chmod -R a+rwX /app CMD ["python", "-m", "yourapp"] ``` ## Checklist - `HOME=/tmp` - `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** - using uv? add `ENV UV_COMPILE_BYTECODE=1` See **[Compose convention](compose.md)** for the `compose.yaml`, and **[Secrets](secrets.md)** for credentials.