# syntax=docker/dockerfile:1 # Single image used by both the web and worker services (see docker-compose.yml). # Includes ffmpeg (audio stitching) + the full node_modules so the worker can run # via tsx and `prisma migrate deploy` can run on web startup. FROM node:22-bookworm-slim AS base RUN apt-get update \ && apt-get install -y --no-install-recommends ffmpeg openssl ca-certificates \ && rm -rf /var/lib/apt/lists/* WORKDIR /app ENV NEXT_TELEMETRY_DISABLED=1 # ---- dependencies ---- FROM base AS deps COPY package.json package-lock.json ./ RUN npm ci # ---- build ---- FROM base AS build COPY --from=deps /app/node_modules ./node_modules COPY . . # NEXT_PUBLIC_* are inlined into the client bundle at build time, so they must be # provided as build args (Dokploy passes them from the env — see docker-compose.yml). ARG NEXT_PUBLIC_APP_URL ARG NEXT_PUBLIC_STRIPE_PUBLISHABLE_KEY ARG NEXT_PUBLIC_GOOGLE_SITE_VERIFICATION ARG NEXT_PUBLIC_BING_SITE_VERIFICATION ARG UMAMI_HOST_URL ARG UMAMI_WEBSITE_ID ARG NEXT_PUBLIC_TURNSTILE_SITE_KEY ENV NEXT_PUBLIC_APP_URL=$NEXT_PUBLIC_APP_URL ENV NEXT_PUBLIC_STRIPE_PUBLISHABLE_KEY=$NEXT_PUBLIC_STRIPE_PUBLISHABLE_KEY ENV NEXT_PUBLIC_GOOGLE_SITE_VERIFICATION=$NEXT_PUBLIC_GOOGLE_SITE_VERIFICATION ENV NEXT_PUBLIC_BING_SITE_VERIFICATION=$NEXT_PUBLIC_BING_SITE_VERIFICATION # Not NEXT_PUBLIC_, but still needed at build time: the root layout is statically # rendered, so the website id is baked into the prerendered HTML. ENV UMAMI_HOST_URL=$UMAMI_HOST_URL ENV UMAMI_WEBSITE_ID=$UMAMI_WEBSITE_ID ENV NEXT_PUBLIC_TURNSTILE_SITE_KEY=$NEXT_PUBLIC_TURNSTILE_SITE_KEY # A throwaway BETTER_AUTH_SECRET, scoped to THIS command only (not a persisted ENV # layer), satisfies the prod-secret guard in lib/auth/auth.ts during `next build`. # Must be >= 32 chars (and not a known placeholder) to pass that guard; the real # secret is injected at run time and is never baked into the bundle. # TURNSTILE_SECRET_KEY gets the same treatment for the Turnstile guard in # lib/auth/auth.ts: a throwaway value satisfies it during `next build`, while the # real secret is injected at run time and never baked into the image. (The SITE # key is not a secret and is a real build arg above.) RUN BETTER_AUTH_SECRET=build-time-placeholder-not-a-real-secret \ TURNSTILE_SECRET_KEY=build-time-placeholder-not-a-real-secret \ npm run build # ---- runtime ---- FROM base AS runner ENV NODE_ENV=production ENV PORT=3000 ENV HOSTNAME=0.0.0.0 ENV STORAGE_DIR=/app/storage # Copy the whole built app (node_modules incl. tsx + prisma CLI, .next, source). COPY --from=build /app ./ # `server-only` throws at import time when resolved outside Next's RSC bundler. # The worker runs lib/* under tsx (plain Node), which hits that throw (e.g. via # lib/flags). Neutralize the runtime module in the FINAL image only — the # build-time client/server guard already ran during `next build`. Web is # unaffected: `next start` serves prebuilt output and never relies on the throw. RUN cp node_modules/server-only/empty.js node_modules/server-only/index.js RUN mkdir -p /app/storage/mp3 /app/storage/art /app/storage/exports # Drop root. `node` (uid 1000) ships with the official image. /app/storage is the # only path written at run time, so it (and the Next.js cache) must be owned by it. RUN chown -R node:node /app/storage /app/.next USER node EXPOSE 3000 # Default = web; the worker service overrides this command in docker-compose.yml. CMD ["npm", "run", "start"]