fix(docker): use multi-stage build to remove build-essential from runtime image (#1846)

* fix(docker): use multi-stage build to remove build-essential from runtime image

The build-essential toolchain (~200 MB) was only needed for compiling
native Python extensions during `uv sync` but remained in the final
image, increasing size and attack surface. Split the Dockerfile into
a builder stage (with build-essential) and a clean runtime stage that
copies only the compiled artifacts, Node.js, Docker CLI, and uv.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* fix(docker): add dev stage and pin docker:cli per review feedback

Address Copilot review comments:
- Add a `dev` build stage (FROM builder) that retains build-essential
  so startup-time `uv sync` in dev containers can compile from source
- Update docker-compose-dev.yaml to use `target: dev` for gateway and
  langgraph services
- Keep the clean runtime stage (no build-essential) as the default
  final stage for production builds

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
yangzheli 2026-04-05 15:30:34 +08:00 committed by GitHub
parent 72d4347adb
commit e5416b539a
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
2 changed files with 46 additions and 9 deletions

View File

@ -1,10 +1,14 @@
# Backend Development Dockerfile # Backend Dockerfile — multi-stage build
# Stage 1 (builder): compiles native Python extensions with build-essential
# Stage 2 (dev): retains toolchain for dev containers (uv sync at startup)
# Stage 3 (runtime): clean image without compiler toolchain for production
# UV source image (override for restricted networks that cannot reach ghcr.io) # UV source image (override for restricted networks that cannot reach ghcr.io)
ARG UV_IMAGE=ghcr.io/astral-sh/uv:0.7.20 ARG UV_IMAGE=ghcr.io/astral-sh/uv:0.7.20
FROM ${UV_IMAGE} AS uv-source FROM ${UV_IMAGE} AS uv-source
FROM python:3.12-slim-bookworm # ── Stage 1: Builder ──────────────────────────────────────────────────────────
FROM python:3.12-slim-bookworm AS builder
ARG NODE_MAJOR=22 ARG NODE_MAJOR=22
ARG APT_MIRROR ARG APT_MIRROR
@ -16,7 +20,7 @@ RUN if [ -n "${APT_MIRROR}" ]; then \
sed -i "s|deb.debian.org|${APT_MIRROR}|g" /etc/apt/sources.list 2>/dev/null || true; \ sed -i "s|deb.debian.org|${APT_MIRROR}|g" /etc/apt/sources.list 2>/dev/null || true; \
fi fi
# Install system dependencies + Node.js (provides npx for MCP servers) # Install build tools + Node.js (build-essential needed for native Python extensions)
RUN apt-get update && apt-get install -y \ RUN apt-get update && apt-get install -y \
curl \ curl \
build-essential \ build-essential \
@ -29,6 +33,41 @@ RUN apt-get update && apt-get install -y \
&& apt-get install -y nodejs \ && apt-get install -y nodejs \
&& rm -rf /var/lib/apt/lists/* && rm -rf /var/lib/apt/lists/*
# Install uv (source image overridable via UV_IMAGE build arg)
COPY --from=uv-source /uv /uvx /usr/local/bin/
# Set working directory
WORKDIR /app
# Copy backend source code
COPY backend ./backend
# Install dependencies with cache mount
RUN --mount=type=cache,target=/root/.cache/uv \
sh -c "cd backend && UV_INDEX_URL=${UV_INDEX_URL:-https://pypi.org/simple} uv sync"
# ── Stage 2: Dev ──────────────────────────────────────────────────────────────
# Retains compiler toolchain from builder so startup-time `uv sync` can build
# source distributions in development containers.
FROM builder AS dev
# Install Docker CLI (for DooD: allows starting sandbox containers via host Docker socket)
COPY --from=docker:cli /usr/local/bin/docker /usr/local/bin/docker
EXPOSE 8001 2024
CMD ["sh", "-c", "cd backend && PYTHONPATH=. uv run uvicorn app.gateway.app:app --host 0.0.0.0 --port 8001"]
# ── Stage 3: Runtime ──────────────────────────────────────────────────────────
# Clean image without build-essential — reduces size (~200 MB) and attack surface.
FROM python:3.12-slim-bookworm
# Copy Node.js runtime from builder (provides npx for MCP servers)
COPY --from=builder /usr/bin/node /usr/bin/node
COPY --from=builder /usr/lib/node_modules /usr/lib/node_modules
RUN ln -s ../lib/node_modules/npm/bin/npm-cli.js /usr/bin/npm \
&& ln -s ../lib/node_modules/npm/bin/npx-cli.js /usr/bin/npx
# Install Docker CLI (for DooD: allows starting sandbox containers via host Docker socket) # Install Docker CLI (for DooD: allows starting sandbox containers via host Docker socket)
COPY --from=docker:cli /usr/local/bin/docker /usr/local/bin/docker COPY --from=docker:cli /usr/local/bin/docker /usr/local/bin/docker
@ -38,12 +77,8 @@ COPY --from=uv-source /uv /uvx /usr/local/bin/
# Set working directory # Set working directory
WORKDIR /app WORKDIR /app
# Copy frontend source code # Copy backend with pre-built virtualenv from builder
COPY backend ./backend COPY --from=builder /app/backend ./backend
# Install dependencies with cache mount
RUN --mount=type=cache,target=/root/.cache/uv \
sh -c "cd backend && UV_INDEX_URL=${UV_INDEX_URL:-https://pypi.org/simple} uv sync"
# Expose ports (gateway: 8001, langgraph: 2024) # Expose ports (gateway: 8001, langgraph: 2024)
EXPOSE 8001 2024 EXPOSE 8001 2024

View File

@ -113,6 +113,7 @@ services:
build: build:
context: ../ context: ../
dockerfile: backend/Dockerfile dockerfile: backend/Dockerfile
target: dev
# cache_from disabled - requires manual setup: mkdir -p /tmp/docker-cache-gateway # cache_from disabled - requires manual setup: mkdir -p /tmp/docker-cache-gateway
args: args:
APT_MIRROR: ${APT_MIRROR:-} APT_MIRROR: ${APT_MIRROR:-}
@ -169,6 +170,7 @@ services:
build: build:
context: ../ context: ../
dockerfile: backend/Dockerfile dockerfile: backend/Dockerfile
target: dev
# cache_from disabled - requires manual setup: mkdir -p /tmp/docker-cache-langgraph # cache_from disabled - requires manual setup: mkdir -p /tmp/docker-cache-langgraph
args: args:
APT_MIRROR: ${APT_MIRROR:-} APT_MIRROR: ${APT_MIRROR:-}