From d4aaf932c9596ef8a90f4a385264f177f95d61dc Mon Sep 17 00:00:00 2001 From: Even Stensberg Date: Fri, 28 Aug 2026 12:54:13 +0200 Subject: [PATCH] chore: add a Dockerfile and document running in docker Two targets share one dependency layer: the default builds the site and serves it from nginx with no Node in the runtime image, and `--target dev` runs the webpack dev server for working on the app with the source mounted. nginx marks the content-hashed chunks immutable and index.html and web.js no-cache, since those two keep their names across builds and index.html is what names the chunks. It listens on IPv6 as well, because localhost resolves to ::1 inside the container and the health check reaches for it. Co-Authored-By: Claude Opus 5 --- .dockerignore | 13 ++++++++++ Dockerfile | 53 ++++++++++++++++++++++++++++++++++++++ README.md | 65 +++++++++++++++++++++++++++++++++++++++++++++++ docker/nginx.conf | 32 +++++++++++++++++++++++ 4 files changed, 163 insertions(+) create mode 100644 .dockerignore create mode 100644 Dockerfile create mode 100644 docker/nginx.conf diff --git a/.dockerignore b/.dockerignore new file mode 100644 index 0000000..7746d26 --- /dev/null +++ b/.dockerignore @@ -0,0 +1,13 @@ +# Everything the image builds for itself, plus anything that would only +# invalidate the build cache. node_modules is the big one: it is installed from +# package-lock.json inside the image and must not be copied in from a host with +# a different platform. +node_modules +dist +.git +.github +.claude +*.log +.DS_Store +Dockerfile +.dockerignore diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 0000000..23c2d60 --- /dev/null +++ b/Dockerfile @@ -0,0 +1,53 @@ +# syntax=docker/dockerfile:1 + +# Two ways to run this repository in a container: +# +# docker build -t webpack-analyse . # static site behind nginx +# docker build -t webpack-analyse-dev --target dev . # webpack dev server +# +# See the "Docker" section of README.md for the run commands. + +# Pinned to the version .nvmrc and `engines.node` ask for. `engine-strict` in +# .npmrc aborts the install on any other one, so this cannot drift silently. +ARG NODE_VERSION=24.20.0 +ARG NGINX_VERSION=1.29-alpine + +FROM node:${NODE_VERSION}-alpine AS deps +WORKDIR /app +# Only the manifests, so a source edit does not invalidate the install layer. +# .npmrc has to come along: it carries the engine-strict flag the install is +# expected to run under. +COPY package.json package-lock.json .npmrc ./ +# `npm ci` rather than `npm install`: it fails on a package-lock.json that does +# not match package.json instead of quietly rewriting it, which is what CI does. +RUN npm ci + +# Development: the webpack dev server, for editing the app with the source +# bind-mounted over /app. Not the default target. +FROM deps AS dev +COPY . . +EXPOSE 8080 +# --host 0.0.0.0 because the server has to answer on the container's external +# interface; the default 127.0.0.1 is only reachable from inside the container. +CMD ["npm", "run", "dev", "--", "--host", "0.0.0.0"] + +FROM deps AS build +COPY . . +# The published site is built with long-term caching and the analytics snippet, +# matching the GitHub Pages deploy. A self-hosted copy that should not report to +# webpack's analytics property can drop the second flag: +# docker build --build-arg WEBPACK_ENV=--env\ longTermCaching . +ARG WEBPACK_ENV="--env longTermCaching --env googleAnalytics" +RUN npx webpack --mode production ${WEBPACK_ENV} + +# Runtime: the built site is static, so nothing of Node ships in the final +# image. The app routes on the URL hash, so no history-API rewrite is needed +# either and nginx can serve the directory as it is. +FROM nginx:${NGINX_VERSION} AS runtime +COPY docker/nginx.conf /etc/nginx/conf.d/default.conf +COPY --from=build /app/dist /usr/share/nginx/html +EXPOSE 80 +# 127.0.0.1 rather than localhost, so the check does not depend on how the +# resolver orders IPv4 and IPv6. +HEALTHCHECK --interval=30s --timeout=3s --start-period=5s \ + CMD wget -q --spider http://127.0.0.1/ || exit 1 diff --git a/README.md b/README.md index fbf6fd5..9b6005f 100644 --- a/README.md +++ b/README.md @@ -18,6 +18,8 @@ This project is a lightweight front-end viewer for webpack output generated with - Node.js 24 (the current Active LTS, "Krypton") — pinned in [`.nvmrc`](.nvmrc) - npm 11.x (ships with Node 24) - A webpack project whose build can emit a stats JSON file +- Or Docker on its own, if you would rather not install Node at all — Docker + Desktop on macOS and Windows, Docker Engine on Linux. See [Docker](#docker) The Node version is enforced: `engines` in `package.json` requires `^24.20.0`, and `engine-strict=true` in `.npmrc` makes npm abort the install on a mismatched @@ -85,6 +87,69 @@ a small hand-written stats file that covers every hint of the hints page and is loadable in the app as the "hint test cases" example. CI runs this before the build, so a failing test stops the run and nothing is deployed. +## Docker + +The [`Dockerfile`](Dockerfile) builds two things from this repository: the +production site behind nginx, and the webpack dev server for working on the app. + +These commands talk to a Docker daemon, so one has to be running first. On macOS +and Windows that means [Docker Desktop][docker-desktop] — installing it is not +enough, it has to be launched, or `docker build` fails with `Cannot connect to +the Docker daemon`. On Linux, Docker Engine with a running `docker` service does +the same job. Check with: + +```bash +docker info +``` + +Serve the built site: + +```bash +docker build -t webpack-analyse . +``` + +```bash +docker run --rm -p 8080:80 webpack-analyse +``` + +Then open and upload a `stats.json`. The file is read in +the browser and never reaches the container, so nothing needs to be mounted to +analyze a build. Node only exists in the build stage; the image that runs is +nginx serving the static `dist/`, with the content-hashed bundles marked +immutable and `index.html` and `web.js` marked `no-cache` so a redeploy is never +served half from cache. + +Work on the app instead, with the source mounted and live rebuilds: + +```bash +docker build -t webpack-analyse-dev --target dev . +``` + +```bash +docker run --rm -p 8080:8080 -v "$PWD:/app" -v /app/node_modules -e WATCHPACK_POLLING=true webpack-analyse-dev +``` + +Two details in that command are worth knowing. The bare `-v /app/node_modules` +keeps the dependencies that were installed inside the image, so the mount of the +host directory over `/app` cannot hide them or replace them with binaries built +for a different platform. `WATCHPACK_POLLING=true` makes webpack poll for +changes, because file system events from a bind mount do not reliably reach a +container on macOS or Windows; on Linux it can be dropped. + +The image builds the site the same way the GitHub Pages deploy does, analytics +snippet included. For a self-hosted copy that should not report to webpack's +analytics property, build it without that flag: + +```bash +docker build --build-arg WEBPACK_ENV="--env longTermCaching" -t webpack-analyse . +``` + +The Node version is pinned by the `NODE_VERSION` build argument, which tracks +[`.nvmrc`](.nvmrc) and `engines.node`. Because `engine-strict=true` is set in +`.npmrc`, a mismatch fails the install rather than building something untested. + +[docker-desktop]: https://www.docker.com/products/docker-desktop/ + ## Reading the graphs The module and chunk graphs carry their meaning in colour, size and direction. diff --git a/docker/nginx.conf b/docker/nginx.conf new file mode 100644 index 0000000..98fd938 --- /dev/null +++ b/docker/nginx.conf @@ -0,0 +1,32 @@ +# Serves the production build from the runtime stage of the Dockerfile. +server { + listen 80; + # Also on IPv6: inside the container "localhost" resolves to ::1 first, so + # an IPv4-only socket makes anything reaching for localhost, the health + # check included, fail with a connection refused. + listen [::]:80; + server_name _; + root /usr/share/nginx/html; + + gzip on; + gzip_types text/css application/javascript application/json image/svg+xml; + gzip_min_length 1024; + + # Bundle filenames carry a content hash when the site is built with + # --env longTermCaching, so a given file never changes under its name. + location ~ "^/[0-9a-f]{16,}\.js$" { + add_header Cache-Control "public, max-age=31536000, immutable"; + } + + # index.html and the entry bundle keep their names from one build to the + # next, and index.html names the hashed chunks, so both have to revalidate + # or a new deploy can be served half from cache and fail to boot. A request + # for "/" reaches this through the internal redirect to /index.html. + location ~ "^/(index\.html|web\.js)$" { + add_header Cache-Control "no-cache"; + } + + location / { + try_files $uri $uri/ =404; + } +}