From 10db8ce058e3cf8c5e893eee690bbf6501df6c4e Mon Sep 17 00:00:00 2001 From: Jim O'Donnell Date: Thu, 5 Dec 2024 10:19:52 +0000 Subject: [PATCH 1/3] build: run the production apps as the node user In the production image, change the owner and group of all the app files to `node:node`, then run the Next.js apps as `node`. --- Dockerfile | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/Dockerfile b/Dockerfile index 7290d997a5..6081ffc78f 100644 --- a/Dockerfile +++ b/Dockerfile @@ -64,6 +64,10 @@ RUN mkdir -p /usr/src WORKDIR /usr/src/ +RUN chown -R node:node . + +USER node + COPY --from=builder /usr/src/package.json /usr/src/package.json COPY --from=builder /usr/src/.yarn /usr/src/.yarn From ed04d92ba1860d9ab87dc4d33d96c3027a8d3f24 Mon Sep 17 00:00:00 2001 From: Jim O'Donnell Date: Thu, 5 Dec 2024 09:29:57 +0000 Subject: [PATCH 2/3] build: use a base image for Docker builds Split the Docker build into three stages: 1. base image: Node 20 Alpine with the build environment, package files, Yarn and Lerna config. 1. builder image: bootstraps the project and root apps from the base image. 1. runner image: installs only production dependencies and runs the built apps from the `builder` image. This is the image that's deployed to Kubernetes. Run the Next.js apps as the `node` user, to secure the apps in production. --- Dockerfile | 49 +++++++++++++------------------------------------ 1 file changed, 13 insertions(+), 36 deletions(-) diff --git a/Dockerfile b/Dockerfile index 6081ffc78f..a1a641cf92 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,4 +1,4 @@ -FROM node:20-alpine AS builder +FROM node:20-alpine AS base ARG COMMIT_ID ENV COMMIT_ID=$COMMIT_ID @@ -18,31 +18,27 @@ ENV APP_ENV=$APP_ENV ENV NEXT_TELEMETRY_DISABLED=1 -ARG CONTENTFUL_ACCESS_TOKEN - -ARG CONTENTFUL_SPACE_ID - -ARG SENTRY_AUTH_TOKEN - RUN mkdir -p /usr/src WORKDIR /usr/src/ -ADD package.json /usr/src/ +RUN chown -R node:node . -COPY .yarn /usr/src/.yarn +ONBUILD COPY --chown=node:node package.json yarn.lock lerna.json .yarnrc /usr/src/ -ADD .yarnrc /usr/src/ +ONBUILD COPY --chown=node:node .yarn /usr/src/.yarn -ADD lerna.json /usr/src/ +ONBUILD USER node -COPY ./packages /usr/src/packages +FROM base AS builder -ADD yarn.lock /usr/src/ +ARG CONTENTFUL_ACCESS_TOKEN -RUN chown -R node:node . +ARG CONTENTFUL_SPACE_ID -USER node +ARG SENTRY_AUTH_TOKEN + +COPY --chown=node:node ./packages /usr/src/packages RUN --mount=type=cache,id=fem-builder-yarn,uid=1000,gid=1000,target=/home/node/.yarn YARN_CACHE_FOLDER=/home/node/.yarn yarn install --production=false --frozen-lockfile --ignore-scripts RUN --mount=type=cache,id=fem-builder-yarn,uid=1000,gid=1000,target=/home/node/.yarn YARN_CACHE_FOLDER=/home/node/.yarn yarn workspace @zooniverse/react-components build:es6 @@ -55,28 +51,9 @@ RUN --mount=type=cache,id=fem-builder-yarn,uid=1000,gid=1000,target=/home/node/. RUN echo $COMMIT_ID > /usr/src/packages/app-root/public/commit_id.txt RUN --mount=type=cache,id=fem-builder-yarn,uid=1000,gid=1000,target=/home/node/.yarn YARN_CACHE_FOLDER=/home/node/.yarn yarn workspace @zooniverse/fe-root build -FROM node:20-alpine AS runner - -ARG NODE_ENV=production -ENV NODE_ENV=$NODE_ENV - -RUN mkdir -p /usr/src - -WORKDIR /usr/src/ - -RUN chown -R node:node . - -USER node - -COPY --from=builder /usr/src/package.json /usr/src/package.json - -COPY --from=builder /usr/src/.yarn /usr/src/.yarn - -COPY --from=builder /usr/src/.yarnrc /usr/src/.yarnrc - -COPY --from=builder /usr/src/packages ./packages +FROM base AS runner -COPY --from=builder /usr/src/yarn.lock /usr/src/yarn.lock +COPY --from=builder --chown=node:node /usr/src/packages ./packages RUN --mount=type=cache,id=fem-runner-yarn,uid=1000,gid=1000,target=/home/node/.yarn YARN_CACHE_FOLDER=/home/node/.yarn yarn install --production --frozen-lockfile --ignore-scripts --prefer-offline From cb7e9921314ec18f64e7eb87947f1032268fe152 Mon Sep 17 00:00:00 2001 From: Jim O'Donnell Date: Fri, 6 Dec 2024 10:40:35 +0000 Subject: [PATCH 3/3] Add explanatory comments (and questions!) --- Dockerfile | 40 ++++++++++++++++++++++++++++++++++++++++ 1 file changed, 40 insertions(+) diff --git a/Dockerfile b/Dockerfile index a1a641cf92..984ede64b6 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,45 +1,76 @@ +# A three-stage docker build, based on +# https://calvinf.com/blog/2023/11/10/node-js-20-yarn-4-and-next-js-on-docker/ + +############################ +# Stage 1: Base Node Alpine image for both the builder and runner stages FROM node:20-alpine AS base +# Latest Git commit hash ARG COMMIT_ID ENV COMMIT_ID=$COMMIT_ID ENV SENTRY_RELEASE=$COMMIT_ID +# GitHub branch name ARG GITHUB_REF_NAME ENV GITHUB_REF_NAME=$GITHUB_REF_NAME +# Panoptes API: staging | production (default) ARG PANOPTES_ENV=production ENV PANOPTES_ENV=$PANOPTES_ENV +# Webpack build mode: development | production (default) +# https://webpack.js.org/configuration/mode/ ARG NODE_ENV=production ENV NODE_ENV=$NODE_ENV +# container deployment environment: development | branch | staging | production (default) ARG APP_ENV=production ENV APP_ENV=$APP_ENV +# disable Next.js telemetry +# https://nextjs.org/telemetry ENV NEXT_TELEMETRY_DISABLED=1 +# Install everything into /usr/src. RUN mkdir -p /usr/src WORKDIR /usr/src/ +# Change the user and group from root to node. RUN chown -R node:node . +# Copy the package file, lock file, Yarn config, and lerna config +# into any image built from this image. ONBUILD COPY --chown=node:node package.json yarn.lock lerna.json .yarnrc /usr/src/ +# Copy the .yarn directory into any image built from this image. ONBUILD COPY --chown=node:node .yarn /usr/src/.yarn +# Run all commands as the node user, in any image built from this image. ONBUILD USER node +######################### +# Stage 2: Builder stage FROM base AS builder +# Add build secrets. +# NB. GitHub warns that ARG should not be used for sensitive data. ARG CONTENTFUL_ACCESS_TOKEN ARG CONTENTFUL_SPACE_ID ARG SENTRY_AUTH_TOKEN +# Copy the source packages into the image. COPY --chown=node:node ./packages /usr/src/packages +# Install dependencies and bootstrap the Next.js apps. +# The monorepo packages depend on each other, +# so must be built in a specific order. +# Set YARN_CACHE_FOLDER=/home/node/.yarn. +# Cache the Yarn cache as fem-builder-yarn across builds. +# (Why is this repeated for every layer?) +# https://classic.yarnpkg.com/lang/en/docs/cli/cache/#toc-change-the-cache-path-for-yarn RUN --mount=type=cache,id=fem-builder-yarn,uid=1000,gid=1000,target=/home/node/.yarn YARN_CACHE_FOLDER=/home/node/.yarn yarn install --production=false --frozen-lockfile --ignore-scripts RUN --mount=type=cache,id=fem-builder-yarn,uid=1000,gid=1000,target=/home/node/.yarn YARN_CACHE_FOLDER=/home/node/.yarn yarn workspace @zooniverse/react-components build:es6 RUN --mount=type=cache,id=fem-builder-yarn,uid=1000,gid=1000,target=/home/node/.yarn YARN_CACHE_FOLDER=/home/node/.yarn yarn workspace @zooniverse/subject-viewers build:es6 @@ -51,12 +82,21 @@ RUN --mount=type=cache,id=fem-builder-yarn,uid=1000,gid=1000,target=/home/node/. RUN echo $COMMIT_ID > /usr/src/packages/app-root/public/commit_id.txt RUN --mount=type=cache,id=fem-builder-yarn,uid=1000,gid=1000,target=/home/node/.yarn YARN_CACHE_FOLDER=/home/node/.yarn yarn workspace @zooniverse/fe-root build +######################### +# Stage 3: Runner stage +# This image is deployed to Kubernetes. FROM base AS runner +# Copy the built packages into the image. COPY --from=builder --chown=node:node /usr/src/packages ./packages +# Install only production dependencies. +# Cache the Yarn cache as fem-runner-yarn across builds. RUN --mount=type=cache,id=fem-runner-yarn,uid=1000,gid=1000,target=/home/node/.yarn YARN_CACHE_FOLDER=/home/node/.yarn yarn install --production --frozen-lockfile --ignore-scripts --prefer-offline +# Remove all the source code. +# Any other files that aren't needed to run the Next.js apps +# should be removed here too. RUN rm -rf /usr/src/packages/lib-react-components/src RUN rm -rf /usr/src/packages/lib-content/src RUN rm -rf /usr/src/packages/lib-classifier/src