FROM node:22-alpine AS builder WORKDIR /app COPY package.json package-lock.json* ./ # `npm ci` installs the locked versions exactly; `npm install` would # silently rewrite package-lock.json mid-build. CI (.gitea/workflows) # also uses `npm ci`, so this keeps the image build deterministic and # matches what the test job validated. RUN npm ci COPY . . RUN npm run build FROM node:22-alpine WORKDIR /app ENV NODE_ENV=production ENV HOST=0.0.0.0 ENV PORT=3000 # node:22-alpine ships a `node` user (UID 1000); use it instead of # running the SvelteKit server as root. COPY --from=builder --chown=node:node /app/build ./build COPY --from=builder --chown=node:node /app/node_modules ./node_modules COPY --from=builder --chown=node:node /app/package.json ./ USER node EXPOSE 3000 # Alpine's busybox `wget` is the canonical lightweight HTTP probe. Probe # 127.0.0.1, not `localhost`: musl resolves `localhost` to IPv6 ::1 first, # but the Node server binds IPv4 0.0.0.0 only, so a localhost probe gets # "connection refused" and the container is wrongly marked unhealthy. Use a # GET (`-O /dev/null`) since `node build` serves 200 on `/`. HEALTHCHECK --interval=30s --timeout=5s --start-period=10s --retries=3 \ CMD wget -q -O /dev/null http://127.0.0.1:3000/ || exit 1 CMD ["node", "build"]