forked from
kandake.africa/twinkl
twinkl.social
1# syntax=docker/dockerfile:1
2
3# --- build stage ---
4FROM node:24-alpine AS build
5
6# Pin pnpm for reproducible builds.
7RUN corepack enable && corepack prepare pnpm@11.0.5 --activate
8WORKDIR /app
9
10# Install dependencies against the lockfile.
11COPY package.json pnpm-lock.yaml pnpm-workspace.yaml .npmrc ./
12RUN pnpm install --frozen-lockfile
13
14COPY . .
15ARG TWINKL_VERSION
16ENV TWINKL_VERSION=$TWINKL_VERSION
17# svelte.config.js reads this directly (not via $env/dynamic/public) to bake the
18# tile loader's origins into the CSP frame-src directive, which SvelteKit fixes
19# at build time — so it must be supplied here, not only as a runtime container
20# env var. (The app ALSO reads it at runtime via $env/dynamic/public, to tell
21# the browser-side loader where to fetch tiles from — both are needed.)
22ARG PUBLIC_TILE_LOAD_DOMAIN
23ENV PUBLIC_TILE_LOAD_DOMAIN=$PUBLIC_TILE_LOAD_DOMAIN
24RUN pnpm build && pnpm prune --prod
25
26# --- runtime stage ---
27FROM node:24-alpine AS runtime
28
29ENV NODE_ENV=production
30WORKDIR /app
31
32# adapter-node output + production node_modules. Stateless: no data volume —
33# the browser holds every session, so a restarted/replaced container has
34# nothing to recover (see CLAUDE.md §9 and the residual-server plan).
35COPY --from=build /app/build ./build
36COPY --from=build /app/node_modules ./node_modules
37COPY --from=build /app/package.json ./package.json
38
39RUN addgroup -S twinkl && adduser -S twinkl -G twinkl && chown -R twinkl:twinkl /app
40USER twinkl
41
42EXPOSE 3000
43CMD ["node", "build/index.js"]