Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 3 additions & 2 deletions DEPLOY.md
Original file line number Diff line number Diff line change
Expand Up @@ -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 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
{
"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))"
],
Expand Down
50 changes: 13 additions & 37 deletions Dockerfile
Original file line number Diff line number Diff line change
@@ -1,49 +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.
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
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"]
Loading