diff --git a/.github/workflows/docker.yml b/.github/workflows/docker.yml index 412731cd1..9ed3a92af 100644 --- a/.github/workflows/docker.yml +++ b/.github/workflows/docker.yml @@ -12,12 +12,31 @@ env: permissions: contents: read packages: write - attestations: write - id-token: write jobs: - docker: + docker-variant-tags: runs-on: ubuntu-latest + strategy: + fail-fast: false + matrix: + include: + - variant: "" + bake_target: runtime + - variant: nonroot + bake_target: runtime-nonroot + - variant: code + bake_target: runtime-code + - variant: code-nonroot + bake_target: runtime-code-nonroot + - variant: slim + bake_target: runtime-slim + - variant: slim-nonroot + bake_target: runtime-slim-nonroot + - variant: code-slim + bake_target: runtime-code-slim + - variant: code-slim-nonroot + bake_target: runtime-code-slim-nonroot + steps: - uses: actions/checkout@v6 @@ -34,34 +53,31 @@ jobs: username: ${{ github.actor }} password: ${{ secrets.GITHUB_TOKEN }} - - name: Extract metadata + - name: Extract metadata (variant) id: meta uses: docker/metadata-action@v6 with: images: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }} tags: | - type=ref,event=branch - type=ref,event=pr - type=semver,pattern={{version}} - type=semver,pattern={{major}}.{{minor}} - type=sha,prefix=sha- + type=ref,event=branch,suffix=${{ matrix.variant != '' && format('-{0}', matrix.variant) || '' }} + type=ref,event=pr,suffix=${{ matrix.variant != '' && format('-{0}', matrix.variant) || '' }} + type=semver,pattern={{version}},suffix=${{ matrix.variant != '' && format('-{0}', matrix.variant) || '' }} + type=semver,pattern={{major}}.{{minor}},suffix=${{ matrix.variant != '' && format('-{0}', matrix.variant) || '' }} + type=semver,pattern={{major}},suffix=${{ matrix.variant != '' && format('-{0}', matrix.variant) || '' }} + type=sha,prefix=${{ matrix.variant != '' && format('{0}-', matrix.variant) || 'sha-' }} + type=raw,value=${{ matrix.variant }},enable=${{ matrix.variant != '' }} + type=raw,value=latest,enable=${{ matrix.variant == '' }} - - name: Build and push - id: push - uses: docker/build-push-action@v7 + - name: Build and push variant (bake) + id: bake + uses: docker/bake-action@v7 with: - context: . - platforms: linux/amd64,linux/arm64 - tags: ${{ steps.meta.outputs.tags }} - labels: ${{ steps.meta.outputs.labels }} - cache-from: type=gha - cache-to: type=gha,mode=max - provenance: true - sbom: true - - - name: Attest build provenance - uses: actions/attest-build-provenance@v4 - with: - subject-name: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }} - subject-digest: ${{ steps.push.outputs.digest }} - push-to-registry: true + files: | + ./docker-bake.hcl + cwd://${{ steps.meta.outputs.bake-file-tags }} + cwd://${{ steps.meta.outputs.bake-file-labels }} + targets: ${{ matrix.bake_target }} + push: true + set: | + *.cache-from=type=gha + *.cache-to=type=gha,mode=max diff --git a/Dockerfile b/Dockerfile index 415ab3b02..c75a79f04 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,5 +1,8 @@ +ARG PYTHON_VERSION=3.11 +ARG DISTROLESS_IMAGE=gcr.io/distroless/python3-debian13 + # ---- Build stage: compile native extensions, build wheel ---- -FROM python:3.11-slim AS builder +FROM python:${PYTHON_VERSION}-slim AS builder RUN apt-get update && \ apt-get install -y --no-install-recommends \ @@ -13,34 +16,41 @@ WORKDIR /build # Layer 1: install deps only (cached unless pyproject.toml/uv.lock change) COPY pyproject.toml uv.lock README.md ./ -# Stub package so uv can resolve the local ".[proxy]" without full source +# Stub package so uv can resolve the local extras without full source RUN mkdir -p headroom && touch headroom/__init__.py +ARG HEADROOM_EXTRAS=proxy,code RUN --mount=type=cache,target=/root/.cache/uv \ - uv pip install --system ".[proxy]" + uv pip install --system ".[${HEADROOM_EXTRAS}]" # Layer 2: copy real source, reinstall only headroom-ai (no deps) COPY headroom/ headroom/ RUN --mount=type=cache,target=/root/.cache/uv \ uv pip install --system --no-deps --reinstall-package headroom-ai . -# ---- Runtime stage: minimal image with only what's needed ---- -FROM python:3.11-slim AS runtime +# ---- Runtime stage (python-slim): supports root/nonroot via build arg ---- +FROM python:${PYTHON_VERSION}-slim AS runtime-slim-base + +ARG RUNTIME_USER=nonroot RUN apt-get update && \ apt-get install -y --no-install-recommends curl && \ rm -rf /var/lib/apt/lists/* -RUN groupadd --gid 1000 headroom && \ - useradd --uid 1000 --gid headroom --create-home headroom - COPY --from=builder /usr/local/lib/python3.11/site-packages /usr/local/lib/python3.11/site-packages COPY --from=builder /usr/local/bin/headroom /usr/local/bin/headroom -RUN mkdir -p /data /home/headroom/.headroom && \ - chown -R headroom:headroom /data /home/headroom/.headroom +RUN mkdir -p /home/nonroot /data && \ + if [ "$RUNTIME_USER" = "nonroot" ]; then \ + groupadd --gid 1000 nonroot && \ + useradd --uid 1000 --gid nonroot --create-home nonroot && \ + mkdir -p /home/nonroot/.headroom && \ + chown -R nonroot:nonroot /data /home/nonroot; \ + else \ + mkdir -p /root/.headroom; \ + fi -USER headroom -WORKDIR /home/headroom +USER ${RUNTIME_USER} +WORKDIR /home/nonroot ENV HEADROOM_HOST=0.0.0.0 \ PYTHONUNBUFFERED=1 \ @@ -50,3 +60,25 @@ EXPOSE 8787 ENTRYPOINT ["headroom", "proxy"] CMD ["--host", "0.0.0.0", "--port", "8787"] + +FROM ${DISTROLESS_IMAGE} AS runtime-slim + +ARG RUNTIME_USER=nonroot + +COPY --from=builder /usr/local/lib/python3.11/site-packages /usr/local/lib/python3.11/site-packages + +USER ${RUNTIME_USER} +WORKDIR /app + +ENV HEADROOM_HOST=0.0.0.0 \ + PYTHONUNBUFFERED=1 \ + PYTHONDONTWRITEBYTECODE=1 \ + PYTHONPATH=/usr/local/lib/python3.11/site-packages + +EXPOSE 8787 + +ENTRYPOINT ["python3", "-m", "headroom.cli", "proxy"] +CMD ["--host", "0.0.0.0", "--port", "8787"] + +# Default published image remains python-slim runtime +FROM runtime-slim-base AS runtime diff --git a/README.md b/README.md index 79a9688c1..49e03ca3f 100644 --- a/README.md +++ b/README.md @@ -420,6 +420,40 @@ pip install "headroom-ai[langchain]" # LangChain (experimental) pip install "headroom-ai[evals]" # Evaluation framework only ``` +### Container images (GHCR tags) + +- supported platforms: `linux/amd64`, `linux/arm64` +- tags `:code` - image with Code-Aware Compression (AST-based) i.e. `pip install "headroom-ai[proxy,code]"` +- tags `:slim` - image with distorless base + +| Tag | | Extras | Docker Bake target | +|---------------------|------------------------------------------------------|--------------|-----------------------------| +| `` | ```ghcr.io/chopratejas/headroom:``` | `proxy` | `runtime` | +| `latest` | ```ghcr.io/chopratejas/headroom:latest``` | `proxy` | `runtime` | +| `nonroot` | ```ghcr.io/chopratejas/headroom:nonroot``` | `proxy` | `runtime-nonroot` | +| `code` | ```ghcr.io/chopratejas/headroom:code``` | `proxy,code` | `runtime-code` | +| `code-nonroot` | ```ghcr.io/chopratejas/headroom:code-nonroot``` | `proxy,code` | `runtime-code-nonroot` | +| `slim` | ```ghcr.io/chopratejas/headroom:slim``` | `proxy` | `runtime-slim` | +| `slim-nonroot` | ```ghcr.io/chopratejas/headroom:slim-nonroot``` | `proxy` | `runtime-slim-nonroot` | +| `code-slim` | ```ghcr.io/chopratejas/headroom:code-slim``` | `proxy,code` | `runtime-code-slim` | +| `code-slim-nonroot` | ```ghcr.io/chopratejas/headroom:code-slim-nonroot``` | `proxy,code` | `runtime-code-slim-nonroot` | + +### Docker Bake + +```bash +# List all available build targets +docker buildx bake --list targets + +# Build default image locally (proxy + nonroot) +docker buildx bake runtime-default + +# Build one variant and load to local Docker image store +docker buildx bake runtime-code-slim-nonroot \ + --set runtime-code-slim-nonroot.platform=linux/amd64 \ + --set runtime-code-slim-nonroot.tags=headroom:local \ + --load +``` + Python 3.10+ --- diff --git a/docker-bake.hcl b/docker-bake.hcl new file mode 100644 index 000000000..07397c965 --- /dev/null +++ b/docker-bake.hcl @@ -0,0 +1,88 @@ +target "docker-metadata-action" {} + +target "_common" { + context = "." + dockerfile = "Dockerfile" + platforms = ["linux/amd64", "linux/arm64"] +} + +target "runtime-default" { + inherits = ["_common", "docker-metadata-action"] + target = "runtime" + args = { + HEADROOM_EXTRAS = "proxy" + RUNTIME_USER = "nonroot" + } +} + +target "runtime" { + inherits = ["_common", "docker-metadata-action"] + target = "runtime" + args = { + HEADROOM_EXTRAS = "proxy" + RUNTIME_USER = "root" + } +} + +target "runtime-nonroot" { + inherits = ["_common", "docker-metadata-action"] + target = "runtime" + args = { + HEADROOM_EXTRAS = "proxy" + RUNTIME_USER = "nonroot" + } +} + +target "runtime-code" { + inherits = ["_common", "docker-metadata-action"] + target = "runtime" + args = { + HEADROOM_EXTRAS = "proxy,code" + RUNTIME_USER = "root" + } +} + +target "runtime-code-nonroot" { + inherits = ["_common", "docker-metadata-action"] + target = "runtime" + args = { + HEADROOM_EXTRAS = "proxy,code" + RUNTIME_USER = "nonroot" + } +} + +target "runtime-slim" { + inherits = ["_common", "docker-metadata-action"] + target = "runtime-slim" + args = { + HEADROOM_EXTRAS = "proxy" + RUNTIME_USER = "root" + } +} + +target "runtime-slim-nonroot" { + inherits = ["_common", "docker-metadata-action"] + target = "runtime-slim" + args = { + HEADROOM_EXTRAS = "proxy" + RUNTIME_USER = "nonroot" + } +} + +target "runtime-code-slim" { + inherits = ["_common", "docker-metadata-action"] + target = "runtime-slim" + args = { + HEADROOM_EXTRAS = "proxy,code" + RUNTIME_USER = "root" + } +} + +target "runtime-code-slim-nonroot" { + inherits = ["_common", "docker-metadata-action"] + target = "runtime-slim" + args = { + HEADROOM_EXTRAS = "proxy,code" + RUNTIME_USER = "nonroot" + } +}