# syntax=docker/dockerfile:1 # --------------------------------------------------------------------------- # Stage 1 — builder: install production deps, compiling native addons (bcrypt) # --------------------------------------------------------------------------- FROM node:20-bookworm-slim AS builder WORKDIR /app # Toolchain required to compile native modules (bcrypt) when no prebuilt # binary matches the platform. Lives only in this stage. RUN apt-get update && apt-get install -y --no-install-recommends \ python3 make g++ \ && rm -rf /var/lib/apt/lists/* # Install against the lockfile for reproducible builds. Copy manifests first # so this layer caches until deps actually change. COPY package.json package-lock.json ./ RUN npm ci --omit=dev # --------------------------------------------------------------------------- # Stage 2 — runtime: slim image with only prod node_modules + app source # --------------------------------------------------------------------------- FROM node:20-bookworm-slim AS runtime ENV NODE_ENV=production WORKDIR /app # Compiled node_modules (same base image → ABI-compatible bcrypt .node binary). COPY --from=builder /app/node_modules ./node_modules COPY package.json ./ COPY src ./src # Drop privileges — node:* images ship a non-root `node` user. USER node # Public listener only. INTERNAL_PORT (3001) binds to 127.0.0.1 inside the # container by default and is intentionally NOT published. EXPOSE 3000 # No HTTP health route exists — probe the TCP port directly so the check is # route-agnostic. Orchestrators (k8s) can override with their own probes. HEALTHCHECK --interval=30s --timeout=3s --start-period=20s --retries=3 \ CMD node -e "require('net').connect({port: process.env.PUBLIC_PORT||3000, host:'127.0.0.1'}).on('connect',()=>process.exit(0)).on('error',()=>process.exit(1))" CMD ["node", "src/server.js"]