json
βββ tsconfig.json
**`.dockerignore`**
```text
node_modules
dist
.git
.env
*.log
coverage
.vscode
2. Multi-Stage Dockerfile Implementation
The Dockerfile uses three stages: deps for dependency installation, build for compilation, and runner for the production image. This structure maximizes layer caching; dependency layers are only rebuilt when package.json changes.
Dockerfile
# Stage 1: Dependencies
FROM node:18-alpine AS deps
WORKDIR /app
COPY package.json package-lock.json ./
# Install production dependencies only
RUN npm ci --only=production && \
npm cache clean --force
# Stage 2: Build
FROM node:18-alpine AS build
WORKDIR /app
COPY package.json package-lock.json ./
# Install all dependencies (including dev) for build tools
RUN npm ci
COPY tsconfig.json ./
COPY src ./src
RUN npm run build
# Stage 3: Production Runner
FROM gcr.io/distroless/nodejs18-debian11 AS runner
WORKDIR /app
# Copy only necessary artifacts from previous stages
COPY --from=deps /app/node_modules ./node_modules
COPY --from=build /app/dist ./dist
COPY --from=build /app/package.json ./package.json
# Security: Run as non-root user
# Distroless images run as nonroot by default, but explicit configuration is best practice
USER nonroot:nonroot
# Metadata
EXPOSE 3000
HEALTHCHECK --interval=30s --timeout=3s --start-period=5s --retries=3 \
CMD ["wget", "--no-verbose", "--tries=1", "--spider", "http://localhost:3000/health"]
# Entrypoint
CMD ["dist/index.js"]
3. Architecture Decisions
- Base Image Selection:
node:18-alpine is used for build stages due to the availability of npm and build tooling. gcr.io/distroless/nodejs18-debian11 is selected for the runner. Distroless provides a minimal environment with no shell, package manager, or standard Linux utilities, drastically reducing the attack surface.
- Layer Ordering:
package.json is copied before source code. This isolates the npm ci step in its own layer. When source code changes but dependencies remain static, Docker reuses the cached dependency layer, reducing build times from seconds to milliseconds.
- Non-Root Execution: The
USER nonroot:nonroot directive ensures the process does not run with root privileges. In Distroless, the nonroot user is pre-configured with UID 65532.
- Healthchecks: The
HEALTHCHECK instruction enables orchestrators to detect and restart unhealthy containers automatically. Using wget is compatible with Distroless, as it includes minimal utilities required for health monitoring.
4. Docker Compose for Development Parity
Use docker-compose.yml to align development and production environments, ensuring configuration consistency.
docker-compose.yml
version: '3.8'
services:
app:
build:
context: .
target: runner
ports:
- "3000:3000"
environment:
- NODE_ENV=production
restart: unless-stopped
deploy:
resources:
limits:
cpus: '0.50'
memory: 256M
read_only: true
tmpfs:
- /tmp
- Resource Limits: Explicit CPU and memory limits prevent a single container from starving the host or other containers.
- Read-Only Filesystem:
read_only: true mounts the filesystem as read-only, preventing runtime writes to the image layer. tmpfs provides a temporary writable directory for processes requiring temporary files.
Pitfall Guide
1. The COPY . . Cache Invalidation Trap
Mistake: Copying the entire source directory before installing dependencies.
Impact: Every code change invalidates the dependency installation layer, forcing a full npm install on every build.
Fix: Copy package.json and package-lock.json first, run install, then copy source code.
2. Running as Root
Mistake: Defaulting to the root user inside the container.
Impact: If the container is compromised, the attacker gains root access, potentially escalating to the host kernel via vulnerabilities like runc escapes.
Fix: Always specify a USER directive. Use non-root users provided by the base image or create a dedicated user with minimal permissions.
3. Ignoring .dockerignore
Mistake: Relying on .gitignore or manually excluding files.
Impact: The build context includes node_modules, .git, and local config files. This bloats the context, slows builds, and may leak secrets or local dependencies into the image.
Fix: Maintain a comprehensive .dockerignore file that excludes all non-essential files.
4. Using latest Tags in Production
Mistake: Referencing node:latest or ubuntu:latest in Dockerfiles.
Impact: Builds become non-deterministic. A base image update can introduce breaking changes or new vulnerabilities without warning, causing inconsistent deployments.
Fix: Pin base images to specific versions (e.g., node:18.17.0-alpine) and digest hashes for maximum immutability.
5. Bloated Images with Debug Tools
Mistake: Installing curl, vim, or bash in production images for troubleshooting.
Impact: Increases image size and introduces additional binaries that may contain vulnerabilities. These tools are unnecessary for the application runtime.
Fix: Use multi-stage builds to exclude debug tools. For production debugging, use ephemeral debug containers or kubectl exec with sidecars.
6. Missing Healthchecks
Mistake: Assuming the process PID 1 exit is sufficient for health monitoring.
Impact: Orchestration platforms may continue routing traffic to a container that is running but deadlocked or unresponsive.
Fix: Implement HEALTHCHECK instructions that validate application endpoints or internal state.
7. Hardcoding Secrets
Mistake: Embedding API keys or database credentials in the Dockerfile or environment variables defined in docker-compose.yml.
Impact: Secrets become part of the image history and can be extracted by anyone with access to the image layers.
Fix: Use runtime secret injection mechanisms like Docker Secrets, Kubernetes Secrets, or external vaults. Never bake secrets into images.
Production Bundle
Action Checklist
Decision Matrix
| Scenario | Recommended Approach | Why | Cost Impact |
|---|
| Web API / Microservice | Distroless or Alpine Multi-stage | Minimal footprint, zero shell access, fast startup. | Low storage, low egress, high security ROI. |
| Data Processing / ML | Ubuntu/Debian Slim with specific libs | Requires glibc, CUDA, or complex system dependencies not available in Alpine/Distroless. | Moderate storage, higher base CVE risk requires diligent patching. |
| Static Binary (Go/Rust) | Scratch | No runtime dependencies; binary is self-contained. | Near-zero storage, maximum security isolation. |
| CI/CD Runner | Full OS Image (e.g., Ubuntu) | Requires build tools, SSH, and package managers for job execution. | High storage, isolated ephemeral usage mitigates risk. |
| Legacy Monolith | Alpine with compatibility layer | Legacy apps may require specific glibc versions or tools; Alpine provides small base with package manager. | Moderate size, allows gradual refactoring. |
Configuration Template
Production Dockerfile Template
ARG NODE_VERSION=18.17.0-alpine
ARG DISTROLESS_VERSION=nodejs18-debian11
# Dependencies Stage
FROM node:${NODE_VERSION} AS deps
WORKDIR /app
COPY package*.json ./
RUN npm ci --only=production --ignore-scripts && \
npm cache clean --force
# Build Stage
FROM node:${NODE_VERSION} AS build
WORKDIR /app
COPY package*.json ./
RUN npm ci
COPY tsconfig.json ./
COPY src ./src
RUN npm run build
# Production Stage
FROM gcr.io/distroless/${DISTROLESS_VERSION} AS runner
WORKDIR /app
COPY --from=deps /app/node_modules ./node_modules
COPY --from=build /app/dist ./dist
COPY --from=build /app/package.json ./package.json
USER nonroot:nonroot
EXPOSE 3000
HEALTHCHECK --interval=30s --timeout=3s --start-period=5s --retries=3 \
CMD ["wget", "--no-verbose", "--tries=1", "--spider", "http://localhost:3000/health"]
CMD ["dist/index.js"]
Docker Compose Production Override
version: '3.8'
services:
app:
image: registry.example.com/myapp:${COMMIT_SHA}
read_only: true
tmpfs:
- /tmp
deploy:
resources:
limits:
cpus: '1.0'
memory: 512M
reservations:
cpus: '0.25'
memory: 128M
secrets:
- db_password
environment:
- NODE_ENV=production
- LOG_LEVEL=warn
secrets:
db_password:
external: true
Quick Start Guide
- Initialize Project: Create
package.json, tsconfig.json, and src/index.ts. Install dependencies and build artifacts.
- Create Dockerfile: Copy the Production Dockerfile Template into the project root. Adjust paths and package manager commands as needed.
- Add
.dockerignore: Create .dockerignore with node_modules, dist, .git, and .env.
- Build Image: Run
docker build -t myapp:latest .. Verify the final image size using docker images.
- Run Container: Execute
docker run -p 3000:3000 myapp:latest. Validate the health endpoint and confirm the process runs as a non-root user using docker exec myapp whoami.