split-brain

Sign in

Docker images

We build and publish three images: router, classifier, and ui. We do not build cloudflared (the chart consumes the upstream cloudflare/cloudflared image, pinned to a tag) and we do not build ScalarLM (upstream publishes per-GPU-architecture images we consume directly).

Image Built from Base Runs on
split-brain-router docker/router/ python:3.12-slim CPU
split-brain-classifier docker/classifier/ python:3.12-slim CPU
split-brain-ui docker/ui/ python:3.12-slim CPU
cloudflare/cloudflared upstream (pinned tag) upstream CPU
scalarlm (upstream) n/a per ScalarLM release GPU

Conventions

  • Multi-stage builds for the Python images: a builder stage installs dependencies into a venv, a runtime stage copies the venv and source. Keeps the final image small and free of build tools.
  • Non-root user (app:app, uid 10001) in every image. The filesystem is read-only except for /tmp.
  • Pinned dependencies via uv.lock; uv itself is pinned to an exact version (uv==0.11.15). Lockfiles are committed and the build uses uv sync --frozen --no-dev.
  • Base image is python:3.12-slim (tag, not currently digest-pinned).

docker/router/Dockerfile (shape)

# syntax=docker/dockerfile:1.7
FROM python:3.12-slim@sha256:<digest> AS builder
WORKDIR /app
RUN pip install --no-cache-dir uv
COPY router/pyproject.toml router/uv.lock ./
RUN uv sync --frozen --no-dev
COPY router/src ./src

FROM python:3.12-slim@sha256:<digest> AS runtime
RUN groupadd -g 10001 app && useradd -u 10001 -g 10001 -s /sbin/nologin app
WORKDIR /app
COPY --from=builder /app/.venv ./.venv
COPY --from=builder /app/src ./src
ENV PATH=/app/.venv/bin:$PATH \
    PYTHONUNBUFFERED=1 \
    PYTHONDONTWRITEBYTECODE=1
USER app
EXPOSE 8080
ENTRYPOINT ["uvicorn", "router.app:app", "--host", "0.0.0.0", "--port", "8080"]

docker/classifier/Dockerfile (shape)

Identical to the router image except:

  • The build stage bakes the MiniLM encoder into the image by importing it once (SentenceTransformer('sentence-transformers/all-MiniLM-L6-v2') into HF_HOME=/opt/model). The image is larger but cold start is bounded by cluster pull bandwidth, not Hugging Face.
  • The runtime stage sets HF_HUB_OFFLINE=1 and TRANSFORMERS_OFFLINE=1 so the container never reaches out to Hugging Face. (The trained head is not baked — it's loaded from the PVC at /var/split-brain/heads/.)

The ui image mirrors the router image and additionally bundles its bootstrap_pipeline/ and the sentence-transformers stack (it trains the head in-process).

There is no cloudflared image in this repo — the chart uses the upstream cloudflare/cloudflared image pinned to a tag in the subchart values.

ScalarLM image selection

ScalarLM is deployed separately (its own chart, possibly its own cluster); its image/tag is not selected in this repo. The router only needs SCALARLM_BASE_URL.

Build process

Images are built with the ./split-brain build CLI (bashly), not a Makefile:

./split-brain build all --remote --tag v0.0.1   # router + classifier + ui
./split-brain build ui  --remote --tag v0.0.1   # one component

--remote rsyncs the working tree to the amd64 build host (BLACKWELL_HOST, default normal@blackwell), runs docker build --platform linux/amd64, and pushes — avoiding slow qemu cross-builds on an arm64 laptop (the cluster is amd64). Without --remote it uses local docker buildx. See cli.md.

Registry

The registry prefix comes from $REGISTRY (default docker.io/gdiamos); the build host is logged in to it. The Helm chart references images via global.image.registry joined with each component's image.repository — see helm.md.

Layout

docker/
  router/Dockerfile
  classifier/Dockerfile
  ui/Dockerfile

The Python source for each service lives outside docker/ (under top-level router/, classifier/, ui/) so the build context is the repo root.