From 24be5bf0f1fdc101ceeff90253a80b8872a3ef7a Mon Sep 17 00:00:00 2001 From: Kris McGinnes Date: Wed, 14 Oct 2026 18:21:10 -0500 Subject: [PATCH 1/2] Split the Docker build into build, deps, and runtime stages --- .github/workflows/test_build_docker.yml | 43 +++++++++-------- Dockerfile | 61 ++++++++++++++----------- 2 files changed, 58 insertions(+), 46 deletions(-) diff --git a/.github/workflows/test_build_docker.yml b/.github/workflows/test_build_docker.yml index ff946b617f..3d6da94f6b 100644 --- a/.github/workflows/test_build_docker.yml +++ b/.github/workflows/test_build_docker.yml @@ -52,25 +52,28 @@ jobs: (command -v dnf && echo "FAIL: dnf found" && exit 1) || echo "✓ dnf removed" ' + # One run per way the entrypoint starts the server: plain HTTP on 80, + # notebook mode on 9250, and HTTPS on 443 with freshly generated certs. - name: Verify server starts and responds run: | - docker run -d --name test-server \ - -p 8080:80 \ - -e PROXY_SERVER_HTTPS_CONNECTION=false \ - test-image - trap 'docker rm -f test-server 2>/dev/null' EXIT - - # Wait for the server to be ready - for i in $(seq 1 30); do - if curl -sf http://localhost:8080/status; then - echo "" - echo "✓ Server responded on /status" - break - fi - if [ "$i" -eq 30 ]; then - echo "FAIL: Server did not respond within 30 seconds" - docker logs test-server - exit 1 - fi - sleep 1 - done + check() { + name=$1 host_port=$2 container_port=$3 scheme=$4 + shift 4 + docker run -d --name "$name" -p "$host_port:$container_port" "$@" test-image + for i in $(seq 1 30); do + if curl -sfk "$scheme://localhost:$host_port/status"; then + echo "" + echo "✓ $name responded on /status" + docker rm -f "$name" >/dev/null + return 0 + fi + sleep 1 + done + echo "FAIL: $name did not respond within 30 seconds" + docker logs "$name" + docker rm -f "$name" >/dev/null + return 1 + } + check http 8080 80 http -e PROXY_SERVER_HTTPS_CONNECTION=false + check notebook 9250 9250 http -e NEPTUNE_NOTEBOOK=true + check https 8443 443 https -e HOST=localhost diff --git a/Dockerfile b/Dockerfile index eaf0458368..a341bb25ef 100644 --- a/Dockerfile +++ b/Dockerfile @@ -2,48 +2,57 @@ # Pinned to a dated ECR Public snapshot for reproducible builds; bump to move forward. # The tag also pins dnf repo metadata to that date, so the yum update below resolves # against that snapshot and cannot reach anything published after it. -FROM public.ecr.aws/amazonlinux/amazonlinux:2023.12.20261013.0 AS base +FROM public.ecr.aws/amazonlinux/amazonlinux:2023.12.20261013.0 AS os ENV NODE_VERSION=24.21.0 -# Install Node.js and openssl, then remove everything not needed at runtime -# (package managers, python3, build tools) to minimize potential issues. +# Node.js and pnpm for the build stages. The runtime copies only the node binary. +FROM os AS toolchain RUN yum update -y && \ - yum install -y tar xz openssl && \ + yum install -y tar xz && \ ARCH=$(uname -m) && \ if [ "$ARCH" = "x86_64" ]; then NODE_ARCH="x64"; \ elif [ "$ARCH" = "aarch64" ]; then NODE_ARCH="arm64"; \ else echo "Unsupported architecture: $ARCH" && exit 1; fi && \ curl -fsSL https://nodejs.org/dist/v${NODE_VERSION}/node-v${NODE_VERSION}-linux-${NODE_ARCH}.tar.xz | tar -xJ -C /usr/local --strip-components=1 && \ npm install --global corepack@0.36.0 && \ - corepack enable && \ - yum remove -y tar xz && \ + corepack enable +WORKDIR /graph-explorer + +FROM toolchain AS build +# The image has no git, so skip installing the Vite+ commit hooks. +ENV VP_GIT_HOOKS=0 +COPY . . +RUN pnpm install --frozen-lockfile && \ + pnpm build && \ + pnpm clean:dep && \ + chmod a+x ./process-environment.sh ./setup-ssl.sh ./docker-entrypoint.sh + +# Installed from the manifests alone, so the layer is reused until a +# dependency changes and never holds devDependencies. +FROM toolchain AS deps +COPY package.json pnpm-lock.yaml pnpm-workspace.yaml .npmrc ./ +COPY packages/graph-explorer/package.json packages/graph-explorer/ +COPY packages/graph-explorer-proxy-server/package.json packages/graph-explorer-proxy-server/ +COPY packages/shared/package.json packages/shared/ +RUN pnpm install --prod --frozen-lockfile --ignore-scripts + +# openssl is for setup-ssl.sh. Package managers and python3 are removed to keep +# the runtime to what it runs. +FROM os AS runtime +RUN yum update -y && \ + yum install -y openssl && \ yum clean all && \ rpm -qa 'python3*' | while read pkg; do rpm -e --nodeps "$pkg"; done && \ rpm -qa 'dnf*' 'yum*' 'libdnf*' | while read pkg; do rpm -e --nodeps "$pkg"; done && \ rm -rf /var/cache/yum /var/cache/dnf - -FROM base +COPY --from=toolchain /usr/local/bin/node /usr/local/bin/node ENV HOME=/graph-explorer -# The image has no git, so skip installing the Vite+ commit hooks. -ENV VP_GIT_HOOKS=0 - -WORKDIR / -COPY . /graph-explorer/ WORKDIR /graph-explorer - -RUN pnpm install --frozen-lockfile && \ - pnpm build && \ - pnpm clean:dep && \ - pnpm install --prod --frozen-lockfile --ignore-scripts && \ - npm uninstall -g npm && \ - corepack disable && \ - rm -rf /usr/local/bin/pnpm* /usr/local/bin/corepack && \ - rm -rf $HOME/.local && \ - rm -rf $HOME/.cache && \ - chmod a+x ./process-environment.sh && \ - chmod a+x ./setup-ssl.sh && \ - chmod a+x ./docker-entrypoint.sh +COPY --from=build /graph-explorer ./ +COPY --from=deps /graph-explorer/node_modules ./node_modules +COPY --from=deps /graph-explorer/packages/graph-explorer/node_modules ./packages/graph-explorer/node_modules +COPY --from=deps /graph-explorer/packages/graph-explorer-proxy-server/node_modules ./packages/graph-explorer-proxy-server/node_modules EXPOSE 443 EXPOSE 80 From 680015e47c651b550d337f763cbaab721e31c5a1 Mon Sep 17 00:00:00 2001 From: Kris McGinnes Date: Wed, 14 Oct 2026 18:47:47 -0500 Subject: [PATCH 2/2] Ship Node's license and copy production deps in one step --- .github/workflows/test_build_docker.yml | 17 +++++++++-------- Dockerfile | 10 +++++----- 2 files changed, 14 insertions(+), 13 deletions(-) diff --git a/.github/workflows/test_build_docker.yml b/.github/workflows/test_build_docker.yml index 3d6da94f6b..0bf28ba462 100644 --- a/.github/workflows/test_build_docker.yml +++ b/.github/workflows/test_build_docker.yml @@ -41,19 +41,20 @@ jobs: run: | docker run --rm --entrypoint="" test-image openssl --version - - name: Verify unnecessary packages are removed + - name: Verify build tooling is absent run: | docker run --rm --entrypoint="" test-image sh -c ' - (command -v npm && echo "FAIL: npm found" && exit 1) || echo "✓ npm removed" - (command -v pnpm && echo "FAIL: pnpm found" && exit 1) || echo "✓ pnpm removed" - (command -v corepack && echo "FAIL: corepack found" && exit 1) || echo "✓ corepack removed" - (command -v python3 && echo "FAIL: python3 found" && exit 1) || echo "✓ python3 removed" - (command -v yum && echo "FAIL: yum found" && exit 1) || echo "✓ yum removed" - (command -v dnf && echo "FAIL: dnf found" && exit 1) || echo "✓ dnf removed" + (command -v npm && echo "FAIL: npm found" && exit 1) || echo "✓ npm absent" + (command -v pnpm && echo "FAIL: pnpm found" && exit 1) || echo "✓ pnpm absent" + (command -v corepack && echo "FAIL: corepack found" && exit 1) || echo "✓ corepack absent" + (command -v python3 && echo "FAIL: python3 found" && exit 1) || echo "✓ python3 absent" + (command -v yum && echo "FAIL: yum found" && exit 1) || echo "✓ yum absent" + (command -v dnf && echo "FAIL: dnf found" && exit 1) || echo "✓ dnf absent" ' # One run per way the entrypoint starts the server: plain HTTP on 80, - # notebook mode on 9250, and HTTPS on 443 with freshly generated certs. + # notebook mode on 9250, and HTTPS on 443, where HOST makes the entrypoint + # generate a self-signed certificate. - name: Verify server starts and responds run: | check() { diff --git a/Dockerfile b/Dockerfile index a341bb25ef..6e715bc17e 100644 --- a/Dockerfile +++ b/Dockerfile @@ -36,8 +36,8 @@ COPY packages/graph-explorer-proxy-server/package.json packages/graph-explorer-p COPY packages/shared/package.json packages/shared/ RUN pnpm install --prod --frozen-lockfile --ignore-scripts -# openssl is for setup-ssl.sh. Package managers and python3 are removed to keep -# the runtime to what it runs. +# openssl is for setup-ssl.sh. yum, dnf, and python3 come with the base image +# and are stripped; npm, pnpm, and corepack never reach this stage. FROM os AS runtime RUN yum update -y && \ yum install -y openssl && \ @@ -45,14 +45,14 @@ RUN yum update -y && \ rpm -qa 'python3*' | while read pkg; do rpm -e --nodeps "$pkg"; done && \ rpm -qa 'dnf*' 'yum*' 'libdnf*' | while read pkg; do rpm -e --nodeps "$pkg"; done && \ rm -rf /var/cache/yum /var/cache/dnf +# LICENSE carries the notices for the third-party code built into node. COPY --from=toolchain /usr/local/bin/node /usr/local/bin/node +COPY --from=toolchain /usr/local/LICENSE /usr/local/LICENSE ENV HOME=/graph-explorer WORKDIR /graph-explorer +COPY --from=deps /graph-explorer ./ COPY --from=build /graph-explorer ./ -COPY --from=deps /graph-explorer/node_modules ./node_modules -COPY --from=deps /graph-explorer/packages/graph-explorer/node_modules ./packages/graph-explorer/node_modules -COPY --from=deps /graph-explorer/packages/graph-explorer-proxy-server/node_modules ./packages/graph-explorer-proxy-server/node_modules EXPOSE 443 EXPOSE 80