Back to Blog

Docker Best Practices: Building Efficient and Secure Images

Learn how to create optimized, secure, and production-ready Docker images with these essential best practices and techniques.

By Sailor Team , February 1, 2026

Docker has revolutionized application deployment, but building efficient and secure container images requires knowledge of best practices. In this guide, we’ll explore techniques to create production-ready Docker images that are fast, secure, and maintainable.

Why Image Optimization Matters

Optimized Docker images provide several benefits:

  • Faster deployments - Smaller images transfer and start quicker
  • Reduced costs - Less storage and bandwidth usage
  • Better security - Smaller attack surface with fewer dependencies
  • Improved reliability - Simpler images are easier to maintain

Multi-Stage Builds

One of the most powerful features for optimization is 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
CMD ["node", "dist/index.js"]

Use Appropriate Base Images

Choose the right base image for your needs:

# Alpine for minimal size
FROM python:3.11-alpine

# Debian slim for compatibility
FROM python:3.11-slim

# Distroless for maximum security
FROM gcr.io/distroless/python3

Layer Caching Strategy

Optimize your Dockerfile to leverage layer caching:

FROM node:18-alpine

WORKDIR /app

# Copy package files first (changes less frequently)
COPY package*.json ./
RUN npm ci --only=production

# Copy source code last (changes more frequently)
COPY . .

EXPOSE 3000
CMD ["npm", "start"]

Security Best Practices

Run as Non-Root User

FROM node:18-alpine

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

WORKDIR /app
COPY --chown=appuser:appuser . .

# Switch to non-root user
USER appuser

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

Scan for Vulnerabilities

# Scan image with Docker Scout
docker scout cves my-image:latest

# Scan with Trivy
trivy image my-image:latest

.dockerignore File

Exclude unnecessary files from your build context:

node_modules
npm-debug.log
.git
.gitignore
README.md
.env
.DS_Store
dist
coverage
*.md

Health Checks

Add health checks to ensure container reliability:

FROM nginx:alpine

COPY nginx.conf /etc/nginx/nginx.conf
COPY --from=builder /app/build /usr/share/nginx/html

HEALTHCHECK --interval=30s --timeout=3s --start-period=5s --retries=3 \
  CMD wget --quiet --tries=1 --spider http://localhost:80/health || exit 1

EXPOSE 80
CMD ["nginx", "-g", "daemon off;"]

Image Tagging Strategy

Use meaningful tags for better version management:

# Semantic versioning
docker build -t myapp:1.2.3 .
docker build -t myapp:1.2 .
docker build -t myapp:1 .
docker build -t myapp:latest .

# Environment-specific tags
docker build -t myapp:dev .
docker build -t myapp:staging .
docker build -t myapp:prod-20260201 .

Reduce Image Size

Combine Commands

# Bad - Creates multiple layers
RUN apt-get update
RUN apt-get install -y curl
RUN apt-get install -y git

# Good - Single layer
RUN apt-get update && \
    apt-get install -y curl git && \
    rm -rf /var/lib/apt/lists/*

Remove Build Dependencies

FROM python:3.11-alpine

RUN apk add --no-cache --virtual .build-deps \
    gcc \
    musl-dev \
    postgresql-dev \
    && pip install --no-cache-dir psycopg2 \
    && apk del .build-deps

Environment Variables

Manage configuration securely:

FROM node:18-alpine

ENV NODE_ENV=production \
    PORT=3000

WORKDIR /app
COPY . .

EXPOSE ${PORT}
CMD ["node", "index.js"]

Practical Example: Full Stack Application

Here’s a complete example combining all best practices:

# Build stage
FROM node:18-alpine AS builder

WORKDIR /app

# Install dependencies
COPY package*.json ./
RUN npm ci --only=production && \
    npm cache clean --force

# Build application
COPY . .
RUN npm run build && \
    rm -rf src tests docs

# Production stage
FROM node:18-alpine

# Security: Create non-root user
RUN addgroup -g 1001 -S nodejs && \
    adduser -u 1001 -S nodejs -G nodejs

WORKDIR /app

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

# Switch to non-root user
USER nodejs

# Add health check
HEALTHCHECK --interval=30s --timeout=3s \
  CMD node healthcheck.js || exit 1

EXPOSE 3000

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

Docker Compose for Development

Create a development environment with Docker Compose:

version: '3.8'

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

  db:
    image: postgres:15-alpine
    environment:
      POSTGRES_PASSWORD: devpassword
    volumes:
      - postgres_data:/var/lib/postgresql/data

  redis:
    image: redis:7-alpine
    ports:
      - "6379:6379"

volumes:
  postgres_data:

Testing Your Images

Always test your images before deployment:

# Build image
docker build -t myapp:test .

# Check image size
docker images myapp:test

# Run security scan
docker scout cves myapp:test

# Test container
docker run --rm -p 3000:3000 myapp:test

# Check running processes
docker exec -it container_id ps aux

# Inspect image layers
docker history myapp:test

Conclusion

Building efficient Docker images is crucial for production deployments. By following these best practices, you’ll create images that are:

  • Smaller - Faster to transfer and deploy
  • More secure - Reduced attack surface
  • More reliable - Easier to maintain and debug
  • More efficient - Better resource utilization

Want to practice Docker? Check out our DevOps Playground for hands-on exercises and real-world scenarios!