dist/plugins/infra-ci-cd-docker/skills/infra-ci-cd-docker/SKILL.md
Docker containerization patterns for Node.js/TypeScript development and production
npx skillsauth add agents-inc/skills infra-ci-cd-dockerInstall this skill globally with one command. Works with Claude Code, Cursor, and Windsurf.
3 of 9 scanners reported clean
Some scanners were skipped, did not run, or reported a non-clean status. Review each row below.
Quick Guide: Docker with BuildKit for containerizing Node.js/TypeScript applications. Multi-stage builds for minimal production images (1GB to under 100MB). Docker Compose v2 for development environments. BuildKit cache mounts for 10x faster dependency installs. Non-root users, health checks, and secret mounts for production security. Alpine for size, Debian slim for compatibility.
<critical_requirements>
All code must follow project conventions in CLAUDE.md (kebab-case, named exports, import ordering,
import type, named constants)
(You MUST use multi-stage builds for production images - NEVER ship dev dependencies, TypeScript compiler, or source files in production)
(You MUST run containers as non-root user - NEVER run production containers as root (default))
(You MUST use CMD ["node", "server.js"] (exec form) - NEVER use npm start or shell form as CMD)
(You MUST use BuildKit secret mounts for sensitive data at build time - NEVER use ARG or ENV for secrets)
(You MUST copy package.json/lockfile BEFORE source code - NEVER COPY . . before npm ci (breaks layer cache))
</critical_requirements>
Auto-detection: Dockerfile, docker-compose, compose.yaml, Docker, container, multi-stage build, BuildKit, .dockerignore, Docker Compose, docker build, docker run, HEALTHCHECK, Docker Scout, docker init, containerize, container image, Docker network, Docker volume
When to use:
When NOT to use:
Key patterns covered:
.dockerignore for build context optimizationContainers provide reproducible, isolated environments that eliminate "works on my machine" problems. Docker is the standard for packaging Node.js/TypeScript applications into portable, lightweight images.
Core principles:
Multi-stage builds compile TypeScript in a builder stage, then copy only compiled JS and production dependencies into a minimal runtime image. This reduces images from 1GB+ to under 100MB.
A production Dockerfile uses three stages:
# Stage 1: Production dependencies
FROM node:22-alpine AS deps
WORKDIR /app
COPY package.json package-lock.json ./
RUN --mount=type=cache,target=/root/.npm \
npm ci --omit=dev --no-audit --no-fund
# Stage 2: Build TypeScript
FROM node:22-alpine AS builder
WORKDIR /app
COPY package.json package-lock.json ./
RUN --mount=type=cache,target=/root/.npm \
npm ci --no-audit --no-fund
COPY tsconfig.json ./
COPY src/ ./src/
RUN npm run build
# Stage 3: Production runtime
FROM node:22-alpine AS runner
WORKDIR /app
RUN apk add --no-cache tini
ENV NODE_ENV=production
RUN addgroup --system --gid 1001 appgroup && \
adduser --system --uid 1001 --ingroup appgroup appuser
COPY --from=deps --chown=appuser:appgroup /app/node_modules ./node_modules
COPY --from=builder --chown=appuser:appgroup /app/dist ./dist
COPY --chown=appuser:appgroup package.json ./
USER appuser
EXPOSE 3000
ENTRYPOINT ["/sbin/tini", "--"]
CMD ["node", "dist/server.js"]
Why good: Three-stage build separates concerns, BuildKit cache mount speeds up npm ci, non-root user, tini for signal handling, only production artifacts in final image
See examples/core.md for complete Dockerfiles including Bun and monorepo variants.
Docker Compose v2 defines multi-container development environments. Use compose.yaml (not docker-compose.yml) with the docker compose command (no hyphen).
# compose.yaml
services:
app:
build:
context: .
target: builder
volumes:
- .:/app:cached
- /app/node_modules
depends_on:
db:
condition: service_healthy
db:
image: postgres:17-alpine
healthcheck:
test: ["CMD-SHELL", "pg_isready -U dev"]
interval: 10s
timeout: 5s
retries: 5
start_period: 30s
volumes:
- pgdata:/var/lib/postgresql/data
volumes:
pgdata:
Why good: Health checks on dependencies with condition: service_healthy, bind mount with :cached for macOS perf, anonymous volume protects node_modules, named volume for database persistence
See examples/compose.md for full development environments with Redis, networking, and volume patterns.
BuildKit (default since Docker Engine 23+) provides cache mounts that persist package manager caches across builds, reducing install times by 10x or more.
# npm
RUN --mount=type=cache,target=/root/.npm \
npm ci --no-audit --no-fund
# pnpm
RUN --mount=type=cache,target=/root/.local/share/pnpm/store \
pnpm install --frozen-lockfile
Layer ordering rules: Pin base image versions > Copy dependency manifests > Install dependencies > Copy source code > Build/compile. This maximizes cache hits.
See examples/core.md for cache mount patterns for npm, Bun, pnpm, and apt.
RUN addgroup --system --gid 1001 appgroup && \
adduser --system --uid 1001 --ingroup appgroup appuser
COPY --chown=appuser:appgroup --from=builder /app/dist ./dist
USER appuser
# Secret is ephemeral - never persisted in image layers
RUN --mount=type=secret,id=npm_token,env=NPM_TOKEN \
npm ci --no-audit --no-fund
# Build: docker build --secret id=npm_token,env=NPM_TOKEN .
services:
app:
read_only: true
tmpfs: [/tmp]
security_opt: [no-new-privileges:true]
cap_drop: [ALL]
See examples/production.md for complete production compose, secrets management, and CI/CD pipelines.
Health checks enable orchestrators to detect unresponsive containers and restart them automatically.
HEALTHCHECK --interval=30s --timeout=10s --retries=3 --start-period=30s \
CMD ["node", "-e", "fetch('http://localhost:3000/health').then(r => { if (!r.ok) process.exit(1) }).catch(() => process.exit(1))"]
Why good: Uses Node.js built-in fetch (no curl needed in Alpine), start-period allows app startup, checks dedicated health endpoint not just root path
See examples/production.md for health check endpoint implementation and Compose healthcheck patterns.
node_modules
dist
.git
.env
.env.*
!.env.example
Dockerfile*
compose.yaml
__tests__
*.test.ts
coverage
.github
Why good: Excludes node_modules (reinstalled deterministically), .env files (prevents secret leaks), .git (invalidates cache), keeps build context small
See examples/core.md for a comprehensive .dockerignore file.
Node.js as PID 1 does not handle SIGTERM/SIGINT correctly. Use tini as init system.
RUN apk add --no-cache tini
ENTRYPOINT ["/sbin/tini", "--"]
CMD ["node", "dist/server.js"]
Why good: Tini forwards signals to node process, handles zombie process reaping, ensures graceful shutdown on docker stop
See examples/core.md for complete TypeScript graceful shutdown implementation.
</patterns>| Technique | Impact |
| ------------------- | ---------------------------------------- |
| Multi-stage builds | 1GB to under 100MB (90%+ reduction) |
| Alpine base image | 135MB vs 1GB (full) vs 200MB (slim) |
| npm ci --omit=dev | Removes dev dependencies from production |
| .dockerignore | Smaller build context, faster sends |
| Technique | Impact |
| -------------------------------------- | ----------------------------------------------- |
| BuildKit cache mounts | 10x faster dependency installs |
| Layer ordering (deps before source) | Cache hit on source-only changes |
| .dockerignore excluding node_modules | Prevents sending GBs to daemon |
| --no-audit --no-fund flags | Skip unnecessary npm checks |
| Parallel multi-stage builds | BuildKit builds independent stages concurrently |
# GitHub Actions - Cache Docker layers
- uses: docker/setup-buildx-action@v3
- uses: docker/build-push-action@v7
with:
cache-from: type=gha
cache-to: type=gha,mode=max
Why good: GitHub Actions cache (type=gha) persists layers across CI runs, mode=max caches all layers (not just final)
See examples/production.md for complete CI/CD pipeline examples.
</performance><decision_framework>
Which base image?
|
+-- Need smallest image? --> Alpine (node:22-alpine, ~135MB)
| +-- musl libc compatibility issues? --> Use Debian slim instead
|
+-- Need maximum compatibility? --> Debian slim (node:22-slim, ~200MB)
| +-- Native extensions work out of the box
|
+-- Need debugging tools? --> Full image (node:22, ~1GB)
| +-- Development only, never for production
|
+-- Maximum security? --> Distroless (gcr.io/distroless/nodejs22-debian12)
+-- No shell, no package manager, smallest attack surface
How to manage dev vs prod?
|
+-- Multi-stage Dockerfile with targets
| +-- `docker compose build --target builder` for dev
| +-- Final stage for production
|
+-- Separate compose files
+-- compose.yaml (base)
+-- compose.override.yaml (dev - auto-loaded)
+-- compose.prod.yaml (production)
What data needs to persist?
|
+-- Source code (dev hot reload) --> Bind mount with :cached
+-- Database files --> Named volume
+-- Temporary/cache data --> tmpfs
+-- Secrets at runtime --> Docker secrets or tmpfs
+-- node_modules in dev --> Anonymous volume (protect from host)
</decision_framework>
Docker ecosystem tools:
docker scout cves for vulnerability scanning in CLI and CICI/CD integration:
Docker images integrate with any CI/CD pipeline. See examples/production.md for build-push patterns and vulnerability scanning workflows.
Container orchestration:
Production images work with any container orchestrator. Health checks (HEALTHCHECK instruction) and non-root users are universally required regardless of platform.
Replaces:
<red_flags>
High Priority Issues:
--mount=type=secret)npm start as CMD (npm swallows SIGTERM - use CMD ["node", "dist/server.js"]).dockerignore file (node_modules sent to daemon, secrets leaked into image):latest tag for base images (non-deterministic builds)Medium Priority Issues:
COPY . . before npm ci (any file change invalidates dependency cache)docker-compose.yml with version: key (deprecated - use compose.yaml without version)depends_on without condition: service_healthy (container starts before dependency is ready)npm install instead of npm ci in Dockerfile (non-deterministic, slower)Common Mistakes:
/app/node_modules) causing host to overwrite container's modules:cached on bind mounts on macOS (significant performance impact)Gotchas & Edge Cases:
npm rebuild or switch to Debian slimfetch() is available since Node 18+ (no need for curl in health checks on Alpine)# syntax=docker/dockerfile:1 or Docker Engine 23+ with BuildKit enabled by defaultdocker compose down -v removes named volumes (data loss) - use docker compose down without -v to preserve dataCOPY --chown is more efficient than COPY + RUN chown (one layer vs two)tini must be installed explicitly on Alpine (apk add --no-cache tini) - it is NOT included by default in node:alpine imagesnode_modules/.cache directory can grow large in development - add it to .dockerignore</red_flags>
<critical_reminders>
All code must follow project conventions in CLAUDE.md (kebab-case, named exports, import ordering,
import type, named constants)
(You MUST use multi-stage builds for production images - NEVER ship dev dependencies, TypeScript compiler, or source files in production)
(You MUST run containers as non-root user - NEVER run production containers as root (default))
(You MUST use CMD ["node", "server.js"] (exec form) - NEVER use npm start or shell form as CMD)
(You MUST use BuildKit secret mounts for sensitive data at build time - NEVER use ARG or ENV for secrets)
(You MUST copy package.json/lockfile BEFORE source code - NEVER COPY . . before npm ci (breaks layer cache))
Failure to follow these rules will result in bloated images (1GB+), security vulnerabilities (root access, leaked secrets), broken graceful shutdown (lost requests on deploy), and slow CI builds (no layer caching).
</critical_reminders>
development
Material Design component library for Vue 3
development
VitePress 1.x — Vue-powered static site generator for documentation sites, built on Vite
tools
Docusaurus 3.x documentation framework — site configuration, docs/blog plugins, sidebars, versioning, MDX, swizzling, and deployment
development
TanStack Form patterns - useForm, form.Field, validators, arrays, linked fields, createFormHook, type safety