Skip to main content

Docker Container Optimization - Building Production-Ready Images

· 8 min read
Hariprasath Ravichandran
Senior Platform Engineer @ CData

Docker has revolutionized how we package and deploy applications, but creating efficient, secure, and production-ready container images requires more than just writing a basic Dockerfile. In this comprehensive guide, we'll explore advanced Docker optimization techniques that will help you build smaller, faster, and more secure containers.


Disclaimer: Docker®, Kubernetes®, and other product names mentioned in this article are trademarks of their respective owners. All logos and trademarks are used for representation purposes only. No prior copyright or trademark authorization has been obtained. This content is for educational purposes only.


1. Multi-Stage Builds: The Foundation of Optimization

Multi-stage builds are the cornerstone of creating lean production images. They allow you to separate build dependencies from runtime dependencies, dramatically reducing image size.

Before: Single-Stage Build

FROM node:18
WORKDIR /app
COPY package*.json ./
RUN npm install
COPY . .
RUN npm run build
EXPOSE 3000
CMD ["node", "dist/server.js"]

Result: ~1.2GB image including build tools

After: Multi-Stage Build

# Build stage
FROM node:18-alpine AS builder
WORKDIR /app
COPY package*.json ./
RUN npm ci --only=production && npm cache clean --force
COPY . .
RUN npm run build

# Production stage
FROM node:18-alpine
WORKDIR /app
RUN addgroup -g 1001 -S nodejs && adduser -S nodejs -u 1001
COPY --from=builder --chown=nodejs:nodejs /app/dist ./dist
COPY --from=builder --chown=nodejs:nodejs /app/node_modules ./node_modules
COPY --from=builder --chown=nodejs:nodejs /app/package*.json ./
USER nodejs
EXPOSE 3000
CMD ["node", "dist/server.js"]

Result: ~150MB image with only runtime dependencies

Benefits of Multi-Stage Builds

90% smaller images - Only production dependencies in final image
Improved security - No build tools or dev dependencies
Faster deployment - Less data to transfer
Better cache utilization - Independent stage caching

2. Layer Optimization and Caching

Understanding Docker's layer caching mechanism is crucial for fast builds.

Layer Ordering Best Practices

FROM python:3.11-slim

# 1. Install system dependencies (rarely changes)
RUN apt-get update && apt-get install -y --no-install-recommends \
gcc \
libpq-dev \
&& rm -rf /var/lib/apt/lists/*

# 2. Set working directory
WORKDIR /app

# 3. Copy and install dependencies (changes occasionally)
COPY requirements.txt .
RUN pip install --no-cache-dir -r requirements.txt

# 4. Copy application code (changes frequently)
COPY . .

# 5. Runtime configuration
EXPOSE 8000
CMD ["gunicorn", "app:app", "--bind", "0.0.0.0:8000"]

Key Principles

  • Order matters - Place frequently changing layers last
  • Combine commands - Use && to reduce layer count
  • Leverage .dockerignore - Exclude unnecessary files

.dockerignore Example

# Version control
.git
.gitignore

# Documentation
README.md
docs/

# Development files
*.log
*.swp
.env.local
node_modules/
npm-debug.log

# Testing
tests/
*.test.js
coverage/

# CI/CD
.github/
.gitlab-ci.yml
Jenkinsfile

3. Base Image Selection

Choosing the right base image significantly impacts size, security, and performance.

Base Image Comparison

Base ImageSizeUse Case
ubuntu:22.04~77MBLegacy apps requiring full OS
node:18~1GBDevelopment
node:18-slim~240MBProduction with some tools
node:18-alpine~180MBProduction, lightweight
distroless~20MBMaximum security
scratch~0MBStatic binaries only

Using Alpine Linux

FROM golang:1.21-alpine AS builder
WORKDIR /app
COPY go.* ./
RUN go mod download
COPY . .
RUN CGO_ENABLED=0 GOOS=linux go build -a -installsuffix cgo -o main .

FROM alpine:3.19
RUN apk --no-cache add ca-certificates
WORKDIR /root/
COPY --from=builder /app/main .
EXPOSE 8080
CMD ["./main"]

Using Distroless for Maximum Security

FROM golang:1.21 AS builder
WORKDIR /app
COPY . .
RUN CGO_ENABLED=0 GOOS=linux go build -ldflags="-w -s" -o app

FROM gcr.io/distroless/static-debian11
COPY --from=builder /app/app /
EXPOSE 8080
USER nonroot:nonroot
ENTRYPOINT ["/app"]

Distroless benefits:

  • No shell, package manager, or unnecessary binaries
  • Reduced attack surface
  • Smaller image size
  • Only application and runtime dependencies

4. Security Best Practices

Run as Non-Root User

FROM node:18-alpine

# Create app user
RUN addgroup -g 1001 -S appuser && \
adduser -S appuser -u 1001

WORKDIR /app

# Copy and set ownership
COPY --chown=appuser:appuser . .

# Install dependencies
RUN npm ci --only=production

# Switch to non-root user
USER appuser

EXPOSE 3000
CMD ["node", "server.js"]

Scan for Vulnerabilities

# Using Trivy
trivy image myapp:latest

# Using Docker Scout
docker scout cves myapp:latest

# Using Snyk
snyk container test myapp:latest

Implement Security Context

FROM python:3.11-slim

# Security configurations
RUN useradd -m -u 1001 appuser && \
chmod -R 755 /home/appuser

WORKDIR /app

# Copy with correct permissions
COPY --chown=appuser:appuser requirements.txt .
RUN pip install --no-cache-dir -r requirements.txt

COPY --chown=appuser:appuser . .

# Security: Read-only root filesystem
USER appuser

# Health check
HEALTHCHECK --interval=30s --timeout=3s --start-period=5s --retries=3 \
CMD python -c "import requests; requests.get('http://localhost:8000/health')"

EXPOSE 8000
CMD ["python", "app.py"]

5. Performance Optimization

Use BuildKit for Faster Builds

# Enable BuildKit
export DOCKER_BUILDKIT=1
docker build -t myapp:latest .

# Or set in daemon.json
{
"features": {
"buildkit": true
}
}

BuildKit Cache Mounts

# syntax=docker/dockerfile:1.4
FROM node:18-alpine

WORKDIR /app

# Use cache mount for npm
RUN --mount=type=cache,target=/root/.npm \
npm ci --only=production

COPY . .
RUN npm run build

FROM node:18-alpine
COPY --from=0 /app/dist ./dist
CMD ["node", "dist/server.js"]

Parallel Build Stages

FROM node:18-alpine AS deps
WORKDIR /app
COPY package*.json ./
RUN npm ci

FROM deps AS builder
COPY . .
RUN npm run build

FROM deps AS test
COPY . .
RUN npm test

FROM node:18-alpine AS runner
COPY --from=builder /app/dist ./dist
CMD ["node", "dist/server.js"]

6. Image Size Reduction Techniques

Remove Unnecessary Files

FROM node:18-alpine

WORKDIR /app

COPY package*.json ./
RUN npm ci --only=production

COPY . .

# Remove development files
RUN rm -rf \
tests/ \
*.test.js \
.git/ \
docs/ \
README.md \
.env.example

# Optimize permissions
RUN find . -type f -exec chmod 644 {} \; && \
find . -type d -exec chmod 755 {} \;

EXPOSE 3000
CMD ["node", "server.js"]

Compress Binaries

FROM golang:1.21 AS builder
WORKDIR /app
COPY . .
RUN go build -ldflags="-w -s" -o app
RUN upx --best --lzma app # Compress binary with UPX

FROM scratch
COPY --from=builder /app/app /
ENTRYPOINT ["/app"]

7. Docker Compose for Local Development

version: '3.8'

services:
app:
build:
context: .
dockerfile: Dockerfile
target: development
volumes:
- .:/app
- /app/node_modules
ports:
- "3000:3000"
environment:
- NODE_ENV=development
depends_on:
- db
- redis

db:
image: postgres:15-alpine
environment:
POSTGRES_DB: myapp
POSTGRES_USER: user
POSTGRES_PASSWORD: password
volumes:
- postgres_data:/var/lib/postgresql/data
ports:
- "5432:5432"

redis:
image: redis:7-alpine
ports:
- "6379:6379"
volumes:
- redis_data:/data

volumes:
postgres_data:
redis_data:

8. Health Checks and Monitoring

Comprehensive Health Check

FROM node:18-alpine

WORKDIR /app

COPY package*.json ./
RUN npm ci --only=production

COPY . .

# Health check with dependencies
HEALTHCHECK --interval=30s --timeout=10s --start-period=40s --retries=3 \
CMD node healthcheck.js || exit 1

EXPOSE 3000
CMD ["node", "server.js"]

healthcheck.js

const http = require('http');

const options = {
host: 'localhost',
port: 3000,
path: '/health',
timeout: 2000
};

const request = http.request(options, (res) => {
if (res.statusCode === 200) {
process.exit(0);
} else {
process.exit(1);
}
});

request.on('error', () => process.exit(1));
request.end();

9. Advanced Dockerfile Patterns

ARG and ENV Best Practices

# Build-time arguments
ARG NODE_VERSION=18
ARG APP_VERSION=1.0.0

FROM node:${NODE_VERSION}-alpine

# Runtime environment variables
ENV NODE_ENV=production \
APP_VERSION=${APP_VERSION} \
PORT=3000

WORKDIR /app

COPY package*.json ./
RUN npm ci --only=production

COPY . .

EXPOSE ${PORT}

# Use exec form for proper signal handling
CMD ["node", "server.js"]

Dynamic Configuration

FROM nginx:alpine

# Copy custom configuration
COPY nginx.conf /etc/nginx/nginx.conf
COPY default.conf.template /etc/nginx/templates/

# Environment variables will be substituted at runtime
ENV API_HOST=api.example.com \
API_PORT=8080

EXPOSE 80

# nginx will automatically process .template files
CMD ["nginx", "-g", "daemon off;"]

10. CI/CD Integration

GitHub Actions Example

name: Build and Push Docker Image

on:
push:
branches: [main]

jobs:
build:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4

- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v3

- name: Login to Docker Hub
uses: docker/login-action@v3
with:
username: ${{ secrets.DOCKER_USERNAME }}
password: ${{ secrets.DOCKER_TOKEN }}

- name: Build and push
uses: docker/build-push-action@v5
with:
context: .
push: true
tags: |
myorg/myapp:latest
myorg/myapp:${{ github.sha }}
cache-from: type=gha
cache-to: type=gha,mode=max
build-args: |
APP_VERSION=${{ github.sha }}

Best Practices Checklist

Use multi-stage builds for smaller images
Choose minimal base images (alpine, distroless)
Run as non-root user for security
Implement health checks for reliability
Use .dockerignore to exclude unnecessary files
Scan for vulnerabilities regularly
Optimize layer caching for faster builds
Set resource limits in production
Use specific version tags instead of latest
Keep images updated with security patches

Measuring Success

Before Optimization

Repository: myapp
Tag: unoptimized
Size: 1.2GB
Layers: 45
Build time: 5 minutes
Security issues: 127 vulnerabilities

After Optimization

Repository: myapp
Tag: optimized
Size: 85MB (93% reduction)
Layers: 12 (73% reduction)
Build time: 1.5 minutes (70% faster)
Security issues: 3 vulnerabilities (98% reduction)

Conclusion

Container optimization is not just about reducing image size—it's about creating secure, efficient, and maintainable containerized applications. By implementing these techniques, you'll achieve:

  • Faster deployments with smaller images
  • Improved security with minimal attack surface
  • Better performance with optimized layers
  • Cost savings on storage and bandwidth
  • Enhanced developer experience with faster builds

Start implementing these practices today, and remember: optimization is an iterative process. Continuously measure, refine, and improve your Docker workflows.


What optimization techniques have worked best for you? Share your Docker success stories in the comments!