# syntax=docker/dockerfile:1
#
# One file, two outputs: the `dev` stage docker-compose runs, and the default
# `runtime` stage a release publishes. They were going to be two files, which is
# how the pnpm pin in here already drifted from the one in .github/workflows
# (11.17.0 against 10). A single base stage means there is one version to bump.
#
# RUNTIME CONFIGURATION
#
# Every value this app reads from the environment is declared in src/env.js and
# read per process at startup, not at build time. So one image serves any
# deployment and nothing below bakes a hostname in:
#
#   PUBLIC_DJANGO_API_URL  required. The Django API origin, no trailing /api.
#   PUBLIC_SENTRY_DSN      optional. Unset disables Sentry (`enabled: !!dsn`).
#   PORT                   defaults to 3000. HOST already defaults to 0.0.0.0.
#   PROTOCOL_HEADER        only for plain http. See below.
#   BODY_SIZE_LIMIT        set to 6M below. adapter-node 6 defaults to 512K, which
#                          would refuse a spreadsheet import (the API caps files
#                          at 5 MB) with a plain-text 413.
#
# The origin check is the one that fails quietly. SvelteKit compares the Origin
# header of every form POST with the origin the server thinks it serves, which
# adapter-node 6 builds from the request's Host header and the scheme `https`.
# (The ORIGIN variable that used to override this was removed in SvelteKit 3,
# so setting it does nothing.) Behind a TLS proxy that forwards Host, such as
# nginx with `proxy_set_header Host $host`, that is already right. If users
# reach the app over plain http, set PROTOCOL_HEADER=x-forwarded-proto and have
# the proxy send that header. Otherwise every form POST is answered `403
# Cross-site POST form submissions are forbidden` while GETs still work, so the
# app looks healthy while all its form actions reject.

FROM node:24-slim AS base
# Pinned so image builds are reproducible.
RUN corepack enable && corepack prepare pnpm@11.17.0 --activate
WORKDIR /app

# Manifests only, so this layer caches on the lockfile rather than on source.
# pnpm-workspace.yaml carries the install settings (onlyBuiltDependencies,
# allowBuilds) that pnpm 10+ no longer reads from package.json. Without it
# @sentry/cli's postinstall is silently skipped and its binary never lands.
FROM base AS deps
COPY package.json pnpm-lock.yaml pnpm-workspace.yaml .npmrc ./
RUN pnpm install --frozen-lockfile


# The compose target. Source and node_modules arrive as mounts, so this stage
# deliberately copies no source of its own.
FROM deps AS dev
EXPOSE 5181
CMD ["pnpm", "dev", "--host", "0.0.0.0"]


FROM deps AS builder
COPY . .
RUN pnpm build


# Runtime dependencies only. `build/` is NOT self-contained: SvelteKit keeps
# instrumentation.server.js external so OpenTelemetry can hook the process
# before anything else loads, and it imports @sentry/sveltekit by name. Without
# this stage the server dies at boot on ERR_MODULE_NOT_FOUND.
#
# --ignore-scripts because the `prepare` script shells out to svelte-kit, which
# is a devDependency and therefore absent here. package.json already tolerates
# that with `|| echo ''`; skipping it outright keeps the failure out of the log.
FROM base AS prod-deps
COPY package.json pnpm-lock.yaml pnpm-workspace.yaml .npmrc ./
RUN pnpm install --prod --frozen-lockfile --ignore-scripts


FROM node:24-slim AS runtime
WORKDIR /app
ENV NODE_ENV=production
ENV BODY_SIZE_LIMIT=6M

COPY --from=prod-deps --chown=node:node /app/node_modules ./node_modules
COPY --from=builder --chown=node:node /app/build ./build
# The server reads its own name and version from here on startup.
COPY --chown=node:node package.json ./

# Present in the node images already. The server binds 3000, so it needs no
# privileged port and no reason to be root.
USER node

EXPOSE 3000
CMD ["node", "build/index.js"]
