Skip to content

Commit

Permalink
Shave a minute off of Docker image build (#54478)
Browse files Browse the repository at this point in the history
  • Loading branch information
heiskr authored Feb 20, 2025
1 parent ae3589c commit eed068d
Show file tree
Hide file tree
Showing 6 changed files with 86 additions and 172 deletions.
3 changes: 1 addition & 2 deletions .dockerignore
Original file line number Diff line number Diff line change
Expand Up @@ -8,5 +8,4 @@ node_modules/
tests/
# Folder is cloned during the preview + prod workflows, the assets are merged into other locations for use before the build
docs-early-access/
# During the preview deploy untrusted user code may be cloned into this directory
user-code/
README.md
3 changes: 0 additions & 3 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -27,9 +27,6 @@ broken_links.md
# still have these files on their disk.
lib/redirects/.redirects-cache*.json

# During the preview deploy untrusted user code may be cloned into this directory
# We ignore it from git to keep things deterministic
user-code/

# Logs from scripts
*/logs/
Expand Down
197 changes: 83 additions & 114 deletions Dockerfile
Original file line number Diff line number Diff line change
@@ -1,162 +1,131 @@
# This Dockerfile is used solely for production deployments to Moda
# For staging deployments, see src/deployments/staging/Dockerfile
# For building this file locally, see src/deployments/production/README.md
# Environment variables are set in the Moda configuration:
# config/moda/configuration/*/env.yaml

# --------------------------------------------------------------------------------
# BASE IMAGE
# --------------------------------------------------------------------------------
# ---------------------------------------------------------------
# BASE STAGE: Install linux dependencies and set up the node user
# ---------------------------------------------------------------
# To update the sha:
# https://github.com/github/gh-base-image/pkgs/container/gh-base-image%2Fgh-base-noble
FROM ghcr.io/github/gh-base-image/gh-base-noble:20250131-172559-g0fd5a2edc AS base

# Install curl for Node install and determining the early access branch
# Install git for cloning docs-early-access & translations repos
# Install curl for determining the early access branch
RUN apt-get -qq update && apt-get -qq install --no-install-recommends git curl

# Install Node.js latest LTS
# https://github.com/nodejs/release#release-schedule
# Ubuntu's apt-get install nodejs is _very_ outdated
RUN curl -sL https://deb.nodesource.com/setup_22.x | bash -
RUN apt-get install -y nodejs
RUN node --version

# This directory is owned by the node user
RUN useradd -ms /bin/bash node
ARG APP_HOME=/home/node/app
RUN mkdir -p $APP_HOME && chown -R node:node $APP_HOME
# Must run as root
RUN apt-get -qq update && apt-get -qq install --no-install-recommends curl git \
&& curl -sL https://deb.nodesource.com/setup_22.x | bash - \
&& apt-get install -y nodejs \
&& node --version

# Create the node user and home directory
ARG APP_HOME="/home/node/app" # Define in base so all child stages inherit it
RUN useradd -ms /bin/bash node \
&& mkdir -p $APP_HOME && chown -R node:node $APP_HOME

# -----------------------------------------------------------------
# CLONES STAGE: Clone docs-internal, early-access, and translations
# -----------------------------------------------------------------
FROM base AS clones
USER node:node
WORKDIR $APP_HOME

# Switch to root to ensure we have permissions to copy, chmod, and install
USER root

# Copy in build scripts
COPY src/deployments/production/build-scripts/*.sh ./build-scripts/

# Make scripts executable
RUN chmod +x build-scripts/*.sh

# We need to copy over content that will be merged with early-access
COPY content ./content
COPY assets ./assets
COPY data ./data
COPY --chown=node:node content content/
COPY --chown=node:node assets assets/
COPY --chown=node:node data data/

# Copy in build scripts and make them executable
COPY --chown=node:node --chmod=+x \
src/deployments/production/build-scripts/*.sh build-scripts/

# Use the mounted --secret to:
# - 1. Fetch the docs-internal repo
# - 2. Fetch the docs-early-access repo & override docs-internal with early access content
# - 3. Fetch each translations repo to the repo/translations directory
# We use --mount-type=secret to avoid the secret being copied into the image layers for security
# The secret passed via --secret can only be used in this RUN command
RUN --mount=type=secret,id=DOCS_BOT_PAT_READPUBLICKEY \
RUN --mount=type=secret,id=DOCS_BOT_PAT_READPUBLICKEY,mode=0444 \
# We don't cache because Docker can't know if we need to fetch new content from remote repos
echo "Don't cache this step by printing date: $(date)" && \
. ./build-scripts/fetch-repos.sh

# Give node user access to the copied content since we cloned as root
RUN chown -R node:node $APP_HOME/content
RUN chown -R node:node $APP_HOME/assets
RUN chown -R node:node $APP_HOME/data
# Give node user access to translations repos
RUN chown -R node:node $APP_HOME/translations

# Change back to node to make sure we don't run anything as the root user
USER node

# ---------------
# ALL DEPS Image
# ---------------
FROM base AS all_deps

ARG APP_HOME=/home/node/app
USER node
# -----------------------------------------
# DEPENDENCIES STAGE: Install node packages
# -----------------------------------------
FROM base AS dependencies
USER node:node
WORKDIR $APP_HOME

# Copy what is needed to run npm ci
COPY --chown=node:node package.json package-lock.json ./

RUN npm ci --no-optional --registry https://registry.npmjs.org/
RUN npm ci --omit=optional --registry https://registry.npmjs.org/

# ---------------
# BUILDER Image
# ---------------
FROM all_deps AS builder

ARG APP_HOME=/home/node/app
USER node
# -----------------------------------------
# BUILD STAGE: Prepare for production stage
# -----------------------------------------
FROM base AS build
USER node:node
WORKDIR $APP_HOME

# Copy what is needed to:
# 1. Build the app
# 2. run warmup-remotejson script
# 3. run precompute-pageinfo script
# Dependencies
COPY --chown=node:node --from=all_deps $APP_HOME/node_modules $APP_HOME/node_modules
# Content with merged early-access content
COPY --chown=node:node --from=base $APP_HOME/data ./data
COPY --chown=node:node --from=base $APP_HOME/assets ./assets
COPY --chown=node:node --from=base $APP_HOME/content ./content
# Source code
COPY --chown=node:node --from=all_deps $APP_HOME/package.json ./
COPY src ./src
COPY next.config.js ./
COPY tsconfig.json ./

# 1. Build
RUN npm run build

# 2. Warm up the remotejson cache
RUN npm run warmup-remotejson

# 3. Precompute the pageinfo cache
RUN npm run precompute-pageinfo -- --max-versions 2

# Prune deps for prod image
RUN npm prune --production

# --------------------------------------------------------------------------------
# PRODUCTION IMAGE
# --------------------------------------------------------------------------------
COPY --chown=node:node src src/
COPY --chown=node:node package.json ./
COPY --chown=node:node next.config.js ./
COPY --chown=node:node tsconfig.json ./

# From the clones stage
COPY --chown=node:node --from=clones $APP_HOME/data data/
COPY --chown=node:node --from=clones $APP_HOME/assets assets/
COPY --chown=node:node --from=clones $APP_HOME/content content/
COPY --chown=node:node --from=clones $APP_HOME/translations translations/

# From the dependencies stage
COPY --chown=node:node --from=dependencies $APP_HOME/node_modules node_modules/

# Generate build files
RUN npm run build \
&& npm run warmup-remotejson \
&& npm run precompute-pageinfo -- --max-versions 2 \
&& npm prune --production

# -------------------------------------------------
# PRODUCTION STAGE: What will run on the containers
# -------------------------------------------------
FROM base AS production

ARG APP_HOME=/home/node/app
USER node
USER node:node
WORKDIR $APP_HOME

# Copy the content with merged early-access content
COPY --chown=node:node --from=base $APP_HOME/data ./data
COPY --chown=node:node --from=base $APP_HOME/assets ./assets
COPY --chown=node:node --from=base $APP_HOME/content ./content

# Include cloned translations
COPY --chown=node:node --from=base $APP_HOME/translations ./translations

# Copy prod dependencies
COPY --chown=node:node --from=builder $APP_HOME/package.json ./
COPY --chown=node:node --from=builder $APP_HOME/node_modules $APP_HOME/node_modules

# Copy built artifacts needed at runtime for the server
COPY --chown=node:node --from=builder $APP_HOME/.next $APP_HOME/.next
# Source code
COPY --chown=node:node src src/
COPY --chown=node:node package.json ./
COPY --chown=node:node next.config.js ./
COPY --chown=node:node tsconfig.json ./

# Copy cache files generated during build scripts
COPY --chown=node:node --from=builder $APP_HOME/.remotejson-cache ./.remotejson-cache
COPY --chown=node:node --from=builder $APP_HOME/.pageinfo-cache.json.br* ./.pageinfo-cache.json.br
# From clones stage
COPY --chown=node:node --from=clones $APP_HOME/data data/
COPY --chown=node:node --from=clones $APP_HOME/assets assets/
COPY --chown=node:node --from=clones $APP_HOME/content content/
COPY --chown=node:node --from=clones $APP_HOME/translations translations/

# Copy only what's needed to run the server
COPY --chown=node:node --from=builder $APP_HOME/src ./src
COPY --chown=node:node --from=builder $APP_HOME/.remotejson-cache ./.remotejson-cache
COPY --chown=node:node --from=builder $APP_HOME/.pageinfo-cache.json.br* ./.pageinfo-cache.json.br
COPY --chown=node:node --from=builder $APP_HOME/next.config.js ./
COPY --chown=node:node --from=builder $APP_HOME/tsconfig.json ./
# From dependencies stage (*modified in build stage)
COPY --chown=node:node --from=build $APP_HOME/node_modules node_modules/

# - - -
# Environment variables are set in the Moda
# configuration: config/moda/configuration/*/env.yaml
# - - -
# From build stage
COPY --chown=node:node --from=build $APP_HOME/.next .next/
COPY --chown=node:node --from=build $APP_HOME/.remotejson-cache ./
COPY --chown=node:node --from=build $APP_HOME/.pageinfo-cache.json.br* ./

# This makes it possible to set `--build-arg BUILD_SHA=abc123`
# and it then becomes available as an environment variable in the docker run.
ARG BUILD_SHA
ENV BUILD_SHA=$BUILD_SHA

# Entrypoint to start the server
# Note: Currently we have to use tsx because we have a mix of `.ts` and `.js` files with multiple import patterns
# Note: Currently we have to use tsx because
# we have a mix of `.ts` and `.js` files with multiple import patterns
CMD ["node_modules/.bin/tsx", "src/frame/server.ts"]
4 changes: 2 additions & 2 deletions contributing/deployments.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,9 +2,9 @@

Staging and production deployments are automated by a deployer service created and maintained by @github/docs-engineering.

### Preview deployments
### Review deployments

When a pull request contains only content changes, it can be previewed without a deployment. Code changes will require a deployment. GitHub Staff can deploy such a PR to a staging environment.
TBD

### Production deployments

Expand Down
45 changes: 0 additions & 45 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Loading

0 comments on commit eed068d

Please sign in to comment.