From 793e2de89936d82058a0856501905fe6c93604a4 Mon Sep 17 00:00:00 2001 From: Kevin Veen-Birkenbach Date: Thu, 10 Sep 2026 17:16:23 +0200 Subject: [PATCH] build(docker): ship only the runtime assets, without node or dev packages The image ran npm install without --omit=dev, so every published tag carried Cypress with its downloaded binary, ESLint and the whole dev tree next to nodejs and npm. The npm install layer alone was 229 MB of the 349 MB compressed 2.0.0 image, and all seven npm audit findings (extract-zip, form-data, tmp, qs, uuid via @cypress/request) came from it, although the app only serves five vendored asset directories. A node:22-slim stage now runs npm install --omit=dev, whose postinstall writes static/vendor, and the final runtime stage copies just that directory onto the Python base. A dev stage keeps nodejs and npm for docker-compose, which bind-mounts app/ and runs npm install on start; compose now builds that target. .dockerignore keeps a local app/node_modules and app/static/vendor out of COPY app/. Measured on make build: the runtime image is 142 MB uncompressed, has no node, npm, node_modules or Cypress cache, and serves /, /de/ and every vendored asset with 200. The dev stage has node 20.19.2 and npm 9.2.0. make test passes, hadolint included, with 107 Cypress tests. Co-Authored-By: Claude Opus 5 (1M context) --- .dockerignore | 2 ++ Dockerfile | 22 +++++++++++++++++----- docker-compose.yml | 1 + 3 files changed, 20 insertions(+), 5 deletions(-) create mode 100644 .dockerignore diff --git a/.dockerignore b/.dockerignore new file mode 100644 index 0000000..efb31e3 --- /dev/null +++ b/.dockerignore @@ -0,0 +1,2 @@ +app/node_modules/ +app/static/vendor/ diff --git a/Dockerfile b/Dockerfile index 3b47662..b146890 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,12 +1,16 @@ -FROM python:3.12-slim +FROM node:22-slim AS assets + +WORKDIR /app +COPY app/package.json ./ +COPY app/scripts ./scripts +RUN npm install --omit=dev --no-audit --no-fund + +FROM python:3.12-slim AS base ENV PYTHONDONTWRITEBYTECODE=1 \ PYTHONUNBUFFERED=1 \ FLASK_HOST=0.0.0.0 -# hadolint ignore=DL3008 -RUN apt-get update && apt-get install -y --no-install-recommends nodejs npm && rm -rf /var/lib/apt/lists/* - WORKDIR /tmp/build COPY pyproject.toml README.md main.py ./ @@ -15,6 +19,14 @@ RUN python -m pip install --no-cache-dir . WORKDIR /app COPY app/ . -RUN npm install --prefix /app CMD ["python", "app.py"] + +FROM base AS dev + +# hadolint ignore=DL3008 +RUN apt-get update && apt-get install -y --no-install-recommends nodejs npm && rm -rf /var/lib/apt/lists/* + +FROM base AS runtime + +COPY --from=assets /app/static/vendor ./static/vendor diff --git a/docker-compose.yml b/docker-compose.yml index 7f0dda4..e72eb1a 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -7,6 +7,7 @@ services: build: context: . dockerfile: Dockerfile + target: dev container_name: portfolio ports: - "${PORT:?PORT must be set in .env (see env.example)}:${PORT:?PORT must be set in .env (see env.example)}"