From 36640827b76c214734bf887e44a8afdc97accdfd Mon Sep 17 00:00:00 2001 From: Jamison Lahman Date: Wed, 12 Aug 2026 19:58:06 +0000 Subject: [PATCH] chore(docker): default web + model-server bases to non-DHI images (#13915) Co-authored-by: Claude Opus 5 --- .../build-model-server-image/action.yml | 24 +++--- .github/actions/dhi-base-images/action.yml | 56 +++++++++++++ .github/workflows/deployment.yml | 84 +++++++++---------- .github/workflows/pr-playwright-tests.yml | 16 ++-- CONTRIBUTING.md | 14 +--- backend/Dockerfile.model_server | 41 +++++---- web/Dockerfile | 33 +++++--- 7 files changed, 170 insertions(+), 98 deletions(-) create mode 100644 .github/actions/dhi-base-images/action.yml diff --git a/.github/actions/build-model-server-image/action.yml b/.github/actions/build-model-server-image/action.yml index 975a464f0b..3806c67a8e 100644 --- a/.github/actions/build-model-server-image/action.yml +++ b/.github/actions/build-model-server-image/action.yml @@ -28,11 +28,15 @@ inputs: description: "ECR registry host for the Docker Hub pull-through cache (the ECR_REGISTRY repo variable)" required: true docker-username: - description: "Docker Hub username (must have access to the DHI catalog on dhi.io)" - required: true + description: >- + Docker Hub username with DHI catalog access. Optional -- without it the image + builds on its public base defaults instead of the hardened ones. + required: false + default: "" docker-token: description: "Docker Hub token" - required: true + required: false + default: "" runs: using: "composite" steps: @@ -59,14 +63,13 @@ runs: with: ecr-registry: ${{ inputs.ecr-registry }} - # Dockerfile.model_server pulls its hardened Python base from dhi.io, authenticated - # with the same Docker account credentials (the account must have DHI catalog access). - - name: Login to Docker Hardened Images (dhi.io) - uses: docker/login-action@c99871dec2022cc055c062a10cc1a1310835ceb4 + # Dockerfile.model_server defaults to the public Python bases. CI builds ship on the + # hardened DHI equivalents, passed as build args below. + - name: Resolve Docker Hardened Image bases + uses: ./.github/actions/dhi-base-images with: - registry: dhi.io - username: ${{ inputs.docker-username }} - password: ${{ inputs.docker-token }} + docker-username: ${{ inputs.docker-username }} + docker-token: ${{ inputs.docker-token }} - name: Build and push Model Server Docker image uses: docker/build-push-action@f9f3042f7e2789586610d6e8b85c8f03e5195baf # ratchet:docker/build-push-action@v6 @@ -77,6 +80,7 @@ runs: platforms: ${{ inputs.platforms }} build-args: | BASE_IMAGE_REGISTRY=${{ env.BASE_IMAGE_REGISTRY }} + ${{ env.DHI_PYTHON_BUILD_ARGS }} tags: ${{ inputs.runs-on-ecr-cache }}:${{ inputs.tag-prefix }}-${{ inputs.run-id }} cache-from: | type=registry,ref=${{ inputs.runs-on-ecr-cache }}:model-server-cache-${{ inputs.github-sha }} diff --git a/.github/actions/dhi-base-images/action.yml b/.github/actions/dhi-base-images/action.yml new file mode 100644 index 0000000000..1bacff551e --- /dev/null +++ b/.github/actions/dhi-base-images/action.yml @@ -0,0 +1,56 @@ +name: "Docker Hardened Image bases" +description: >- + Logs in to dhi.io and exports the build args that point web/Dockerfile and + backend/Dockerfile.model_server at the pinned Docker Hardened Images (DHI). + Both Dockerfiles default to public Docker Hub images, so without DHI credentials + (fork pull requests) this action exports nothing and the defaults apply. +inputs: + docker-username: + description: "Docker Hub username (the account must have DHI catalog access)" + required: false + default: "" + docker-token: + description: "Docker Hub token" + required: false + default: "" +runs: + using: "composite" + steps: + - name: Check for DHI credentials + id: creds + shell: bash + env: + DOCKER_USERNAME: ${{ inputs.docker-username }} + DOCKER_TOKEN: ${{ inputs.docker-token }} + run: | + if [ -n "${DOCKER_USERNAME}" ] && [ -n "${DOCKER_TOKEN}" ]; then + echo "available=true" >> "$GITHUB_OUTPUT" + else + echo "available=false" >> "$GITHUB_OUTPUT" + echo "No DHI credentials; the images build on their public base defaults." + fi + + - name: Login to Docker Hardened Images (dhi.io) + if: steps.creds.outputs.available == 'true' + uses: docker/login-action@c99871dec2022cc055c062a10cc1a1310835ceb4 + with: + registry: dhi.io + username: ${{ inputs.docker-username }} + password: ${{ inputs.docker-token }} + + # Single source of truth for the DHI digests. Refresh one with: + # docker buildx imagetools inspect + - name: Export DHI build args + if: steps.creds.outputs.available == 'true' + shell: bash + run: | + { + echo "DHI_NODE_BUILD_ARGS<> "$GITHUB_ENV" diff --git a/.github/workflows/deployment.yml b/.github/workflows/deployment.yml index 3b3db477c5..93a533264f 100644 --- a/.github/workflows/deployment.yml +++ b/.github/workflows/deployment.yml @@ -535,14 +535,13 @@ jobs: username: ${{ env.DOCKER_USERNAME }} password: ${{ env.DOCKER_TOKEN }} - # web/Dockerfile pulls its hardened Node base from dhi.io, authenticated with the same - # Docker account credentials (the account must have access to the DHI catalog). - - name: Login to Docker Hardened Images (dhi.io) - uses: docker/login-action@c99871dec2022cc055c062a10cc1a1310835ceb4 + # web/Dockerfile defaults to the public Node bases. Release builds ship on the + # hardened DHI equivalents, passed as build args below. + - name: Resolve Docker Hardened Image bases + uses: ./.github/actions/dhi-base-images with: - registry: dhi.io - username: ${{ env.DOCKER_USERNAME }} - password: ${{ env.DOCKER_TOKEN }} + docker-username: ${{ env.DOCKER_USERNAME }} + docker-token: ${{ env.DOCKER_TOKEN }} - name: Build and push AMD64 id: build @@ -554,6 +553,7 @@ jobs: labels: ${{ steps.meta.outputs.labels }} build-args: | ONYX_VERSION=${{ github.ref_name }} + ${{ env.DHI_NODE_BUILD_ARGS }} NODE_OPTIONS=--max-old-space-size=8192 cache-from: | type=registry,ref=${{ env.REGISTRY_IMAGE }}:edge @@ -617,14 +617,13 @@ jobs: username: ${{ env.DOCKER_USERNAME }} password: ${{ env.DOCKER_TOKEN }} - # web/Dockerfile pulls its hardened Node base from dhi.io, authenticated with the same - # Docker account credentials (the account must have access to the DHI catalog). - - name: Login to Docker Hardened Images (dhi.io) - uses: docker/login-action@c99871dec2022cc055c062a10cc1a1310835ceb4 + # web/Dockerfile defaults to the public Node bases. Release builds ship on the + # hardened DHI equivalents, passed as build args below. + - name: Resolve Docker Hardened Image bases + uses: ./.github/actions/dhi-base-images with: - registry: dhi.io - username: ${{ env.DOCKER_USERNAME }} - password: ${{ env.DOCKER_TOKEN }} + docker-username: ${{ env.DOCKER_USERNAME }} + docker-token: ${{ env.DOCKER_TOKEN }} - name: Build and push ARM64 id: build @@ -636,6 +635,7 @@ jobs: labels: ${{ steps.meta.outputs.labels }} build-args: | ONYX_VERSION=${{ github.ref_name }} + ${{ env.DHI_NODE_BUILD_ARGS }} NODE_OPTIONS=--max-old-space-size=8192 cache-from: | type=registry,ref=${{ env.REGISTRY_IMAGE }}:edge @@ -791,14 +791,13 @@ jobs: username: ${{ env.DOCKER_USERNAME }} password: ${{ env.DOCKER_TOKEN }} - # web/Dockerfile pulls its hardened Node base from dhi.io, authenticated with the same - # Docker account credentials (the account must have access to the DHI catalog). - - name: Login to Docker Hardened Images (dhi.io) - uses: docker/login-action@c99871dec2022cc055c062a10cc1a1310835ceb4 + # web/Dockerfile defaults to the public Node bases. Release builds ship on the + # hardened DHI equivalents, passed as build args below. + - name: Resolve Docker Hardened Image bases + uses: ./.github/actions/dhi-base-images with: - registry: dhi.io - username: ${{ env.DOCKER_USERNAME }} - password: ${{ env.DOCKER_TOKEN }} + docker-username: ${{ env.DOCKER_USERNAME }} + docker-token: ${{ env.DOCKER_TOKEN }} - name: Build and push AMD64 id: build @@ -810,6 +809,7 @@ jobs: labels: ${{ steps.meta.outputs.labels }} build-args: | ONYX_VERSION=${{ github.ref_name }} + ${{ env.DHI_NODE_BUILD_ARGS }} NEXT_PUBLIC_CLOUD_ENABLED=true WEB_FRAME_PROTECTION_ENABLED=false NEXT_PUBLIC_POSTHOG_KEY=${{ secrets.POSTHOG_KEY }} @@ -884,14 +884,13 @@ jobs: username: ${{ env.DOCKER_USERNAME }} password: ${{ env.DOCKER_TOKEN }} - # web/Dockerfile pulls its hardened Node base from dhi.io, authenticated with the same - # Docker account credentials (the account must have access to the DHI catalog). - - name: Login to Docker Hardened Images (dhi.io) - uses: docker/login-action@c99871dec2022cc055c062a10cc1a1310835ceb4 + # web/Dockerfile defaults to the public Node bases. Release builds ship on the + # hardened DHI equivalents, passed as build args below. + - name: Resolve Docker Hardened Image bases + uses: ./.github/actions/dhi-base-images with: - registry: dhi.io - username: ${{ env.DOCKER_USERNAME }} - password: ${{ env.DOCKER_TOKEN }} + docker-username: ${{ env.DOCKER_USERNAME }} + docker-token: ${{ env.DOCKER_TOKEN }} - name: Build and push ARM64 id: build @@ -903,6 +902,7 @@ jobs: labels: ${{ steps.meta.outputs.labels }} build-args: | ONYX_VERSION=${{ github.ref_name }} + ${{ env.DHI_NODE_BUILD_ARGS }} NEXT_PUBLIC_CLOUD_ENABLED=true WEB_FRAME_PROTECTION_ENABLED=false NEXT_PUBLIC_POSTHOG_KEY=${{ secrets.POSTHOG_KEY }} @@ -1351,14 +1351,13 @@ jobs: username: ${{ env.DOCKER_USERNAME }} password: ${{ env.DOCKER_TOKEN }} - # Dockerfile.model_server pulls its hardened Python base from dhi.io, authenticated - # with the same Docker account credentials (the account must have DHI catalog access). - - name: Login to Docker Hardened Images (dhi.io) - uses: docker/login-action@c99871dec2022cc055c062a10cc1a1310835ceb4 + # Dockerfile.model_server defaults to the public Python bases. Release builds ship + # on the hardened DHI equivalents, passed as build args below. + - name: Resolve Docker Hardened Image bases + uses: ./.github/actions/dhi-base-images with: - registry: dhi.io - username: ${{ env.DOCKER_USERNAME }} - password: ${{ env.DOCKER_TOKEN }} + docker-username: ${{ env.DOCKER_USERNAME }} + docker-token: ${{ env.DOCKER_TOKEN }} - name: Build and push AMD64 id: build @@ -1372,6 +1371,7 @@ jobs: labels: ${{ steps.meta.outputs.labels }} build-args: | ONYX_VERSION=${{ github.ref_name }} + ${{ env.DHI_PYTHON_BUILD_ARGS }} cache-from: | type=registry,ref=${{ env.REGISTRY_IMAGE }}:edge type=registry,ref=${{ env.REGISTRY_IMAGE }}:latest @@ -1437,14 +1437,13 @@ jobs: username: ${{ env.DOCKER_USERNAME }} password: ${{ env.DOCKER_TOKEN }} - # Dockerfile.model_server pulls its hardened Python base from dhi.io, authenticated - # with the same Docker account credentials (the account must have DHI catalog access). - - name: Login to Docker Hardened Images (dhi.io) - uses: docker/login-action@c99871dec2022cc055c062a10cc1a1310835ceb4 + # Dockerfile.model_server defaults to the public Python bases. Release builds ship + # on the hardened DHI equivalents, passed as build args below. + - name: Resolve Docker Hardened Image bases + uses: ./.github/actions/dhi-base-images with: - registry: dhi.io - username: ${{ env.DOCKER_USERNAME }} - password: ${{ env.DOCKER_TOKEN }} + docker-username: ${{ env.DOCKER_USERNAME }} + docker-token: ${{ env.DOCKER_TOKEN }} - name: Build and push ARM64 id: build @@ -1458,6 +1457,7 @@ jobs: labels: ${{ steps.meta.outputs.labels }} build-args: | ONYX_VERSION=${{ github.ref_name }} + ${{ env.DHI_PYTHON_BUILD_ARGS }} cache-from: | type=registry,ref=${{ env.REGISTRY_IMAGE }}:edge type=registry,ref=${{ env.REGISTRY_IMAGE }}:latest diff --git a/.github/workflows/pr-playwright-tests.yml b/.github/workflows/pr-playwright-tests.yml index fb7f9581b3..ed3b1e576c 100644 --- a/.github/workflows/pr-playwright-tests.yml +++ b/.github/workflows/pr-playwright-tests.yml @@ -120,6 +120,7 @@ jobs: - '.github/workflows/pr-playwright-tests.yml' - '.github/actions/setup-test-license/**' - '.github/actions/login-ecr-pullthrough-cache/**' + - '.github/actions/dhi-base-images/**' airgap: - 'backend/Dockerfile' - 'backend/Dockerfile.model_server' @@ -147,6 +148,7 @@ jobs: - '.github/workflows/pr-playwright-tests.yml' - '.github/actions/build-model-server-image/**' - '.github/actions/login-ecr-pullthrough-cache/**' + - '.github/actions/dhi-base-images/**' mcp_oauth: - 'backend/onyx/server/features/mcp/**' - 'backend/tests/integration/mock_services/mcp_test_server/**' @@ -196,14 +198,13 @@ jobs: with: ecr-registry: ${{ vars.ECR_REGISTRY }} - # web/Dockerfile pulls its hardened Node base from dhi.io, authenticated with the same - # Docker account credentials (the account must have access to the DHI catalog). - - name: Login to Docker Hardened Images (dhi.io) - uses: docker/login-action@c99871dec2022cc055c062a10cc1a1310835ceb4 + # web/Dockerfile defaults to the public Node bases. CI builds ship on the hardened + # DHI equivalents, passed as build args below. + - name: Resolve Docker Hardened Image bases + uses: ./.github/actions/dhi-base-images with: - registry: dhi.io - username: ${{ secrets.DOCKER_USERNAME }} - password: ${{ secrets.DOCKER_TOKEN }} + docker-username: ${{ secrets.DOCKER_USERNAME }} + docker-token: ${{ secrets.DOCKER_TOKEN }} # SKIP_TYPE_CHECK cuts the build time of this image. Types are still checked # by the `typescript-check` prek hook in the Quality Checks PR workflow. @@ -217,6 +218,7 @@ jobs: push: true build-args: | BASE_IMAGE_REGISTRY=${{ env.BASE_IMAGE_REGISTRY }} + ${{ env.DHI_NODE_BUILD_ARGS }} SKIP_TYPE_CHECK=1 cache-from: | type=registry,ref=${{ env.RUNS_ON_ECR_CACHE }}:web-cache-${{ github.event.pull_request.head.sha || github.sha }} diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 589b393477..597df73568 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -306,16 +306,10 @@ If you want to make changes to Onyx and run those changes in Docker, you can als docker compose up -d --build ``` -> **Note:** Building the web image (`web/Dockerfile`) and the model-server image -> (`backend/Dockerfile.model_server`) pulls their bases from Docker Hardened Images (`dhi.io`), -> so you must authenticate first with a Docker account that has access to the DHI catalog: -> -> ```bash -> docker login dhi.io -> ``` -> -> Pulling the pre-built `onyxdotapp/onyx-web-server` / `onyxdotapp/onyx-model-server` images -> (the default `docker compose up -d` without `--build`) does not require this. +> **Note:** Local builds use the public Docker Hub base images, so they need no extra +> registry access. Our release builds override the base images with the Docker Hardened +> Image (`dhi.io`) equivalents, so the published `onyxdotapp/onyx-web-server` and +> `onyxdotapp/onyx-model-server` images differ from a local `--build` in their base layers. > **Note:** `docker-compose.yml`, `docker-compose.prod.yml` and > `docker-compose.prod-no-letsencrypt.yml` are generated from `docker-compose.template.yml` diff --git a/backend/Dockerfile.model_server b/backend/Dockerfile.model_server index 6f888d67b0..39e75947dc 100644 --- a/backend/Dockerfile.model_server +++ b/backend/Dockerfile.model_server @@ -1,13 +1,18 @@ -# Registry prefix retained for parity with the other Dockerfiles and so the -# BASE_IMAGE_REGISTRY build-arg (passed by docker-bake.hcl / CI) stays valid. -# The hardened Python bases below come from dhi.io -- a separate registry not -# served by the ECR pull-through cache -- so they intentionally bypass this ARG. +# Registry prefix for the base images below. Defaults to Docker Hub; CI overrides it to +# the ECR pull-through cache to dodge rate limits. It only applies to the default images +# -- the DHI overrides below carry their own registry (dhi.io), which the cache does not serve. ARG BASE_IMAGE_REGISTRY=docker.io -# Build stage. The DHI "-dev" variant runs as root and ships a shell, apt, and -# build tooling -- everything needed to install the wheels. -# Refresh the digest with: docker buildx imagetools inspect dhi.io/python:3.13-debian13-dev -FROM dhi.io/python:3.13-debian13-dev@sha256:7933d16e50454c39bca6e935027166d5e5b05c6c03383dc2f58e8fe6e2440b7d AS builder +# Python bases. The defaults are the public slim images, so a plain `docker build` needs no +# extra registry access. CI overrides both with the matching Docker Hardened Images from +# dhi.io, which need a Docker account with DHI catalog access. +# Refresh a digest with: docker buildx imagetools inspect +ARG PYTHON_BUILDER_IMAGE=${BASE_IMAGE_REGISTRY}/library/python:3.13-slim@sha256:b04b5d7233d2ad9c379e22ea8927cd1378cd15c60d4ef876c065b25ea8fb3bf3 +ARG PYTHON_RUNTIME_IMAGE=${BASE_IMAGE_REGISTRY}/library/python:3.13-slim@sha256:b04b5d7233d2ad9c379e22ea8927cd1378cd15c60d4ef876c065b25ea8fb3bf3 + +# Build stage. Needs a root user plus a shell and build tooling to install the wheels, +# which both the default slim image and the DHI "-dev" variant provide. +FROM ${PYTHON_BUILDER_IMAGE} AS builder ENV ONYX_RUNNING_IN_DOCKER="true" \ HF_HOME=/app/.cache/huggingface \ @@ -19,11 +24,11 @@ ENV ONYX_RUNNING_IN_DOCKER="true" \ COPY --from=ghcr.io/astral-sh/uv:0.11.25@sha256:1e3808aa9023d0980e7c15b1fa7c1ac16ff35925780cf5c459858b2d693f01a9 /uv /uvx /bin/ # Install into a self-contained venv rather than the system site-packages. The -# venv lives at a fixed path we control (/app/.venv), so the distroless runtime -# stage can copy it wholesale without depending on the base image's Python layout. +# venv lives at a fixed path we control (/app/.venv), so the runtime stage can +# copy it wholesale without depending on the base image's Python layout. RUN uv venv /app/.venv --python 3.13 -# Pre-create the runtime writable dirs here (the distroless runtime has no shell +# Pre-create the runtime writable dirs here (the DHI runtime has no shell # to mkdir). Left empty in this stage; the model download happens downstream. RUN mkdir -p /app/.cache/huggingface /var/log/onyx @@ -31,7 +36,7 @@ RUN mkdir -p /app/.cache/huggingface /var/log/onyx # pin `-u onyx` / `user: onyx` keep resolving and their 1001-owned volumes stay # writable. The DHI "-dev" image ships a shell but not the `shadow` tools # (groupadd/useradd), so write the passwd/group entries directly; they're copied -# into the distroless runtime below. +# into the runtime stage below. RUN printf 'onyx:x:1001:\n' >> /etc/group && \ printf 'onyx:x:1001:1001::/home/onyx:/usr/sbin/nologin\n' >> /etc/passwd @@ -55,11 +60,11 @@ FROM builder AS embedding-models RUN python -c "from sentence_transformers import SentenceTransformer; \ SentenceTransformer(model_name_or_path='nomic-ai/nomic-embed-text-v1', trust_remote_code=False);" -# Runtime stage. The DHI runtime variant is near-distroless: no shell or package -# manager, runs non-root. We run as the `onyx` user (UID 1001, carried over from -# the previous image; see USER below) and chown everything the runtime writes to it. -# Refresh the digest with: docker buildx imagetools inspect dhi.io/python:3.13-debian13 -FROM dhi.io/python:3.13-debian13@sha256:05827957dafc7b83633d56f24d9281525d3546a1d600eef90385c7212d3def5e AS final +# Runtime stage. We run as the `onyx` user (UID 1001, carried over from the previous +# image; see USER below) and chown everything the runtime writes to it. The DHI runtime +# variant is near-distroless: no shell or package manager, so keep this stage free of +# RUN instructions. +FROM ${PYTHON_RUNTIME_IMAGE} AS final LABEL com.danswer.maintainer="founders@onyx.app" LABEL com.danswer.description="This image is for the Onyx model server which runs all of the \ @@ -76,7 +81,7 @@ ENV ONYX_RUNNING_IN_DOCKER="true" \ VIRTUAL_ENV=/app/.venv \ PATH="/app/.venv/bin:$PATH" -# Bring the `onyx` user (1001) into the distroless runtime. The -dev builder's +# Bring the `onyx` user (1001) into the runtime stage. The -dev builder's # passwd/group are a superset of the runtime's, so copying them preserves the # base's own entries while adding `onyx`, letting `USER onyx` and any runtime # `-u onyx` override resolve the name. diff --git a/web/Dockerfile b/web/Dockerfile index cfa15db764..a621071913 100644 --- a/web/Dockerfile +++ b/web/Dockerfile @@ -1,15 +1,22 @@ -# Registry prefix for the bun binary source below. Defaults to Docker Hub; CI overrides it to -# the ECR pull-through cache to dodge rate limits. Applies only to `bun_source` -- the DHI Node -# bases come from dhi.io, a separate registry not served by the cache (see the FROM lines below). +# Registry prefix for the base images below. Defaults to Docker Hub; CI overrides it to the +# ECR pull-through cache to dodge rate limits. It only applies to the default images -- the +# DHI overrides below carry their own registry (dhi.io), which the cache does not serve. ARG BASE_IMAGE_REGISTRY=docker.io +# Node bases. The defaults are the public Debian slim images, so a plain `docker build` needs +# no extra registry access. CI overrides both with the matching Docker Hardened Images from +# dhi.io, which need a Docker account with DHI catalog access. +# Refresh a digest with: docker buildx imagetools inspect +ARG NODE_BUILDER_IMAGE=${BASE_IMAGE_REGISTRY}/library/node:24-trixie-slim@sha256:0711b541c1c33a8a530ac4f0d391baa9a15b3d804695b1b24a47daa5fb60e74d +ARG NODE_RUNTIME_IMAGE=${BASE_IMAGE_REGISTRY}/library/node:24-trixie-slim@sha256:0711b541c1c33a8a530ac4f0d391baa9a15b3d804695b1b24a47daa5fb60e74d + # bun binary source, in its own stage because buildx only expands the registry ARG in a FROM, -# not in `COPY --from=`. Use the glibc (Debian) build so it runs on the glibc DHI node images. +# not in `COPY --from=`. Use the glibc (Debian) build so it runs on the glibc node images. FROM ${BASE_IMAGE_REGISTRY}/oven/bun:1@sha256:e10577f0db68676a7024391c6e5cb4b879ebd17188ab750cf10024a6d700e5c4 AS bun_source -# Build stage. The DHI "-dev" variant runs as root and ships a shell, apt, and npm/npx. -# Refresh the digest with: docker buildx imagetools inspect dhi.io/node:24-debian13-dev -FROM dhi.io/node:24-debian13-dev@sha256:25a83f18150669e9ce3c9437327add4d75ae4ea26cbdb5e8747b66d3b74a567a AS builder +# Build stage. Needs a root user plus a shell and npm/npx, which both the default slim image +# and the DHI "-dev" variant provide. +FROM ${NODE_BUILDER_IMAGE} AS builder COPY --from=bun_source /usr/local/bin/bun /usr/local/bin/bun COPY --from=bun_source /usr/local/bin/bunx /usr/local/bin/bunx WORKDIR /app @@ -100,10 +107,10 @@ RUN --mount=type=secret,id=sentry_auth_token \ fi && \ NODE_OPTIONS="${NODE_OPTIONS}" npx next build -# Runtime stage. The DHI runtime variant is near-distroless: no shell or package manager, runs -# as the non-root `node` user (uid 1000) with WORKDIR /app pre-set. -# Refresh the digest with: docker buildx imagetools inspect dhi.io/node:24-debian13 -FROM dhi.io/node:24-debian13@sha256:805278f24c1146c6d3c96577b6256f8f97c43196fff88315fe3291a1ce118ddd AS runner +# Runtime stage. Runs as the non-root `node` user (uid 1000), which both the default slim +# image and the DHI runtime variant ship. The DHI variant is near-distroless: no shell or +# package manager, so keep this stage free of RUN instructions. +FROM ${NODE_RUNTIME_IMAGE} AS runner LABEL com.onyx.maintainer="founders@onyx.app" LABEL com.onyx.description="This image is the web/frontend container of Onyx which \ @@ -193,5 +200,9 @@ ENV ONYX_VERSION=${ONYX_VERSION} # name only — breaking loopback healthchecks. Force 0.0.0.0 instead. ENV HOSTNAME="0.0.0.0" +# Don't run production as root. The DHI runtime already defaults to this user; the default +# slim image does not. +USER node + # The DHI runtime image sets no default ENTRYPOINT, so invoke node explicitly. CMD ["node", "server.js"]