(In 2024) I like to build compatible pairs of "builder" and slim "runner" images. For Alpine/Node, here's a builder (your basket of dev libraries will vary, of course):
# node-alpine-builder
FROM node:20.15.1-alpine
RUN npm set registry "${NPM_REGISTRY}"
RUN apk update && apk --no-cache add bash curl file git jq build-base g++ make \
python3 python3-dev cyrus-sasl-dev openssl-dev ca-certificates lz4-dev \
musl-dev cairo-dev pango-dev giflib-dev librsvg-dev fontconfig
RUN npm install -g node-gyp
and here's a minimal runner (I usually add matching runtime libraries at the app Dockerfile
level rather than in a kitchen sink base image):
# node-alpine-runner
FROM node:20.15.1-alpine
RUN apk update && apk --no-cache add openssl ca-certificates cyrus-sasl \
lz4-libs curl file
(My Dockerfiles are expanded using envsubst
.)
This then lets me make my project Dockerfile
look like this:
FROM ${DOCKER_REGISTRY}/node-alpine-builder AS BUILDER
WORKDIR /build
COPY build/ .
RUN --mount=type=cache,target=/root/.npm npm ci --omit=dev --registry=${NPM_REGISTRY} && npm run build --if-present
FROM ${DOCKER_REGISTRY}/node-alpine-runner
WORKDIR /app
COPY . .
COPY --from=BUILDER /build/node_modules ./node_modules
HEALTHCHECK --start-period=120s CMD curl -fs http://localhost:8080/health | grep -q OK || exit 1
CMD ["node", "server.js"]
To seed the first phase, I copy just the package.json
and package-lock.json
files (and a .npmrc
file with registry creds) into a temporary build
directory.
The build is kicked off with:
docker buildx build --pull --push \
--platform linux/arm64,linux/amd64 \
-t "${DOCKER_REGISTRY}/${PACKAGE_NAME}:${VERSION}" \
-t "${DOCKER_REGISTRY}/${PACKAGE_NAME}:${LATEST}" \
.
(Note that if for whatever reason you can't or don't want to use multi-phase Dockerfiles, you can achieve much of the same toolchain-slimming effect by doing a docker run -v $(pwd)/build:/build -t node-alpine-builder npm ci
and then copying the subsequent node_modules out into your slim image.)