Docker and containerization — multi-stage builds, layer caching, security, compose for dev.
Containers are infrastructure as code. Treat your Dockerfile like production code — no shortcuts, no "it works on my machine."
# Stage 1: Build
FROM node:20-alpine AS builder
WORKDIR /app
COPY package*.json ./
RUN npm ci
COPY . .
RUN npm run build
# Stage 2: Run
FROM node:20-alpine AS runner
WORKDIR /app
ENV NODE_ENV=production
RUN addgroup -S appgroup && adduser -S appuser -G appgroup
COPY --from=builder /app/dist ./dist
COPY --from=builder /app/node_modules ./node_modules
COPY --from=builder /app/package.json ./
USER appuser
EXPOSE 3000
HEALTHCHECK --interval=30s --timeout=3s CMD wget -qO- http://localhost:3000/health || exit 1
CMD ["node", "dist/index.js"]
COPY package.json package-lock.json ./
RUN npm ci
COPY . .
npm ci only re-runs when dependencies change, not on every code change.npm ci (clean install), never npm install in Docker — it's deterministic and respects lockfile..dockerignore:
node_modules
.git
.env
.env.*
dist
coverage
.next
*.log
.DS_Store
RUN addgroup -S appgroup && adduser -S appuser -G appgroup
USER appuser
node:20.11-alpine, not node:latest. latest is a moving target — your build will break silently.ENV SECRET_KEY=... in Dockerfile.--no-cache for sensitive builds to avoid caching credentials in layers.docker scout cves <image>.HEALTHCHECK --interval=30s --timeout=3s --start-period=10s --retries=3 \
CMD wget -qO- http://localhost:3000/health || exit 1
services:
app:
build: .
ports:
- "3000:3000"
env_file: .env
volumes:
- .:/app
- /app/node_modules
depends_on:
db:
condition: service_healthy
db:
image: postgres:16-alpine
environment:
POSTGRES_DB: ${DB_NAME}
POSTGRES_USER: ${DB_USER}
POSTGRES_PASSWORD: ${DB_PASSWORD}
volumes:
- pgdata:/var/lib/postgresql/data
healthcheck:
test: ["CMD-SHELL", "pg_isready -U ${DB_USER}"]
interval: 5s
timeout: 3s
volumes:
pgdata:
env_file: .env..env.example committed to repo with placeholder values. .env is in .gitignore.${VAR_NAME} syntax.volumes:
pgdata:
volumes:
- .:/app
- /app/node_modules # Prevent overwriting container's node_modules
/app/node_modules anonymous volume trick prevents the bind mount from overwriting the container's installed dependencies.node:20.11-alpine.RUN apk add --no-cache curl.postgres://db:5432/myapp.ports:.latest tag in production images.docker-compose for production — use Kubernetes, ECS, or similar..dockerignore — it's not optional.ADD when COPY suffices. ADD has magic behaviors (tar extraction, URL fetching) you don't want.