ADR 0025 — Production image: pip on python:3.13-slim (multi-stage); conda retained for local dev¶
- Status: Accepted
- Date: 2026-06-27
- Deciders: @TheurgicDuke771
- Related: ADR 0017 (Python 3.13 runtime — same pinned interpreter, different packager), 0023 (GHCR pulls cross the internet → image size = cold-start/scale-out latency), 0024 (ACA deploy this image runs on). Amends — does not fully revoke — the Week-1 "conda only" tooling lock (CLAUDE.md §6/§11): conda stays for local dev; only the container image changes.
Context¶
The backend image was built on continuumio/miniconda3 and created the dataq conda env at build
time. But environment.yml installs nothing from conda channels except the Python interpreter +
pip — its entire dependencies: is pip: [-r backend/requirements-dev.txt]. So conda's one real
advantage (prebuilt binary packages from conda-forge) was unused: all ~30 runtime deps already come
from pip wheels.
Meanwhile the Week-7 cloud move made image size load-bearing: the conda image was ~2.84 GB, GHCR pulls cross the internet (ADR 0023), and the amd64 image must be built under emulation locally. We were paying conda's full cost (base image + env + build friction) for none of its benefit.
Decision¶
Build the production image with pip on a multi-stage python:3.13-slim. Keep conda as the local-dev
tool unchanged.
- Multi-stage (backend/Dockerfile): a
builderstage (python:3.13-slim build-essential, discarded) installsrequirements.txtinto a/opt/venv; theruntimestage ispython:3.13-slim+ that venv + the app code. No compilers / apt lists in the final layer.- Runtime deps only — the image installs
requirements.txt, not the dev/typecheck/tooling chain. Tests + linters run in the dev env (conda locally,pip installin CI), never in the image. CI already usesactions/setup-python+ pip directly, so it is unaffected. - Same pinned deps + interpreter —
requirements.txtstays the single source of truth and Python stays 3.13 (ADR 0017). Only the packager/base image changed. - Local dev unchanged —
environment.yml/setup.sh/ the conda workflow stay. CLAUDE.md's "conda only" now means local dev; the production image is pip-on-slim. psycopg2-binary(a wheel) is kept so the image stays apt-free; the psycopg2-source swap noted inrequirements.txtremains a deferred nicety.
Consequences¶
Positive - Image ~2.84 GB → ~0.8–1.2 GB. Faster ACA cold-start + scale-out and smaller cross-internet GHCR pulls (the ADR 0023 concern), and a faster local amd64 emulated build. - Smaller attack surface (no conda toolchain / build tools / dev+test deps in the runtime image). - Standard, portable container idiom (venv + slim) — friendlier for the BYOL/marketplace image (ADR 0013) than a conda base.
Negative / watch
- Dev/prod packager split (conda locally, pip in the image). Mitigated because deps + versions +
interpreter are identical (one requirements source, same 3.13); only the install mechanism differs.
- In-container pytest/mypy no longer work (runtime-only image) — intentional; use the dev env.
- A future conda-only dependency would reopen this; none exists today (all deps ship 3.13 wheels).
Alternatives considered¶
- Keep conda in the image. Rejected: ~2× the image size + slower builds for zero functional gain (no conda-channel packages in use).
- Full switch to venv+pip everywhere (retire conda, drop the §6 rule). Deferred: larger blast radius (setup.sh / dev docs / contributor habits) for little extra benefit beyond this image. Can be a later ADR if the dev/prod split proves annoying.
- Distroless / Alpine base. Rejected for now: distroless complicates the venv + shell-form migrate
command; Alpine's musl breaks many manylinux wheels (would force source builds).
slim(glibc) keeps wheels working with most of the size win.