docker

Production-Optimized Dockerfile

Hardened production Dockerfile with security scanning, minimal attack surface, read-only filesystem, and resource limits.

Overview

A production-hardened Dockerfile that goes beyond basic multi-stage builds. Includes security scanning, minimal base images, read-only filesystem support, proper metadata labels, and optimization techniques for the smallest possible attack surface.

Configuration

# Dockerfile

# ── Build Arguments ──
ARG NODE_VERSION=20
ARG ALPINE_VERSION=3.19

# ══════════════════════════════════════
# Stage 1: Install Dependencies
# ══════════════════════════════════════
FROM node:${NODE_VERSION}-alpine${ALPINE_VERSION} AS deps

WORKDIR /app

# Copy only dependency files for optimal caching
COPY package.json package-lock.json ./

# Install production dependencies with clean cache
RUN npm ci --omit=dev && \
    npm cache clean --force

# ══════════════════════════════════════
# Stage 2: Build Application
# ══════════════════════════════════════
FROM node:${NODE_VERSION}-alpine${ALPINE_VERSION} AS build

WORKDIR /app

COPY package.json package-lock.json ./
RUN npm ci

COPY tsconfig.json ./
COPY src/ ./src/

RUN npm run build

# ══════════════════════════════════════
# Stage 3: Production Runtime
# ══════════════════════════════════════
FROM node:${NODE_VERSION}-alpine${ALPINE_VERSION} AS production

# OCI image metadata labels
LABEL org.opencontainers.image.title="My Application"
LABEL org.opencontainers.image.description="Production Node.js application"
LABEL org.opencontainers.image.version="1.0.0"
LABEL org.opencontainers.image.source="https://github.com/org/repo"
LABEL org.opencontainers.image.licenses="MIT"

# Install security updates and remove package manager
RUN apk update && \
    apk upgrade --no-cache && \
    apk add --no-cache tini curl && \
    # Remove package manager to reduce attack surface
    apk del apk-tools && \
    rm -rf /var/cache/apk/* /tmp/*

# Create non-root user with specific UID/GID
RUN addgroup -g 1001 -S appgroup && \
    adduser -u 1001 -S appuser -G appgroup -h /app

WORKDIR /app

# Copy production dependencies
COPY --from=deps --chown=appuser:appgroup /app/node_modules ./node_modules

# Copy built application
COPY --from=build --chown=appuser:appgroup /app/dist ./dist
COPY --from=build --chown=appuser:appgroup /app/package.json ./

# Create directories for runtime data (logs, temp)
RUN mkdir -p /app/logs /app/tmp && \
    chown -R appuser:appgroup /app/logs /app/tmp

# Environment configuration
ENV NODE_ENV=production
ENV PORT=3000
ENV NODE_OPTIONS="--max-old-space-size=512 --enable-source-maps"

# Switch to non-root user
USER appuser

# Expose only necessary port
EXPOSE 3000

# Health check with startup grace period
HEALTHCHECK --interval=30s --timeout=5s --start-period=15s --retries=3 \
    CMD curl -f http://localhost:3000/health || exit 1

# Proper signal handling with tini
ENTRYPOINT ["/sbin/tini", "--"]

# Start application
CMD ["node", "dist/index.js"]
# .dockerignore (must accompany the Dockerfile)

node_modules
npm-debug.log*
.git
.gitignore
.env*
.vscode
.idea
*.md
Dockerfile*
docker-compose*
.dockerignore
coverage
.nyc_output
tests
__tests__
*.test.*
*.spec.*
.eslintrc*
.prettierrc*
tsconfig.json

Key Options Explained

  • ARG for versions — Parameterizes base image versions so they can be updated in CI without editing the Dockerfile.
  • OCI labels — Standard metadata that registries and tools use to display image information. Required for many container scanning tools.
  • apk del apk-tools — Removes the package manager itself from the final image. Prevents attackers from installing tools if they gain access.
  • Specific UID/GID (1001) — Consistent user IDs across environments avoid permission issues with mounted volumes and audit logging.
  • NODE_OPTIONS="--max-old-space-size=512" — Caps V8 heap memory to prevent the container from being OOM-killed. Set to ~75% of your container memory limit.
  • --enable-source-maps — Allows proper stack traces in production error reports even with transpiled TypeScript.
  • .dockerignore — Critical for build performance and security. Prevents sending unnecessary files (tests, docs, secrets) to the Docker daemon.

Common Modifications

  • Distroless base: Replace Alpine with gcr.io/distroless/nodejs20-debian12 for even smaller attack surface (no shell, no package manager).
  • Read-only filesystem: Run with docker run --read-only --tmpfs /app/tmp --tmpfs /app/logs to prevent filesystem writes.
  • Security scanning: Add RUN npm audit --production --audit-level=high in the build stage to fail on known vulnerabilities.
  • Secrets management: Use RUN --mount=type=secret,id=npmrc,target=/app/.npmrc npm ci to access private registries without baking credentials into layers.
  • Resource limits: Set in deployment (not Dockerfile): docker run --memory=1g --cpus=1.0 --pids-limit=100.