# syntax=docker/dockerfile:1 FROM node:22-alpine AS base RUN corepack enable # ---- deps: install full workspace deps ---- FROM base AS deps WORKDIR /repo COPY pnpm-workspace.yaml package.json pnpm-lock.yaml ./ COPY apps/web/package.json apps/web/package.json COPY packages/db/package.json packages/db/package.json RUN pnpm install --frozen-lockfile # ---- migrator: applies drizzle migrations + seeds tier definitions ---- FROM deps AS migrator WORKDIR /repo/packages/db COPY packages/db . CMD ["sh", "-c", "pnpm migrate && pnpm seed"] # ---- build ---- FROM base AS build WORKDIR /repo COPY --from=deps /repo/node_modules ./node_modules COPY --from=deps /repo/apps/web/node_modules ./apps/web/node_modules COPY --from=deps /repo/packages/db/node_modules ./packages/db/node_modules COPY . . # apps/web/.env.local is normally a symlink to repo-root .env.local (gitignored); # it doesn't exist in the build context, so Next's env loader chokes on the dangling # link. No secrets are needed at build time — they're injected at container runtime. RUN rm -f apps/web/.env.local && touch apps/web/.env.local ENV NEXT_TELEMETRY_DISABLED=1 # NEXT_PUBLIC_* vars are inlined into the client bundle at build time, not read at # container runtime — must be passed as build args (compose.prod.yml wires these # from the same stack env vars used at runtime, so they stay in sync). ARG NEXT_PUBLIC_VAPID_PUBLIC_KEY ARG NEXT_PUBLIC_GITHUB_ENABLED ARG NEXT_PUBLIC_DISCORD_ENABLED ARG NEXT_PUBLIC_AUTHENTIK_ENABLED ENV NEXT_PUBLIC_VAPID_PUBLIC_KEY=$NEXT_PUBLIC_VAPID_PUBLIC_KEY ENV NEXT_PUBLIC_GITHUB_ENABLED=$NEXT_PUBLIC_GITHUB_ENABLED ENV NEXT_PUBLIC_DISCORD_ENABLED=$NEXT_PUBLIC_DISCORD_ENABLED ENV NEXT_PUBLIC_AUTHENTIK_ENABLED=$NEXT_PUBLIC_AUTHENTIK_ENABLED # next.config.ts reads STORAGE_PUBLIC_URL to compute the CSP connect-src/img-src # allowlist, and next.config.ts's headers() are evaluated at build time — so this # also needs to be a build arg, not just a runtime env var (unlike STORAGE_ENDPOINT). ARG STORAGE_PUBLIC_URL ENV STORAGE_PUBLIC_URL=$STORAGE_PUBLIC_URL # lib/storage.ts's getPublicUrl() is called from client components to render photo # tags — a plain (non-NEXT_PUBLIC_) env var is never inlined into the browser # bundle, so it must be duplicated under a NEXT_PUBLIC_ name to reach the client. ARG NEXT_PUBLIC_STORAGE_PUBLIC_URL ENV NEXT_PUBLIC_STORAGE_PUBLIC_URL=$NEXT_PUBLIC_STORAGE_PUBLIC_URL RUN pnpm --filter web build # ---- runtime ---- FROM base AS runner WORKDIR /app ENV NODE_ENV=production ENV NEXT_TELEMETRY_DISABLED=1 ENV PORT=3000 RUN addgroup -g 1001 -S nodejs && adduser -S nextjs -u 1001 COPY --from=build /repo/apps/web/public ./public COPY --from=build --chown=nextjs:nodejs /repo/apps/web/.next/standalone ./ COPY --from=build --chown=nextjs:nodejs /repo/apps/web/.next/static ./apps/web/.next/static USER nextjs EXPOSE 3000 CMD ["node", "apps/web/server.js"] # ---- cron: periodically triggers internal cron endpoints (e.g. weekly digest) ---- # Minimal alpine + busybox crond image — just curls the running `web` service on a # schedule. Doesn't need the app build output at all, so it's based on `base` # rather than `runner`, keeping the image tiny and independent of the Next build. FROM base AS cron RUN apk add --no-cache curl COPY cron/crontab /etc/crontabs/root COPY cron/run-digest.sh /usr/local/bin/run-digest.sh COPY cron/run-leftover-reminders.sh /usr/local/bin/run-leftover-reminders.sh COPY cron/run-chat-cleanup.sh /usr/local/bin/run-chat-cleanup.sh RUN chmod +x /usr/local/bin/run-digest.sh /usr/local/bin/run-leftover-reminders.sh /usr/local/bin/run-chat-cleanup.sh CMD ["crond", "-f", "-l", "2"]