Here are some valuable tips based on Docker documentation to enhance the architecture and quality of your Docker projects.
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
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
Select minimal, trusted base images:
This reduces vulnerabilities and image size. Reference: Building best practices
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.
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
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
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
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
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.