Containers2025-10-2911 min read

Docker Best Practices for Production Environments

Share:

Free DevOps Audit Checklist

Get our comprehensive checklist to identify gaps in your infrastructure, security, and deployment processes

Instant delivery. No spam, ever.

Introduction

Docker has revolutionized how we build, ship, and run applications. But there's a significant gap between running Docker on your laptop and deploying containers in production. A poorly configured Docker setup can lead to security vulnerabilities, performance issues, bloated images, and operational nightmares.

In this comprehensive guide, we'll explore battle-tested best practices for running Docker in production environments. These practices have been refined through real-world experience managing thousands of containers across diverse production workloads.

1. Build Minimal, Secure Images

Your Docker image is the foundation of your containerized application. Larger images mean longer deployment times, increased security surface area, and higher storage costs.

Use Minimal Base Images

Choose the smallest base image that meets your needs:

# Bad: Full Debian image (124MB)
FROM debian:11

# Better: Debian slim (74MB)
FROM debian:11-slim

# Best: Alpine Linux (5MB) - if compatible
FROM alpine:3.18

# Excellent: Distroless (2MB) - no shell, minimal attack surface
FROM gcr.io/distroless/python3-debian11

Multi-Stage Builds

Separate build dependencies from runtime dependencies using multi-stage builds:

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

# Production stage
FROM node:18-alpine
WORKDIR /app
COPY --from=builder /app/dist ./dist
COPY --from=builder /app/node_modules ./node_modules
EXPOSE 3000
USER node
CMD ["node", "dist/index.js"]

# Result: 150MB instead of 1GB+

Order Layers by Change Frequency

Docker caches layers. Place frequently changing instructions last:

# Bad: Cache invalidated on every code change
FROM node:18-alpine
WORKDIR /app
COPY . .
RUN npm install
CMD ["node", "index.js"]

# Good: Dependencies cached separately
FROM node:18-alpine
WORKDIR /app
COPY package*.json ./
RUN npm ci --only=production
COPY . .
CMD ["node", "index.js"]

2. Run as Non-Root User

Running containers as root is a critical security vulnerability. If an attacker escapes the container, they have root access to the host system.

Create and Use Non-Root User

FROM node:18-alpine

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

# Set ownership
WORKDIR /app
COPY --chown=nodejs:nodejs . .

# Switch to non-root user
USER nodejs

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

Use Read-Only Root Filesystem

docker run -d   --read-only   --tmpfs /tmp   --tmpfs /var/run   myapp:latest

# In Kubernetes
securityContext:
  readOnlyRootFilesystem: true
  runAsNonRoot: true
  runAsUser: 1001

3. Implement Health Checks

Health checks enable orchestrators to detect and recover from failures automatically. Without them, a container might appear running while the application inside is unresponsive.

Dockerfile Health Check

FROM node:18-alpine
WORKDIR /app
COPY . .

HEALTHCHECK --interval=30s --timeout=3s --start-period=40s --retries=3   CMD node healthcheck.js || exit 1

CMD ["node", "index.js"]

Health Check Script Example

// healthcheck.js
const http = require('http');

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

const request = http.request(options, (res) => {
  console.log(`Health check status: ${res.statusCode}`);
  if (res.statusCode === 200) {
    process.exit(0);
  } else {
    process.exit(1);
  }
});

request.on('error', (err) => {
  console.log('Health check failed:', err);
  process.exit(1);
});

request.end();

Kubernetes Liveness and Readiness Probes

livenessProbe:
  httpGet:
    path: /health
    port: 3000
  initialDelaySeconds: 30
  periodSeconds: 10
  timeoutSeconds: 5
  failureThreshold: 3

readinessProbe:
  httpGet:
    path: /ready
    port: 3000
  initialDelaySeconds: 10
  periodSeconds: 5
  timeoutSeconds: 3
  successThreshold: 1

4. Use .dockerignore

Just like .gitignore, .dockerignore prevents unnecessary files from being copied into your image, reducing size and build time.

# .dockerignore
node_modules
npm-debug.log
.git
.gitignore
README.md
.env
.env.local
.DS_Store
coverage/
.vscode/
.idea/
*.md
docker-compose*.yml
Dockerfile*
.dockerignore
tests/
*.test.js

5. Handle Secrets Securely

Never bake secrets into images. They'll persist in image layers even if you delete them in later layers.

What NOT to Do

# NEVER DO THIS!
FROM node:18-alpine
COPY .env .
ENV API_KEY=my-secret-key-12345
RUN echo "password123" > /app/credentials.txt

Proper Secret Management

# Use environment variables at runtime
docker run -d   -e DATABASE_URL=$DATABASE_URL   -e API_KEY=$API_KEY   myapp:latest

# Use Docker secrets (Swarm)
docker secret create db_password ./password.txt
docker service create   --secret db_password   myapp:latest

# Use Kubernetes secrets
kubectl create secret generic app-secrets   --from-literal=database-url=postgres://...

# Reference in deployment
env:
  - name: DATABASE_URL
    valueFrom:
      secretKeyRef:
        name: app-secrets
        key: database-url

Build-Time Secrets with BuildKit

# Dockerfile
FROM node:18-alpine AS builder
WORKDIR /app

# Mount secret during build only
RUN --mount=type=secret,id=npmrc,target=/root/.npmrc     npm ci --only=production

COPY . .
RUN npm run build

# Secret not present in final image
FROM node:18-alpine
COPY --from=builder /app/dist ./dist
CMD ["node", "dist/index.js"]

# Build with secret
DOCKER_BUILDKIT=1 docker build   --secret id=npmrc,src=$HOME/.npmrc   -t myapp:latest .

6. Optimize for Production Performance

Set Resource Limits

Prevent containers from consuming all host resources:

docker run -d   --memory="512m"   --memory-swap="1g"   --cpus="1.5"   --pids-limit=200   myapp:latest

# Kubernetes resource limits
resources:
  requests:
    memory: "256Mi"
    cpu: "500m"
  limits:
    memory: "512Mi"
    cpu: "1000m"

Enable Application Metrics

FROM node:18-alpine
WORKDIR /app

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

# Copy application
COPY . .

# Expose metrics endpoint
EXPOSE 3000 9090

# Set NODE_ENV for performance
ENV NODE_ENV=production

CMD ["node", "index.js"]

Use Init Process for Proper Signal Handling

FROM node:18-alpine
WORKDIR /app
COPY . .

# Use tini for proper signal handling
RUN apk add --no-cache tini
ENTRYPOINT ["/sbin/tini", "--"]
CMD ["node", "index.js"]

# Or use Docker's built-in init
docker run -d --init myapp:latest

7. Implement Proper Logging

Containers are ephemeral—logs must be externalized for debugging and compliance.

Log to STDOUT/STDERR

// Good: Log to stdout
console.log('Application started on port 3000');
console.error('Database connection failed:', error);

// Bad: Log to files inside container
fs.appendFileSync('/var/log/app.log', 'Message');

// Use structured logging
const logger = require('winston');
logger.info('User logged in', { userId: 123, ip: '1.2.3.4' });

Configure Log Driver

# Docker daemon.json
{
  "log-driver": "json-file",
  "log-opts": {
    "max-size": "10m",
    "max-file": "3",
    "labels": "production_status",
    "env": "os,customer"
  }
}

# Use specific log driver per container
docker run -d   --log-driver=awslogs   --log-opt awslogs-region=us-east-1   --log-opt awslogs-group=myapp   --log-opt awslogs-stream=production   myapp:latest

8. Scan for Vulnerabilities

Container images can contain vulnerable packages. Regular scanning is essential for security.

Scan with Trivy

# Install Trivy
brew install aquasecurity/trivy/trivy

# Scan image
trivy image myapp:latest

# Fail CI on HIGH or CRITICAL vulnerabilities
trivy image --exit-code 1 --severity HIGH,CRITICAL myapp:latest

# Generate SBOM (Software Bill of Materials)
trivy image --format cyclonedx myapp:latest > sbom.json

CI/CD Integration

# GitHub Actions
- name: Build image
  run: docker build -t myapp:${{ github.sha }} .

- name: Scan for vulnerabilities
  uses: aquasecurity/trivy-action@master
  with:
    image-ref: 'myapp:${{ github.sha }}'
    format: 'sarif'
    output: 'trivy-results.sarif'
    severity: 'HIGH,CRITICAL'
    exit-code: '1'

9. Tag Images Properly

Good tagging enables reproducible deployments and easy rollbacks.

Tagging Strategy

# Bad: Using latest in production
docker build -t myapp:latest .

# Good: Use semantic versioning + git commit
docker build -t myapp:1.2.3 .
docker tag myapp:1.2.3 myapp:1.2
docker tag myapp:1.2.3 myapp:1
docker tag myapp:1.2.3 myapp:latest

# Better: Include git commit hash
docker build -t myapp:1.2.3-a1b2c3d .

# Best: Full metadata
docker build   -t myapp:1.2.3   -t myapp:1.2.3-$(git rev-parse --short HEAD)   -t myapp:$(git rev-parse --short HEAD)   --label version=1.2.3   --label git.commit=$(git rev-parse HEAD)   --label build.date=$(date -u +"%Y-%m-%dT%H:%M:%SZ")   .

10. Optimize Build Cache

Use BuildKit

# Enable BuildKit
export DOCKER_BUILDKIT=1

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

# Advanced caching with BuildKit
docker build   --cache-from myapp:latest   --build-arg BUILDKIT_INLINE_CACHE=1   -t myapp:latest .

# Use external cache
docker build   --cache-from type=registry,ref=myapp:buildcache   --cache-to type=registry,ref=myapp:buildcache,mode=max   -t myapp:latest .

11. Production Deployment Checklist

Before deploying containers to production, verify:

Security

  • ✓ Running as non-root user
  • ✓ Read-only root filesystem where possible
  • ✓ No secrets in images or environment variables visible in logs
  • ✓ Vulnerability scanning in CI/CD pipeline
  • ✓ Minimal base image (Alpine, distroless)
  • ✓ Security contexts configured (Kubernetes)
  • ✓ Network policies defined

Reliability

  • ✓ Health checks implemented (liveness and readiness)
  • ✓ Resource limits set (CPU, memory)
  • ✓ Graceful shutdown handling (SIGTERM)
  • ✓ Proper init process (tini or --init)
  • ✓ Multi-replica deployment for high availability
  • ✓ Pod disruption budgets configured (Kubernetes)

Observability

  • ✓ Structured logging to STDOUT/STDERR
  • ✓ Metrics exposed (Prometheus format)
  • ✓ Distributed tracing configured
  • ✓ Log aggregation configured (ELK, CloudWatch)
  • ✓ Monitoring and alerting set up

Performance

  • ✓ Image size optimized (<500MB ideally)
  • ✓ Build cache leveraged
  • ✓ Layer caching optimized
  • ✓ .dockerignore configured
  • ✓ Multi-stage builds used

12. Example Production-Ready Dockerfile

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

# Install build dependencies
RUN apk add --no-cache python3 make g++

WORKDIR /app

# Copy package files
COPY package*.json ./

# Install dependencies with cache mount
RUN --mount=type=cache,target=/root/.npm     npm ci --only=production

# Copy application code
COPY . .

# Build application
RUN npm run build

# Production stage
FROM node:18-alpine

# Install tini for proper init
RUN apk add --no-cache tini

# Create non-root user
RUN addgroup -g 1001 -S nodejs &&     adduser -S nodejs -u 1001

WORKDIR /app

# Copy built application from builder
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 ./

# Set environment
ENV NODE_ENV=production     PORT=3000

# Expose ports
EXPOSE 3000 9090

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

# Switch to non-root user
USER nodejs

# Use tini as init
ENTRYPOINT ["/sbin/tini", "--"]

# Start application
CMD ["node", "dist/index.js"]

# Metadata
LABEL maintainer="devops@instadevops.com"       version="1.0.0"       description="Production-ready Node.js application"

Conclusion

Running Docker in production requires discipline and attention to detail. The practices outlined in this guide—from minimal secure images to proper logging and health checks—are not optional niceties but essential requirements for reliable, secure, and maintainable containerized applications.

Start by implementing the security basics: non-root users, minimal images, and secret management. Then layer on operational excellence with health checks, resource limits, and proper logging. Finally, optimize your builds and implement vulnerability scanning in your CI/CD pipeline.

Remember that containers are not magic—they're a tool. Used correctly with these best practices, they enable faster deployments, better resource utilization, and improved reliability. Used carelessly, they can introduce security vulnerabilities and operational complexity.

Quick Wins to Start Today

  1. Add a .dockerignore file to all projects
  2. Implement health checks in all Dockerfiles
  3. Switch from root to non-root users
  4. Add Trivy scanning to your CI/CD pipeline
  5. Use multi-stage builds to reduce image sizes

Need help containerizing your applications or optimizing existing Docker deployments? InstaDevOps specializes in production-ready container solutions and Kubernetes deployments. Reach out to learn how we can help you build secure, scalable containerized infrastructure.

Ready to Transform Your DevOps?

Get started with InstaDevOps and experience world-class DevOps services.

Book a Free Call

Never Miss an Update

Get the latest DevOps insights, tutorials, and best practices delivered straight to your inbox. Join 500+ engineers leveling up their DevOps skills.

We respect your privacy. Unsubscribe at any time. No spam, ever.