Docker Architecture & Quality Best Practices

Here are some valuable tips based on Docker documentation to enhance the architecture and quality of your Docker projects.

Multi-stage builds for smaller images

Multi-stage builds significantly reduce your final image size by separating build-time dependencies from runtime needs:

FROM node:18 AS builder
WORKDIR /app
COPY package*.json ./
RUN npm install
COPY . .
RUN npm run build

FROM node:18-alpine
WORKDIR /app
COPY --from=builder /app/dist ./dist
COPY --from=builder /app/package*.json ./
RUN npm install --production
CMD ["node", "dist/server.js"]

This approach creates smaller, more secure production images by only including what's necessary to run your application. Reference: Building best practices

Decouple applications into separate containers

Each container should have only one concern. This makes it easier to scale horizontally and reuse containers:

This approach follows the principle of "one process per container" and makes your architecture more maintainable. Reference: Building best practices

Choose the right base image

Select minimal, trusted base images:

This reduces vulnerabilities and image size. Reference: Building best practices

Use BuildKit cache mounts for dependencies

Leverage BuildKit cache mounts (enable BuildKit first: `DOCKER_BUILDKIT=1 docker build ...`) to speed up builds without bloating images:

RUN --mount=type=cache,target=/root/.npm npm install

This caches dependencies (like the npm cache directory) between builds without including them in the image layers themselves.

More details on BuildKit cache mounts can be found in the Docker documentation on build caching.

Don't install unnecessary packages

Only install what your application strictly needs to run. Combine package manager commands and clean up afterwards:

RUN apt-get update && apt-get install -y --no-install-recommends \
    package1 package2 \
    && rm -rf /var/lib/apt/lists/*

This reduces image size, potential attack surface, and build time. Reference: Building best practices

Use Docker Compose for development consistency

Docker Compose helps define and run multi-container Docker applications, maintaining consistent environments:

services:
  web:
    build: .
    ports:
      - "8080:80"
    depends_on:
      db:
        condition: service_healthy # Waits for db to be healthy
  db:
    image: postgres:15
    healthcheck:
      test: ["CMD", "pg_isready", "-U", "postgres"]
      interval: 10s
      timeout: 5s
      retries: 5

This ensures services start in the correct order and are ready before dependent services start. Reference: Common challenges and questions

Leverage Docker Compose service extension

Use the `extends` attribute in `docker-compose.yml` to share common configurations between services or across multiple Compose files:

# common-services.yml
services:
  webapp:
    build: ./app
    environment:
      - COMMON_VAR=value

# docker-compose.yml
services:
  web:
    extends:
      file: common-services.yml
      service: webapp
    ports:
      - "80:8000"
    environment:
      - DEBUG=1 # Overrides or adds to common environment

This promotes DRY (Don't Repeat Yourself) principles in your configuration, making maintenance easier. Reference: Extend your Compose file

Rebuild images regularly

Regularly rebuild your application images to incorporate security updates from base images and dependencies:

# Rebuild without using the layer cache for base image updates
docker build --no-cache -t my-image:my-tag .

# Alternatively, pull the latest base image first, then build (often sufficient)
docker pull node:18-alpine
docker build -t my-image:my-tag .

This ensures you're using the latest security patches. Consider automating this process in your CI/CD pipeline. Reference: Building best practices

Use secrets for sensitive data

In Docker Compose and Swarm, use Docker secrets instead of environment variables for sensitive information like passwords or API keys:

# docker-compose.yml
services:
  app:
    image: my-app
    secrets:
      - db_password
    environment:
      - DB_PASSWORD_FILE=/run/secrets/db_password # App reads password from this file

secrets:
  db_password:
    file: ./secrets/db_password.txt # Source file on host (DO NOT commit this)

Secrets are mounted as temporary files in `/run/secrets/` within the container, preventing accidental exposure in logs, environment variables (`docker inspect`), or version control.

This is a more secure way to handle sensitive data compared to plain environment variables.

These practices will help you build more efficient, secure, and maintainable Docker projects. The key is to focus on creating minimal, purpose-built containers that work together through well-defined interfaces.