April 11, 2026

One codebase, eight Lambda images, one lockfile

Eight Lambda functions in one repo. Each gets its own container image with only the dependencies it needs. One pyproject.toml, one uv.lock.

Dependency Groups

uv supports dependency groups — named sets of packages in a single pyproject.toml:

[dependency-groups]
base = ["boto3>=1.34", "awslambdaric>=3.0"]

api = [
    {include-group = "base"},
    "fastapi>=0.115", "PyJWT>=2.10", "stripe>=8.0",
]

authorizer = [
    {include-group = "base"},
    "PyJWT>=2.10",
]

video-processing = [{include-group = "base"}]

One lockfile resolves all groups together.

Dockerfile

Every function uses the same pattern. Only the group name and handler path change:

FROM python:3.13-slim AS builder
COPY --from=ghcr.io/astral-sh/uv:latest /uv /usr/local/bin/uv
WORKDIR /build
COPY pyproject.toml uv.lock ./
RUN uv export --frozen --only-group video-processing --no-dev > requirements.txt \
    && pip install --no-cache-dir -r requirements.txt --target /install

FROM python:3.13-slim
WORKDIR /function
COPY --from=builder /install ./
COPY backend/v1/video-processing/ ./
COPY backend/v1/wshtlib/ ./wshtlib/
ENTRYPOINT ["/usr/local/bin/python", "-m", "awslambdaric"]
CMD ["index.lambda_handler"]

uv export --only-group video-processing produces a minimal requirements file for that function only.

wshtlib

COPY backend/v1/wshtlib/ ./wshtlib/

wshtlib is a shared observability library — structured logging, request context, CloudWatch metrics, Lambda warming. Stdlib only, no external dependencies, so it doesn’t belong in any dependency group. Just copied into every image.

Image Sizes

The authorizer image is ~80MB. The API image is ~180MB. They share a base layer in ECR.