FROM node:22.23.4-alpine AS base
RUN apk add --no-cache libc6-compat openssl postgresql-client bash ffmpeg

# Install dependencies only when needed
FROM base AS deps
WORKDIR /app

COPY package.json package-lock.json* ./
COPY prisma ./prisma/
COPY prisma.config.ts ./prisma.config.ts
# npm ci runs the prepare script, which patches next for next-ws.
RUN npm ci

# Rebuild the source code only when needed
FROM base AS builder
WORKDIR /app
COPY --from=deps /app/node_modules ./node_modules
COPY . .

# Generate Prisma client
RUN npx prisma generate

ENV NEXT_TELEMETRY_DISABLED=1

# Server action IDs are hashed from this key, the action's file path and its
# name. Without one, Next makes a random key for every build, so each release
# gives every action a new ID and a tab opened before the deploy fails its next
# save. The same key on every build keeps the IDs, and those tabs keep working.
# Optional: without the secret the build makes its own key, as before. Next
# writes the key into the build output, so it is not secret once built; never
# reuse the key a deployment sets at run time (NEXT_SERVER_ACTIONS_ENCRYPTION_KEY).
RUN --mount=type=secret,id=next_actions_key \
    if [ -s /run/secrets/next_actions_key ]; then \
      export NEXT_SERVER_ACTIONS_ENCRYPTION_KEY="$(cat /run/secrets/next_actions_key)"; \
    fi && \
    npm run build

# What the container runs beside the server: the Prisma CLI for the migrations
# at start, and tsx with a full Prisma client for the demo seed. Installed in
# a folder of its own, with no package.json beside it. Run inside /app, where
# the standalone build keeps the project's package.json, the same install
# pulled in every production dependency the project has, replaced the patched
# next the build had traced, and made the image well over a gigabyte larger.
FROM base AS tools
WORKDIR /tools
RUN npm install --no-save --no-audit --no-fund \
      prisma@7.6.0 @prisma/client@7.6.0 @prisma/adapter-pg@7.6.0 pg dotenv tsx

# restic, for the scheduled backups a workshop sends to its own storage. The
# release binary is fetched for the platform being built and checked against a
# pinned SHA-256, so a changed or tampered download fails the build instead of
# ending up in the image. Bumping the version means bumping both sums with it.
FROM base AS restic
ARG TARGETARCH
ARG RESTIC_VERSION=0.19.1
RUN set -e; \
    case "$TARGETARCH" in \
      amd64) sum=f415415624dcc452f2a02b8c33641791a8c6d6d3b65bbb3543fcf9a25151585c ;; \
      arm64) sum=a5f64aaab53d51e311fa3829124c5b703f2d14cf187d8640b6be3b2b49376465 ;; \
      *) echo "no pinned restic checksum for TARGETARCH '${TARGETARCH}'"; exit 1 ;; \
    esac; \
    wget -q -O /restic.bz2 \
      "https://github.com/restic/restic/releases/download/v${RESTIC_VERSION}/restic_${RESTIC_VERSION}_linux_${TARGETARCH}.bz2"; \
    echo "$sum  /restic.bz2" | sha256sum -c -; \
    bunzip2 /restic.bz2; \
    chmod 755 /restic

# Production image, copy all the files and run next
FROM base AS runner
WORKDIR /app

ARG APP_VERSION=development
ENV APP_VERSION=${APP_VERSION}
ENV NODE_ENV=production
ENV NEXT_TELEMETRY_DISABLED=1

RUN addgroup --system --gid 1001 nodejs && \
    adduser --system --uid 1001 --home /home/nextjs nextjs

# Copy public assets
COPY --from=builder /app/public ./public

# Set correct permissions for prerender cache
RUN mkdir .next && chown nextjs:nodejs .next

# Copy standalone build
COPY --from=builder --chown=nextjs:nodejs /app/.next/standalone ./
COPY --from=builder --chown=nextjs:nodejs /app/.next/static ./.next/static

# Copy generated Prisma client
COPY --from=builder --chown=nextjs:nodejs /app/src/generated ./src/generated

# The server runs on the node_modules Next traced for it. That is exactly what
# the build ran with: next carrying the next-ws patch the prepare script
# applied in the deps stage, and sharp with the binary for
# this image's platform. Nothing is installed over it, so none of that has to
# be patched or resolved a second time here.
#
# The traced node_modules is not a full install, though: several packages are
# a package.json with their entry point pruned away, and a plain Node script
# run in /app dies on a missing module. So what runs beside the server lives
# in /app/tools with its own complete node_modules: the migrations (see
# init-db.sh) and the demo seed (see .github/workflows/deploy-demo.yml), each
# next to the files it reads.
COPY --from=tools --chown=nextjs:nodejs /tools/node_modules ./tools/node_modules
COPY --from=builder --chown=nextjs:nodejs /app/prisma ./tools/prisma
COPY --from=builder --chown=nextjs:nodejs /app/prisma.config.ts ./tools/prisma.config.ts
COPY --from=builder --chown=nextjs:nodejs /app/src/generated ./tools/src/generated

# Fail the build here rather than at the first live update or certificate
# download. WebSocket routes only work on a next that carries the next-ws
# patch, with next-ws waiting for the lazily loaded route module; a native
# module that cannot be loaded throws while its route is being evaluated,
# which reaches the browser as an empty HTTP 500 with nothing to explain it.
RUN grep -qs "next-ws" node_modules/next/dist/server/next-server.js \
      || { echo "the traced next is missing the next-ws patch"; exit 1; }
# next-ws 2.2.19 waits for the route module itself (ensureUserland); before
# that, scripts/ carried a patch for it, and an older next-ws must not slip back in.
RUN grep -qs "ensureUserland" node_modules/next-ws/dist/server/index.cjs \
      || { echo "the traced next-ws does not wait for the lazily loaded route module"; exit 1; }
RUN node -e "require('sharp'); console.log('sharp loads')"

# Copy init script
COPY --chown=nextjs:nodejs init-db.sh ./init-db.sh
RUN chmod +x ./init-db.sh

# restic runs as a child of the server for scheduled backups. Run it once here
# so a binary that cannot start on this platform fails the build, not the
# first backup a workshop relies on.
COPY --from=restic /restic /usr/local/bin/restic
RUN restic version

# Create the data directories: uploads, the local backup repositories (the
# compose file mounts a volume there) and restic's cache
RUN mkdir -p /app/data/uploads /app/data/backups /app/data/restic-cache \
      && chown -R nextjs:nodejs /app/data

USER nextjs

EXPOSE 3000

ENV PORT=3000
ENV HOSTNAME="0.0.0.0"

ENTRYPOINT ["./init-db.sh"]
CMD ["node", "server.js"]
