Challenge 1: Write a Multi-Stage Dockerfile — Possible Solution ==================================================================== # Dockerfile # --- Stage 1: build --- FROM node:20 AS build WORKDIR /app # Copy only package files first so this layer caches unless dependencies change COPY package.json package-lock.json ./ RUN npm ci COPY tsconfig.json ./ COPY src ./src RUN npm run build # Assumes package.json has: "build": "tsc" # Output goes to /app/dist per tsconfig's "outDir" # --- Stage 2: production --- FROM node:20-slim AS production WORKDIR /app # Only production dependencies, not devDependencies (jest, ts-jest, typescript, etc.) COPY package.json package-lock.json ./ RUN npm ci --omit=dev # Only the compiled JavaScript, not the TypeScript source COPY --from=build /app/dist ./dist ENV NODE_ENV=production EXPOSE 3000 CMD ["node", "dist/main.js"] WHY THIS WORKS -------------- - The build stage installs ALL dependencies (including TypeScript itself, Jest, and any @types/* packages) because compiling needs them — but none of that ever reaches the final image. - The production stage runs `npm ci --omit=dev`, installing only the packages the running app actually needs (Express, a database client, etc.), which keeps the final image smaller and reduces its attack surface. - COPY --from=build /app/dist ./dist pulls only the compiled output across stages — the TypeScript source files, tsconfig.json, and the compiler itself never exist in the final image at all. - Copying package.json/package-lock.json before the rest of the source in the build stage means Docker's layer cache can skip the (slow) npm ci step entirely on rebuilds where only application code changed.