Back to Blog
DockerContainersDevOpsSecurity

Docker Best Practices: Building Production-Ready Containers

Build efficient, secure Docker containers. Learn multi-stage builds, layer optimization, security hardening, and production patterns.

B
Bootspring Team
Engineering
February 26, 2026
6 min read

Well-crafted Docker images are smaller, faster, and more secure. This guide covers best practices for building production-ready containers.

Multi-Stage Builds#

Node.js Application#

1# Build stage 2FROM node:20-alpine AS builder 3 4WORKDIR /app 5 6# Copy package files first (better layer caching) 7COPY package.json pnpm-lock.yaml ./ 8RUN corepack enable && pnpm install --frozen-lockfile 9 10# Copy source and build 11COPY . . 12RUN pnpm build 13 14# Prune dev dependencies 15RUN pnpm prune --prod 16 17# Production stage 18FROM node:20-alpine AS runner 19 20WORKDIR /app 21 22# Create non-root user 23RUN addgroup --system --gid 1001 nodejs && \ 24 adduser --system --uid 1001 appuser 25 26# Copy only production artifacts 27COPY --from=builder --chown=appuser:nodejs /app/dist ./dist 28COPY --from=builder --chown=appuser:nodejs /app/node_modules ./node_modules 29COPY --from=builder --chown=appuser:nodejs /app/package.json ./ 30 31USER appuser 32 33EXPOSE 3000 34 35CMD ["node", "dist/index.js"]

Go Application#

1# Build stage 2FROM golang:1.22-alpine AS builder 3 4WORKDIR /app 5 6# Download dependencies 7COPY go.mod go.sum ./ 8RUN go mod download 9 10# Build binary 11COPY . . 12RUN CGO_ENABLED=0 GOOS=linux go build -ldflags="-w -s" -o /app/server ./cmd/server 13 14# Production stage - scratch for minimal image 15FROM scratch 16 17# Copy CA certificates for HTTPS 18COPY --from=builder /etc/ssl/certs/ca-certificates.crt /etc/ssl/certs/ 19 20# Copy binary 21COPY --from=builder /app/server /server 22 23EXPOSE 8080 24 25ENTRYPOINT ["/server"]

Layer Optimization#

Order Instructions by Change Frequency#

1# Base image changes rarely 2FROM node:20-alpine 3 4WORKDIR /app 5 6# System dependencies change rarely 7RUN apk add --no-cache tini 8 9# Package files change occasionally 10COPY package.json pnpm-lock.yaml ./ 11RUN corepack enable && pnpm install --frozen-lockfile 12 13# Source code changes frequently 14COPY . . 15 16RUN pnpm build 17 18ENTRYPOINT ["/sbin/tini", "--"] 19CMD ["node", "dist/index.js"]

Minimize Layer Count#

1# Bad: Multiple RUN commands 2RUN apt-get update 3RUN apt-get install -y curl 4RUN apt-get install -y git 5RUN rm -rf /var/lib/apt/lists/* 6 7# Good: Single RUN command 8RUN apt-get update && \ 9 apt-get install -y --no-install-recommends \ 10 curl \ 11 git && \ 12 rm -rf /var/lib/apt/lists/*

Use .dockerignore#

1# .dockerignore 2node_modules 3npm-debug.log 4.git 5.gitignore 6.env 7.env.* 8Dockerfile 9docker-compose*.yml 10.dockerignore 11README.md 12.vscode 13coverage 14.nyc_output 15dist 16*.log

Security Hardening#

Run as Non-Root User#

1FROM node:20-alpine 2 3# Create app user 4RUN addgroup -g 1001 -S appgroup && \ 5 adduser -u 1001 -S appuser -G appgroup 6 7WORKDIR /app 8 9COPY --chown=appuser:appgroup . . 10 11# Switch to non-root user 12USER appuser 13 14CMD ["node", "index.js"]

Use Minimal Base Images#

1# Instead of full images 2FROM node:20 # ~1GB 3FROM python:3.12 # ~1GB 4 5# Use slim or alpine variants 6FROM node:20-slim # ~200MB 7FROM node:20-alpine # ~130MB 8FROM python:3.12-slim # ~150MB 9 10# Or distroless for even smaller images 11FROM gcr.io/distroless/nodejs20-debian12

Scan for Vulnerabilities#

1# Build with security scanning 2FROM node:20-alpine AS builder 3 4# ... build steps ... 5 6# Scan stage 7FROM aquasec/trivy:latest AS scanner 8COPY --from=builder /app /app 9RUN trivy filesystem --exit-code 1 --severity HIGH,CRITICAL /app 10 11# Final stage 12FROM node:20-alpine 13COPY --from=builder /app /app

Don't Store Secrets in Images#

1# Bad: Secret in image 2ENV API_KEY=secret123 3 4# Good: Use runtime secrets 5# docker run -e API_KEY=secret123 myapp 6 7# Or use Docker secrets 8# docker run --secret source=api_key,target=/run/secrets/api_key myapp

Health Checks#

1FROM node:20-alpine 2 3WORKDIR /app 4COPY . . 5RUN npm ci && npm run build 6 7EXPOSE 3000 8 9# Add health check 10HEALTHCHECK --interval=30s --timeout=3s --start-period=5s --retries=3 \ 11 CMD wget --no-verbose --tries=1 --spider http://localhost:3000/health || exit 1 12 13CMD ["node", "dist/index.js"]

Handling Signals#

1FROM node:20-alpine 2 3# Use tini as init system 4RUN apk add --no-cache tini 5 6WORKDIR /app 7COPY . . 8 9# Tini handles signals properly 10ENTRYPOINT ["/sbin/tini", "--"] 11CMD ["node", "index.js"]
1// Handle signals in your app 2process.on('SIGTERM', () => { 3 console.log('SIGTERM received, shutting down gracefully'); 4 server.close(() => { 5 console.log('Server closed'); 6 process.exit(0); 7 }); 8}); 9 10process.on('SIGINT', () => { 11 console.log('SIGINT received, shutting down gracefully'); 12 server.close(() => { 13 process.exit(0); 14 }); 15});

Caching Dependencies#

Node.js with BuildKit Cache#

1# syntax=docker/dockerfile:1 2FROM node:20-alpine 3 4WORKDIR /app 5 6# Use BuildKit cache mount 7RUN --mount=type=cache,target=/root/.npm \ 8 npm ci 9 10COPY . . 11RUN npm run build

Python with pip Cache#

1# syntax=docker/dockerfile:1 2FROM python:3.12-slim 3 4WORKDIR /app 5 6COPY requirements.txt . 7 8# Cache pip downloads 9RUN --mount=type=cache,target=/root/.cache/pip \ 10 pip install -r requirements.txt 11 12COPY . .

Docker Compose for Development#

1# docker-compose.yml 2version: '3.8' 3 4services: 5 app: 6 build: 7 context: . 8 dockerfile: Dockerfile.dev 9 volumes: 10 - .:/app 11 - /app/node_modules # Preserve node_modules 12 ports: 13 - "3000:3000" 14 environment: 15 - NODE_ENV=development 16 - DATABASE_URL=postgres://postgres:password@db:5432/app 17 depends_on: 18 db: 19 condition: service_healthy 20 21 db: 22 image: postgres:16-alpine 23 environment: 24 POSTGRES_PASSWORD: password 25 POSTGRES_DB: app 26 volumes: 27 - postgres_data:/var/lib/postgresql/data 28 healthcheck: 29 test: ["CMD-SHELL", "pg_isready -U postgres"] 30 interval: 5s 31 timeout: 5s 32 retries: 5 33 34volumes: 35 postgres_data:

Development Dockerfile#

1# Dockerfile.dev 2FROM node:20-alpine 3 4WORKDIR /app 5 6# Install development dependencies 7RUN apk add --no-cache git 8 9COPY package.json pnpm-lock.yaml ./ 10RUN corepack enable && pnpm install 11 12# Don't copy source - use volume mount 13EXPOSE 3000 14 15CMD ["pnpm", "dev"]

Production Patterns#

Environment-Specific Builds#

1# Use build arguments 2ARG NODE_ENV=production 3 4FROM node:20-alpine 5 6ENV NODE_ENV=${NODE_ENV} 7 8WORKDIR /app 9 10COPY package.json pnpm-lock.yaml ./ 11RUN corepack enable && \ 12 if [ "$NODE_ENV" = "production" ]; then \ 13 pnpm install --frozen-lockfile --prod; \ 14 else \ 15 pnpm install --frozen-lockfile; \ 16 fi 17 18COPY . . 19 20RUN if [ "$NODE_ENV" = "production" ]; then pnpm build; fi 21 22CMD ["node", "dist/index.js"]

Labels and Metadata#

1FROM node:20-alpine 2 3LABEL org.opencontainers.image.source="https://github.com/myorg/myapp" 4LABEL org.opencontainers.image.description="My Application" 5LABEL org.opencontainers.image.version="1.0.0" 6LABEL org.opencontainers.image.vendor="My Company" 7 8# ... rest of Dockerfile

Debugging#

1# Add debug tools to development image 2FROM node:20-alpine AS base 3 4FROM base AS development 5RUN apk add --no-cache \ 6 curl \ 7 vim \ 8 htop 9 10FROM base AS production 11# Minimal production image

Common Mistakes#

1. Using Latest Tag#

# Bad: Unpredictable builds FROM node:latest # Good: Pin specific version FROM node:20.11.0-alpine3.19

2. Running as Root#

1# Bad: Default root user 2FROM node:20-alpine 3CMD ["node", "index.js"] 4 5# Good: Explicit non-root user 6FROM node:20-alpine 7USER node 8CMD ["node", "index.js"]

3. Not Cleaning Up#

1# Bad: Leaves cache 2RUN apt-get update && apt-get install -y curl 3 4# Good: Clean up in same layer 5RUN apt-get update && \ 6 apt-get install -y --no-install-recommends curl && \ 7 rm -rf /var/lib/apt/lists/*

Conclusion#

Good Docker practices lead to smaller, faster, more secure containers. Use multi-stage builds, optimize layers, run as non-root, and scan for vulnerabilities. Your production containers should be minimal and immutable.

Share this article

Help spread the word about Bootspring