From 95960f537940d550691fff4a8c229369dfc56bef Mon Sep 17 00:00:00 2001 From: Dev Talan <84081651+devchaudhary24k@users.noreply.github.com> Date: Sun, 27 Sep 2026 23:43:04 +0530 Subject: [PATCH 1/3] fix(docker): drop npm, corepack and yarn from the runtime image The Docker image workflow failed its Trivy gate on CVE-2026-59873 in the tar package bundled inside the base image's npm, so no image was pushed to GHCR. The server never runs a package manager, so remove them after installing Sentry. Co-Authored-By: Claude Opus 5.5 (1M context) --- Dockerfile | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/Dockerfile b/Dockerfile index c4bf779..118189b 100644 --- a/Dockerfile +++ b/Dockerfile @@ -33,12 +33,16 @@ ENV NODE_ENV=production \ PORT=3000 \ HEXLODE_VERSION=$HEXLODE_VERSION # The server keeps Sentry outside its bundle, so it is installed next to it, at the version the -# lockfile pins. +# lockfile pins. npm, corepack and yarn are removed afterwards: the server never uses them, and +# their bundled dependencies would otherwise ship, and be scanned, as part of the image. COPY --from=build /app/.output/sentry-version /tmp/sentry-version RUN npm install --omit=dev --no-save --no-audit --no-fund \ "@sentry/tanstackstart-react@$(cat /tmp/sentry-version)" \ && npm cache clean --force \ - && rm /tmp/sentry-version + && rm -rf /tmp/sentry-version /root/.npm \ + /usr/local/lib/node_modules/npm /usr/local/lib/node_modules/corepack \ + /usr/local/bin/npm /usr/local/bin/npx /usr/local/bin/corepack \ + /opt/yarn-* /usr/local/bin/yarn /usr/local/bin/yarnpkg COPY --from=build --chown=1000:1000 /app/.output ./.output # The node image's unprivileged user. USER 1000:1000 From b4635bad4d52d109decfefd61e4675545eebd0ad Mon Sep 17 00:00:00 2001 From: Dev Talan <84081651+devchaudhary24k@users.noreply.github.com> Date: Sun, 27 Sep 2026 23:51:41 +0530 Subject: [PATCH 2/3] build(docker): run on a distroless Node image Build the Sentry runtime dependencies in the build stage and copy them into gcr.io/distroless/nodejs24-debian13:nonroot. The image has no npm, yarn, corepack or shell, so there is nothing to strip afterwards, it runs as a non-root user by default and drops about 140 MB. Co-Authored-By: Claude Opus 5.5 (1M context) --- DEPLOY.md | 2 +- Dockerfile | 54 +++++++++++++----------------------------------------- 2 files changed, 14 insertions(+), 42 deletions(-) diff --git a/DEPLOY.md b/DEPLOY.md index 5708d24..74e2064 100644 --- a/DEPLOY.md +++ b/DEPLOY.md @@ -70,7 +70,7 @@ starts, and passes its health check, before the old one stops: ``` The image already has a health check. Only if you want to change its timing, set **Health -Check** (times are in nanoseconds). The image has no `curl`, so the check uses Node: +Check** (times are in nanoseconds). The image is distroless, with no shell or `curl`, so the check runs Node directly: ```json { diff --git a/Dockerfile b/Dockerfile index 118189b..141c902 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,53 +1,25 @@ -# Hexlode: one container serving the Nitro build. See DEPLOY.md for running it on Dokploy. +# syntax=docker/dockerfile:1 -# The build output is plain JavaScript and WebAssembly, so it is built once on the build machine's -# own platform and shared by every image platform. FROM --platform=$BUILDPLATFORM node:24.17.0-slim AS build WORKDIR /app -ENV HUSKY=0 \ - PLAYWRIGHT_SKIP_BROWSER_DOWNLOAD=1 \ - SKIP_ENV_VALIDATION=1 +ENV HUSKY=0 PLAYWRIGHT_SKIP_BROWSER_DOWNLOAD=1 SKIP_ENV_VALIDATION=1 RUN corepack enable COPY package.json pnpm-lock.yaml pnpm-workspace.yaml ./ RUN pnpm install --frozen-lockfile COPY . . -# Optional fallbacks. The server reads these from its environment at runtime, which wins. -ARG VITE_POSTHOG_KEY="" -ARG VITE_POSTHOG_HOST="" -ARG VITE_SENTRY_DSN="" -ENV VITE_POSTHOG_KEY=$VITE_POSTHOG_KEY \ - VITE_POSTHOG_HOST=$VITE_POSTHOG_HOST \ - VITE_SENTRY_DSN=$VITE_SENTRY_DSN -RUN pnpm build && chmod -R a+rX .output \ - && node -p "require('@sentry/tanstackstart-react/package.json').version" > .output/sentry-version +RUN pnpm build && chmod -R a+rX .output +# Nitro leaves Sentry out of the server bundle, so install it on its own at the locked version. +RUN SENTRY=$(node -p "require('@sentry/tanstackstart-react/package.json').version") \ + && npm install --prefix /runtime --omit=dev --omit=optional --ignore-scripts \ + --no-package-lock --no-audit --no-fund "@sentry/tanstackstart-react@$SENTRY" -FROM node:24.17.0-slim AS runtime +FROM gcr.io/distroless/nodejs24-debian13:nonroot WORKDIR /app ARG HEXLODE_VERSION=dev -LABEL org.opencontainers.image.title="Hexlode" \ - org.opencontainers.image.description="Image pipelines that run in your browser" \ - org.opencontainers.image.source="https://github.com/pixelactstudio/hexlode" \ - org.opencontainers.image.licenses="Apache-2.0" -ENV NODE_ENV=production \ - HOST=0.0.0.0 \ - PORT=3000 \ - HEXLODE_VERSION=$HEXLODE_VERSION -# The server keeps Sentry outside its bundle, so it is installed next to it, at the version the -# lockfile pins. npm, corepack and yarn are removed afterwards: the server never uses them, and -# their bundled dependencies would otherwise ship, and be scanned, as part of the image. -COPY --from=build /app/.output/sentry-version /tmp/sentry-version -RUN npm install --omit=dev --no-save --no-audit --no-fund \ - "@sentry/tanstackstart-react@$(cat /tmp/sentry-version)" \ - && npm cache clean --force \ - && rm -rf /tmp/sentry-version /root/.npm \ - /usr/local/lib/node_modules/npm /usr/local/lib/node_modules/corepack \ - /usr/local/bin/npm /usr/local/bin/npx /usr/local/bin/corepack \ - /opt/yarn-* /usr/local/bin/yarn /usr/local/bin/yarnpkg -COPY --from=build --chown=1000:1000 /app/.output ./.output -# The node image's unprivileged user. -USER 1000:1000 +ENV NODE_ENV=production HOST=0.0.0.0 PORT=3000 HEXLODE_VERSION=$HEXLODE_VERSION +COPY --from=build /runtime/node_modules ./node_modules +COPY --from=build /app/.output ./.output EXPOSE 3000 -# Dokploy's zero-downtime updates wait for this before moving traffic to a new container. HEALTHCHECK --interval=15s --timeout=5s --start-period=20s --retries=3 \ - CMD ["node", "-e", "fetch('http://127.0.0.1:' + (process.env.PORT || 3000) + '/api/health').then((r) => process.exit(r.ok ? 0 : 1), () => process.exit(1))"] -CMD ["node", "--import", "./.output/server/instrument.server.mjs", ".output/server/index.mjs"] + CMD ["/nodejs/bin/node", "-e", "fetch('http://127.0.0.1:3000/api/health').then(r => process.exit(r.ok ? 0 : 1), () => process.exit(1))"] +CMD ["--import", "./.output/server/instrument.server.mjs", ".output/server/index.mjs"] From 8fcd5392a9be81053f59b38f4b640523b4c82628 Mon Sep 17 00:00:00 2001 From: Dev Talan <84081651+devchaudhary24k@users.noreply.github.com> Date: Sun, 27 Sep 2026 23:51:53 +0530 Subject: [PATCH 3/3] docs(deploy): point the health check at the distroless Node path Co-Authored-By: Claude Opus 5.5 (1M context) --- DEPLOY.md | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/DEPLOY.md b/DEPLOY.md index 74e2064..3f026c4 100644 --- a/DEPLOY.md +++ b/DEPLOY.md @@ -70,13 +70,14 @@ starts, and passes its health check, before the old one stops: ``` The image already has a health check. Only if you want to change its timing, set **Health -Check** (times are in nanoseconds). The image is distroless, with no shell or `curl`, so the check runs Node directly: +Check** (times are in nanoseconds). The image is distroless, with no shell or `curl`, so the check +runs Node directly: ```json { "Test": [ "CMD", - "node", + "/nodejs/bin/node", "-e", "fetch('http://127.0.0.1:3000/api/health').then((r) => process.exit(r.ok ? 0 : 1), () => process.exit(1))" ],