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
ARGfor 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-debian12for even smaller attack surface (no shell, no package manager). - Read-only filesystem: Run with
docker run --read-only --tmpfs /app/tmp --tmpfs /app/logsto prevent filesystem writes. - Security scanning: Add
RUN npm audit --production --audit-level=highin the build stage to fail on known vulnerabilities. - Secrets management: Use
RUN --mount=type=secret,id=npmrc,target=/app/.npmrc npm cito 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.