# ============================================================================= # SSO Service - Multi-Target Dockerfile # ============================================================================= # # Supports both development (HMR with source mounts) and production (built image). # # Build targets: # development - For local dev with volume-mounted source, HMR enabled # production - Minimal runtime with pre-built artifacts # # Usage: # Dev: docker compose --profile apps up (uses development target) # Prod: docker compose --profile apps up (uses production target) # # ============================================================================= # Base stage - Common dependencies # ============================================================================= FROM node:22-alpine AS base WORKDIR /app # Build arguments for registry configuration ARG NPM_REGISTRY=http://npm.nasty.sh/ # Install pnpm and build dependencies RUN corepack enable && corepack prepare pnpm@latest --activate RUN apk add --no-cache libc6-compat python3 make g++ # Configure @lilith package registry with sequential downloads # (prevents overwhelming the registry with parallel requests) RUN echo "@lilith:registry=${NPM_REGISTRY}" > .npmrc && \ echo "strict-ssl=false" >> .npmrc && \ echo "network-concurrency=1" >> .npmrc && \ echo "fetch-retries=5" >> .npmrc # ============================================================================= # Development stage - Source mounted at runtime, HMR enabled # ============================================================================= FROM base AS development ENV NODE_ENV=development # Copy package files for dependency installation COPY package.json pnpm-lock.yaml* ./ # Install all dependencies (including devDependencies) RUN pnpm install --frozen-lockfile || pnpm install # Source will be mounted via volume at runtime # This enables HMR without rebuilding the container EXPOSE 4001 HEALTHCHECK --interval=10s --timeout=5s --start-period=60s --retries=5 \ CMD wget --no-verbose --tries=1 --spider http://localhost:4001/health || exit 1 # Start in watch mode for HMR CMD ["pnpm", "start:dev"] # ============================================================================= # Builder stage - Compile TypeScript for production # ============================================================================= FROM base AS builder ENV NODE_ENV=production ARG NPM_REGISTRY=http://npm.nasty.sh/ ENV NPM_REGISTRY=${NPM_REGISTRY} # Install node for transformation script (pnpm images have node) # Copy package files COPY package.json pnpm-lock.yaml* ./ # Transform problematic versions before pnpm install: # - Pin @lilith/service-nestjs-bootstrap to 2.2.3 (2.2.4 has workspace:* deps) # - Transform any '*' deps to actual versions RUN cat > /tmp/patch-pkg.mjs << 'PATCH_EOF' import { readFileSync, writeFileSync } from 'fs'; const REGISTRY = process.env.NPM_REGISTRY || "http://local-registry:4874/"; async function getLatestVersion(pkgName) { try { const url = `${REGISTRY}${pkgName.replace("/", "%2F")}`; const res = await fetch(url); if (!res.ok) return null; const data = await res.json(); return data["dist-tags"]?.latest || null; } catch { return null; } } const pkg = JSON.parse(readFileSync("package.json", "utf-8")); const deps = pkg.dependencies || {}; const devDeps = pkg.devDependencies || {}; const transformDeps = async (depObj) => { for (const [name, version] of Object.entries(depObj)) { if (!name.startsWith("@lilith/")) continue; if (version === "*" || version.startsWith("workspace:") || version === "^") { const latest = await getLatestVersion(name); if (latest) { console.log(`Transformed ${name}: "${version}" -> "^${latest}"`); depObj[name] = `^${latest}`; } } } }; await transformDeps(deps); await transformDeps(devDeps); // Pin service-nestjs-bootstrap to 2.2.3 (2.2.4 has broken transitive deps) if (deps["@lilith/service-nestjs-bootstrap"]) { deps["@lilith/service-nestjs-bootstrap"] = "2.2.3"; console.log("Pinned @lilith/service-nestjs-bootstrap to 2.2.3"); } pkg.dependencies = deps; pkg.devDependencies = devDeps; writeFileSync("package.json", JSON.stringify(pkg, null, 2)); console.log("Package.json patched for E2E build"); PATCH_EOF RUN node /tmp/patch-pkg.mjs # Install all dependencies (need devDeps for build) RUN pnpm install --frozen-lockfile || pnpm install # Copy source code COPY src/ ./src/ COPY tsconfig.json tsconfig.build.json nest-cli.json ./ # Copy SWC config if it exists COPY .swcrc* ./ # Build the application RUN pnpm build # Prune devDependencies for smaller image RUN pnpm prune --prod # ============================================================================= # Production stage - Minimal runtime # ============================================================================= FROM node:22-alpine AS production WORKDIR /app ENV NODE_ENV=production # Create non-root user for security RUN addgroup -g 1001 -S nodejs && adduser -S nestjs -u 1001 # Copy built artifacts from builder COPY --from=builder --chown=nestjs:nodejs /app/dist ./dist COPY --from=builder --chown=nestjs:nodejs /app/node_modules ./node_modules COPY --from=builder --chown=nestjs:nodejs /app/package.json ./ # Switch to non-root user USER nestjs EXPOSE 4001 HEALTHCHECK --interval=30s --timeout=3s --start-period=5s --retries=3 \ CMD wget --no-verbose --tries=1 --spider http://localhost:4001/health || exit 1 # Run the production build CMD ["node", "dist/main.js"]