# Debian (glibc), not Alpine (musl): MP4 and poster renders run Chromium,
# which needs glibc.
FROM oven/bun:1.3.14-slim AS dependencies
WORKDIR /app
COPY package.json bun.lock ./
COPY patches ./patches
RUN bun install --frozen-lockfile
# Turbopack's build cache from the last image build, when the deploy passes
# one in (--build-context next-cache=
, scripts/cf-container-image.mjs);
# empty otherwise.
FROM scratch AS next-cache
FROM node:22-bookworm-slim AS builder
WORKDIR /app
COPY . .
ENV NEXT_TELEMETRY_DISABLED=1
ENV RAILWAY_DOCKER_BUILD=1
# NEXT_PUBLIC_* values are compiled into the client at build time, and .env
# files never reach the image, so they come in as build arguments (Railway
# passes a service variable to every ARG of the same name). Without them the
# video controls build switched off and live presence unset.
ARG NEXT_PUBLIC_POSTHOG_KEY
ARG NEXT_PUBLIC_PRESENCE_URL
ARG NEXT_PUBLIC_VIDEO_EXPLAINER
ENV NEXT_PUBLIC_POSTHOG_KEY=$NEXT_PUBLIC_POSTHOG_KEY
ENV NEXT_PUBLIC_PRESENCE_URL=$NEXT_PUBLIC_PRESENCE_URL
ENV NEXT_PUBLIC_VIDEO_EXPLAINER=$NEXT_PUBLIC_VIDEO_EXPLAINER
# Match the standalone runtime. Bun's Linux ARM64 worker can crash while Next
# runs its TypeScript build; Bun still handles the frozen dependency install.
# Types are checked by the deploy's own build (scripts/cf-build.sh), not here.
# The dependencies are mounted for the build rather than copied in (a
# gigabyte, about 13 s); the standalone output traces what it needs from them.
RUN --mount=type=bind,from=dependencies,source=/app/node_modules,target=/app/node_modules,rw \
--mount=type=bind,from=next-cache,target=/tmp/next-cache \
mkdir -p .next/cache && cp -a /tmp/next-cache/. .next/cache/ \
&& node scripts/build-mcp-app.mjs \
&& NEXT_SKIP_TYPECHECK=1 node node_modules/next/dist/bin/next build
# The cache this build leaves, for the next one (--target next-cache-out).
FROM scratch AS next-cache-out
COPY --from=builder /app/.next/cache /
FROM node:22-bookworm-slim AS runner
WORKDIR /app
ENV NODE_ENV=production
ENV NEXT_TELEMETRY_DISABLED=1
# Listen on every interface. Next then reports request URLs as 0.0.0.0, so
# renders call the server back on 127.0.0.1:$PORT instead (render-origin.ts;
# VIDEO_INTERNAL_ORIGIN overrides it).
ENV HOSTNAME=0.0.0.0
ENV PORT=3000
# On SIGTERM (a deploy, or the platform putting an idle instance to sleep)
# the server finishes renders and videos in flight before it exits, instead
# of Next's default of refusing new connections at once (src/server/drain.ts).
ENV NEXT_MANUAL_SIG_HANDLE=1
# Renders launch Debian's Chromium. A container has no user namespaces for
# Chrome's sandbox and a small /dev/shm, hence the two flags below. The films
# bring their own fonts; the system ones only cover
# what those lack, which otherwise renders as empty boxes: emoji, Chinese,
# Japanese and Korean text, and (fonts-noto-core) Arabic, Hebrew, Thai and
# the Indian scripts, all of which stored videos contain. tini is the init
# process (see ENTRYPOINT).
RUN apt-get update \
&& apt-get install -y --no-install-recommends \
ca-certificates \
chromium \
fonts-liberation \
fonts-noto-cjk \
fonts-noto-color-emoji \
fonts-noto-core \
tini \
&& rm -rf /var/lib/apt/lists/*
ENV VIDEO_RENDER_CHROME_PATH=/usr/bin/chromium
ENV VIDEO_RENDER_CHROME_ARGS="--no-sandbox --disable-dev-shm-usage"
RUN groupadd --system --gid 1001 nodejs \
&& useradd --system --uid 1001 --gid nodejs --create-home nextjs \
&& mkdir .next \
&& chown nextjs:nodejs .next
# --link makes each copy a layer of its own, so a cached build can push the
# app without first fetching the Chromium layer beneath it. Linked copies
# cannot look up names, hence the numeric nextjs:nodejs.
COPY --link --from=builder --chown=1001:1001 /app/public ./public
COPY --link --from=builder --chown=1001:1001 /app/.next/standalone ./
COPY --link --from=builder --chown=1001:1001 /app/.next/static ./.next/static
USER nextjs
EXPOSE 3000
# Every render leaves Chromium helper processes behind for process 1 to reap.
# Node does not, so they piled up as zombies until the container restarted;
# tini reaps them and passes signals on to the server.
ENTRYPOINT ["tini", "--"]
CMD ["node", "server.js"]