Repository navigation
Expand file tree
/
Copy pathDockerfile
More file actions
312 lines (263 loc) · 14.5 KB
/
Copy pathDockerfile
File metadata and controls
312 lines (263 loc) · 14.5 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
# FIPS 140-3 base: UBI 9 with the nodejs:24 module-stream RPM
# instead of node:24-alpine. The RPM Node dynamically links the system
# libcrypto.so.3, so node:crypto routes through RHEL's CMVP-validated OpenSSL
# FIPS provider (/usr/lib64/ossl-modules/fips.so). Upstream node images
# statically bundle their own non-validated OpenSSL and bypass the platform
# module entirely.
# The defaults are Red Hat's public UBI9 images, so the build needs no
# credentials. Iron Bank serves the same content from an authenticated registry
# with DoD provenance and scan attestations. Release builds opt in by overriding
# both args after `docker login registry1.dso.mil` (free P1 account plus a CLI
# secret):
# --build-arg BASE_IMAGE=registry1.dso.mil/ironbank/redhat/ubi/ubi9:9.8
# --build-arg MICRO_BASE_IMAGE=registry1.dso.mil/ironbank/redhat/ubi/ubi9-micro:9.8
ARG BASE_IMAGE=registry.access.redhat.com/ubi9/ubi:9.8
ARG MICRO_BASE_IMAGE=registry.access.redhat.com/ubi9/ubi-micro:9.8
FROM --platform=${TARGETPLATFORM:-linux/amd64} ${BASE_IMAGE} AS base
# nodejs:24 and openssl (incl. the FIPS provider) come from the freely
# redistributable UBI repos, so no RHEL entitlement is required.
# nodejs-full-i18n is required: the RPM Node is a small-ICU build and V8
# segfaults in Intl.Segmenter (used e.g. by the prisma CLI's output
# formatting) without the full ICU data.
RUN dnf -y upgrade && \
dnf -y module enable nodejs:24 && \
dnf -y install --setopt=install_weak_deps=0 nodejs npm nodejs-full-i18n && \
dnf clean all
FROM --platform=${TARGETPLATFORM:-linux/amd64} base AS build-base
# Pin turbo to avoid nondeterministic prune output from future patch releases.
# pnpm is installed pinned via npm instead of corepack: the corepack shim would
# fetch pnpm from an unpinned CDN on first use.
RUN npm install -g turbo@2.11.9 pnpm@12.8.2
# Pass --build-arg NODE_USE_ENV_PROXY=1 when building behind a proxy.
ARG NODE_USE_ENV_PROXY
# Runtime rootfs assembled with dnf --installroot and layered onto ubi9-micro:
# the final image carries no package manager, python, or curl. It holds only
# the packages listed below plus their deps, with an intact rpmdb for scanners.
FROM --platform=${TARGETPLATFORM:-linux/amd64} base AS rpm-installer
ARG TARGETARCH
ARG UID=1001
ARG GID=1001
# nodejs-full-i18n is required: the RPM Node is a small-ICU build and V8
# segfaults in Intl.Segmenter (used e.g. by the prisma CLI's output
# formatting) without the full ICU data. sed is used by entrypoint.sh.
# wget keeps exec-style container health checks working (e.g. `wget --spider
# http://127.0.0.1:<port>/...` in ECS task definitions); it links GnuTLS,
# which follows the host's FIPS mode like OpenSSL does.
RUN dnf -y --installroot /mnt/rootfs --releasever 9 --setopt=install_weak_deps=0 \
--disableexcludes=all \
module enable nodejs:24 && \
dnf -y --installroot /mnt/rootfs --releasever 9 \
--setopt=install_weak_deps=0 --setopt=tsflags=nodocs \
--disableexcludes=all \
install nodejs nodejs-full-i18n bash coreutils-single sed \
tzdata ca-certificates crypto-policies wget && \
dnf --installroot /mnt/rootfs clean all && \
rm -rf /mnt/rootfs/var/cache /mnt/rootfs/var/log/dnf*
# The nodejs RPM drags npm in as a dependency; the runtime image must carry
# no package manager, so remove it from the rootfs (rpmdb stays consistent).
RUN if rpm --root /mnt/rootfs -q npm >/dev/null 2>&1; then \
rpm --root /mnt/rootfs -e --nodeps npm; \
fi
# The FIPS crypto policy for OpenSSL ships as an opt-in config instead of
# being activated system-wide, so installs without FIPS mode keep the default
# TLS policy. entrypoint.sh points OPENSSL_CONF at it when
# LANGFUSE_REQUIRE_FIPS=true; deployments that override the entrypoint set
# OPENSSL_CONF=/etc/pki/tls/openssl-fips.cnf themselves. The FIPS provider
# itself loads only when the host kernel runs in FIPS mode; with
# LANGFUSE_REQUIRE_FIPS=true, startup fails unless it is active and an
# enterprise license is configured.
RUN sed 's#/etc/crypto-policies/back-ends/opensslcnf.config#/usr/share/crypto-policies/FIPS/opensslcnf.txt#' \
/mnt/rootfs/etc/pki/tls/openssl.cnf > /mnt/rootfs/etc/pki/tls/openssl-fips.cnf && \
grep -q '/usr/share/crypto-policies/FIPS/opensslcnf.txt' /mnt/rootfs/etc/pki/tls/openssl-fips.cnf
# dumb-init is not packaged in UBI; install the pinned, checksum-verified
# static release binary (preserves the semver-covered ENTRYPOINT contract).
RUN set -eux; \
case "${TARGETARCH:-amd64}" in \
amd64) di_arch=x86_64; di_sha256=e874b55f3279ca41415d290c512a7ba9d08f98041b28ae7c2acb19a545f1c4df ;; \
arm64) di_arch=aarch64; di_sha256=b7d648f97154a99c539b63c55979cd29f005f88430fb383007fe3458340b795e ;; \
*) echo "unsupported TARGETARCH: ${TARGETARCH}" >&2; exit 1 ;; \
esac; \
curl -fsSL -o /mnt/rootfs/usr/bin/dumb-init "https://github.com/Yelp/dumb-init/releases/download/v1.2.5/dumb-init_1.2.5_${di_arch}"; \
echo "${di_sha256} /mnt/rootfs/usr/bin/dumb-init" | sha256sum -c -; \
chmod 0755 /mnt/rootfs/usr/bin/dumb-init
# The UBI EULA requires every image derived from UBI to include it unmodified.
# The magic-byte check rejects an HTML error page served in its place.
# Keep in sync with worker/Dockerfile and ai-gateway/Dockerfile.
RUN mkdir -p /mnt/rootfs/usr/share/licenses/redhat-ubi && \
curl -fsSL --retry 5 --retry-all-errors -o /mnt/rootfs/usr/share/licenses/redhat-ubi/EULA_Red_Hat_Universal_Base_Image_English_20190422.pdf \
https://www.redhat.com/licenses/EULA_Red_Hat_Universal_Base_Image_English_20190422.pdf && \
head -c 5 /mnt/rootfs/usr/share/licenses/redhat-ubi/EULA_Red_Hat_Universal_Base_Image_English_20190422.pdf | grep -qx '%PDF-'
# Bake the non-root user into the rootfs (micro has no shadow-utils).
RUN echo "nodejs:x:${GID}:" >> /mnt/rootfs/etc/group && \
echo "nextjs:x:${UID}:${GID}::/home/nextjs:/sbin/nologin" >> /mnt/rootfs/etc/passwd
FROM --platform=${TARGETPLATFORM:-linux/amd64} ${MICRO_BASE_IMAGE} AS runtime-base
COPY --from=rpm-installer /mnt/rootfs /
# prisma CLI + optional dd-trace are npm-installed here and copied into the
# package-manager-less runtime image.
FROM --platform=${TARGETPLATFORM:-linux/amd64} build-base AS web-tools
RUN npm install -g --prefix /opt/prisma-cli --no-package-lock --no-save prisma@6.19.3
ARG NEXT_PUBLIC_LANGFUSE_CLOUD_REGION
RUN mkdir -p /opt/dd/node_modules && \
if [ -n "$NEXT_PUBLIC_LANGFUSE_CLOUD_REGION" ]; then \
cd /opt/dd && npm install --no-package-lock --no-save dd-trace@5.109.0; \
fi
FROM --platform=${BUILDPLATFORM} golang:1.26 AS migrate-builder
ARG TARGETOS
ARG TARGETARCH
ENV CGO_ENABLED=0 \
GOBIN=/out \
GOOS=${TARGETOS} \
GOARCH=${TARGETARCH}
# Build against the validated Go Cryptographic Module snapshot (Go >= 1.24) so
# the migrate CLI's ClickHouse TLS uses FIPS-approved crypto; GOFIPS140 also
# defaults GODEBUG=fips140=on at runtime.
ENV GOFIPS140=v1.0.0
# Build only the ClickHouse migrate CLI Langfuse uses at runtime.
# compile this ourselves instead of downloading the upstream release
# because prebuilt bins bundle many unused drivers and thus inherit CVEs
# eg.: https://github.com/golang-migrate/migrate/issues/1357
RUN /usr/local/go/bin/go install -trimpath -tags 'clickhouse' -ldflags='-s -w' \
github.com/golang-migrate/migrate/v4/cmd/migrate@v4.19.1
FROM --platform=${TARGETPLATFORM:-linux/amd64} build-base AS pruner
WORKDIR /app
COPY . .
RUN turbo prune web --docker
FROM --platform=${TARGETPLATFORM:-linux/amd64} build-base AS builder
WORKDIR /app
# First install the dependencies (as they change less often)
COPY --from=pruner /app/out/pnpm-lock.yaml ./pnpm-lock.yaml
COPY --from=pruner /app/out/pnpm-workspace.yaml ./pnpm-workspace.yaml
COPY --from=pruner /app/out/json/ .
RUN pnpm install --frozen-lockfile
# pnpm can verify dependencies before each `pnpm run`, so every command
# spawned by turbo re-checks node_modules via file mtimes. Cached install
# layers make those mtimes look stale, and the concurrent auto-installs race
# in the hoisting step (ENOENT unlink in node_modules/.pnpm/node_modules).
# The frozen-lockfile install above is authoritative; skip the check.
ENV pnpm_config_verify_deps_before_run=false
ENV DOCKER_BUILD 1
ENV NEXT_MANUAL_SIG_HANDLE true
# Allows the CI docker build smoke test to skip the Next.js type check that
# the lint job already runs (`pnpm run typecheck`). Release builds do not set
# this and still fail on TypeScript errors.
ARG NEXT_IGNORE_BUILD_ERRORS
ENV NEXT_IGNORE_BUILD_ERRORS=$NEXT_IGNORE_BUILD_ERRORS
# pass public variables in build step
ARG NEXT_PUBLIC_PLAIN_APP_ID
ARG NEXT_PUBLIC_LANGFUSE_CLOUD_REGION
ENV NEXT_PUBLIC_LANGFUSE_CLOUD_REGION=$NEXT_PUBLIC_LANGFUSE_CLOUD_REGION
ARG NEXT_PUBLIC_DEMO_ORG_ID
ENV NEXT_PUBLIC_DEMO_ORG_ID=$NEXT_PUBLIC_DEMO_ORG_ID
ARG NEXT_PUBLIC_DEMO_PROJECT_ID
ENV NEXT_PUBLIC_DEMO_PROJECT_ID=$NEXT_PUBLIC_DEMO_PROJECT_ID
ARG NEXT_PUBLIC_SIGN_UP_DISABLED
ENV NEXT_PUBLIC_SIGN_UP_DISABLED=$NEXT_PUBLIC_SIGN_UP_DISABLED
ARG NEXT_PUBLIC_TURNSTILE_SITE_KEY
ENV NEXT_PUBLIC_TURNSTILE_SITE_KEY=$NEXT_PUBLIC_TURNSTILE_SITE_KEY
ARG NEXT_PUBLIC_PREVIEW_PR_URL
ENV NEXT_PUBLIC_PREVIEW_PR_URL=$NEXT_PUBLIC_PREVIEW_PR_URL
ARG NEXT_PUBLIC_PREVIEW_PR_AUTHOR
ENV NEXT_PUBLIC_PREVIEW_PR_AUTHOR=$NEXT_PUBLIC_PREVIEW_PR_AUTHOR
ARG NEXT_PUBLIC_PREVIEW_LAST_UPDATED
ENV NEXT_PUBLIC_PREVIEW_LAST_UPDATED=$NEXT_PUBLIC_PREVIEW_LAST_UPDATED
ARG NEXT_PUBLIC_PREVIEW_DEMO_AUTO_SIGN_IN
ENV NEXT_PUBLIC_PREVIEW_DEMO_AUTO_SIGN_IN=$NEXT_PUBLIC_PREVIEW_DEMO_AUTO_SIGN_IN
ARG NEXT_PUBLIC_POSTHOG_KEY
ENV NEXT_PUBLIC_POSTHOG_KEY=$NEXT_PUBLIC_POSTHOG_KEY
ARG NEXT_PUBLIC_POSTHOG_HOST
ENV NEXT_PUBLIC_POSTHOG_HOST=$NEXT_PUBLIC_POSTHOG_HOST
ARG NEXT_PUBLIC_LANGFUSE_TRACING_SAMPLE_RATE
ENV NEXT_PUBLIC_LANGFUSE_TRACING_SAMPLE_RATE=$NEXT_PUBLIC_LANGFUSE_TRACING_SAMPLE_RATE
ARG NEXT_PUBLIC_SENTRY_ENVIRONMENT
ENV NEXT_PUBLIC_SENTRY_ENVIRONMENT=$NEXT_PUBLIC_SENTRY_ENVIRONMENT
ARG NEXT_PUBLIC_SENTRY_DSN
ENV NEXT_PUBLIC_SENTRY_DSN=$NEXT_PUBLIC_SENTRY_DSN
ARG NEXT_PUBLIC_BASE_PATH
ENV NEXT_PUBLIC_BASE_PATH=$NEXT_PUBLIC_BASE_PATH
# Origin serving this build's /_next/static/* output. Must be a build arg: the
# bundler writes it into the client runtime's public path, so it cannot be
# changed by a runtime environment variable.
ARG NEXT_PUBLIC_ASSET_PREFIX
ENV NEXT_PUBLIC_ASSET_PREFIX=$NEXT_PUBLIC_ASSET_PREFIX
# Sentry already needs to be set on build time to upload sourcemaps
# This must not be set for OSS releases as we would share our Sentry secret.
ARG SENTRY_AUTH_TOKEN
ENV SENTRY_AUTH_TOKEN=$SENTRY_AUTH_TOKEN
ARG SENTRY_ORG
ENV SENTRY_ORG=$SENTRY_ORG
ARG SENTRY_PROJECT
ENV SENTRY_PROJECT=$SENTRY_PROJECT
# Accept build id as NEXT_PUBLIC_BUILD_ID
ARG NEXT_PUBLIC_BUILD_ID
ENV NEXT_PUBLIC_BUILD_ID=$NEXT_PUBLIC_BUILD_ID
ENV SENTRY_RELEASE=$NEXT_PUBLIC_BUILD_ID
# Copy source code of isolated subworkspace
COPY --from=pruner /app/out/full/ .
# Keep the documented direct golang-migrate recovery paths available in the
# production image while maintaining canonical/ as the only authored tree.
RUN set -eu; \
node ./packages/shared/clickhouse/scripts/prepare-migrations.mjs materialize ./packages/shared/clickhouse/migrations; \
chmod 755 ./packages/shared/clickhouse/migrations/clustered ./packages/shared/clickhouse/migrations/unclustered
# remove middleware.ts if it exists - not needed in self-hosted environments
RUN rm -f ./web/src/middleware.ts
# Next.js collects completely anonymous telemetry data about general usage.
# Learn more here: https://nextjs.org/telemetry
# Uncomment the following line in case you want to disable telemetry during the build.
ENV NEXT_TELEMETRY_DISABLED 1
ENV NEXT_MANUAL_SIG_HANDLE true
# set the CI flag to true to get CI specific logs
ENV CI true
RUN NODE_OPTIONS='--max-old-space-size-percentage=75' turbo run build --filter=web...
# Production image, copy all the files and run next
FROM --platform=${TARGETPLATFORM:-linux/amd64} runtime-base AS runner
ARG TARGETPLATFORM
ARG BUILDPLATFORM
WORKDIR /app
ARG NEXT_PUBLIC_BUILD_ID
ENV BUILD_ID=$NEXT_PUBLIC_BUILD_ID
ARG NEXT_PUBLIC_LANGFUSE_CLOUD_REGION
ENV NEXT_PUBLIC_LANGFUSE_CLOUD_REGION=$NEXT_PUBLIC_LANGFUSE_CLOUD_REGION
ENV NODE_ENV production
# Uncomment the following line in case you want to disable telemetry during runtime.
ENV NEXT_TELEMETRY_DISABLED 1
# Needed to re-enable validation of environment variables during runtime
ENV DOCKER_BUILD 0
# Set NEXT_MANUAL_SIG_HANDLE for runtime
ENV NEXT_MANUAL_SIG_HANDLE true
# The non-root user (nextjs, uid 1001) is baked into the runtime rootfs by
# the rpm-installer stage, since micro has no shadow-utils.
# prisma CLI from the tools stage (bin/prisma symlink + lib/node_modules
# structure lands under /usr/local); the runtime image has no npm.
COPY --from=web-tools /opt/prisma-cli /usr/local
# dd-trace, npm-installed in the tools stage (node_modules is empty unless
# NEXT_PUBLIC_LANGFUSE_CLOUD_REGION was set); the standalone output copied
# below merges on top of it.
COPY --from=web-tools --chown=nextjs:nodejs /opt/dd/node_modules ./node_modules
COPY --from=migrate-builder /out/migrate /usr/bin/migrate
COPY --from=builder --chown=nextjs:nodejs /app/web/next.config.mjs .
COPY --from=builder --chown=nextjs:nodejs /app/web/package.json .
# Automatically leverage output traces to reduce image size
# https://nextjs.org/docs/advanced-features/output-file-tracing
COPY --from=builder --chown=nextjs:nodejs /app/web/.next/standalone ./
COPY --from=builder --chown=nextjs:nodejs /app/web/.next/static ./web/.next/static
COPY --from=builder --chown=nextjs:nodejs /app/web/public ./web/public
COPY --from=builder --chown=nextjs:nodejs /app/packages/shared/prisma ./packages/shared/prisma
COPY --from=builder --chown=nextjs:nodejs /app/packages/shared/clickhouse ./packages/shared/clickhouse
COPY --chown=nextjs:nodejs ./web/entrypoint.sh ./web/entrypoint.sh
COPY --chown=nextjs:nodejs ./packages/shared/scripts/cleanup.sql ./packages/shared/scripts/cleanup.sql
RUN chmod +x ./web/entrypoint.sh
# Numeric USER (uid of nextjs) so runAsNonRoot checks and Iron Bank hardening
# validation can verify the user without resolving names.
ARG UID=1001
USER ${UID}
# Default port to 3000
ENV PORT 3000
# Docker ENTRYPOINT (dumb-init) is covered by semantic versioning, not the entrypoint.sh itself
# Reasoning: ENTRYPOINT is overridden by some self-hosted deployments, thus changing this is breaking
ENTRYPOINT ["dumb-init", "--", "./web/entrypoint.sh"]
# startup command - use dd-trace if NEXT_PUBLIC_LANGFUSE_CLOUD_REGION is configured
CMD if [ -n "$NEXT_PUBLIC_LANGFUSE_CLOUD_REGION" ]; then \
node --import dd-trace/initialize.mjs ./web/server.js --keepAliveTimeout 110000; \
else \
node ./web/server.js --keepAliveTimeout 110000; \
fi