Devops
Difficulty: Advanced
17 min read

Docker in Production: BuildKit, Compose v2, Multi-stage and Security

Master Docker in a production environment with BuildKit, multi-stage builds, Compose v2, image security and monitoring. A complete and pragmatic guide.

Back to tutorials
Prerequisites
This tutorial assumes a basic knowledge of Docker (images, containers, Dockerfile). If you are just starting out, first read the Docker installation and configuration guide.

Docker in development vs Docker in production

Most teams use Docker in development without difficulty. A docker compose up, a few mounted volumes, and everything works. But moving to production reveals very different challenges: build time, image size, security, resource management, monitoring and reliability.

In development, you tolerate 2 GB images, 10-minute builds and containers running as root. In production, every megabyte counts, every second of build impacts the deployment cycle, and every root container is a potential vulnerability.

This guide covers the techniques and tools essential to running Docker professionally: BuildKit for fast and secure builds, multi-stage builds for minimal images, Compose v2 for orchestration, and the best practices for security, monitoring and networking.

BuildKit: the next-generation build engine

BuildKit has been the default build engine since Docker 23.0 (January 2023). It replaces the legacy builder and brings major improvements in terms of performance and security. If you are using a recent version of Docker, BuildKit is already active.

Enabling and configuring

Check that BuildKit is properly enabled on your system:

# Check the version and the active builder
docker buildx version
docker buildx ls

# Force BuildKit for older versions
export DOCKER_BUILDKIT=1

# Or in /etc/docker/daemon.json
{
  "features": {
    "buildkit": true
  }
}

Create a dedicated builder with advanced options for production:

# Create a builder with a larger cache
docker buildx create --name production-builder \
  --driver docker-container \
  --config /etc/buildkitd.toml \
  --use

# buildkitd.toml configuration
[worker.oci]
  max-parallelism = 4
  gc = true
  gckeepstorage = 10000  # 10 GB of cache
  [[worker.oci.gcpolicy]]
    keepBytes = 5000000000  # 5 GB minimum
    keepDuration = 604800   # 7 days

Cache mounts: speeding up builds

Cache mounts are the most impactful BuildKit feature. They allow package manager caches to persist between builds, eliminating repetitive downloads.

# syntax=docker/dockerfile:1

FROM python:3.12-slim AS builder

WORKDIR /app
COPY requirements.txt .

# Cache mount for pip: downloaded packages are reused
RUN --mount=type=cache,target=/root/.cache/pip \
    pip install --no-compile -r requirements.txt

# Equivalent for Node.js
FROM node:20-alpine AS node-builder
WORKDIR /app
COPY package*.json ./
RUN --mount=type=cache,target=/root/.npm \
    npm ci --production

# Equivalent for apt
FROM debian:bookworm-slim
RUN --mount=type=cache,target=/var/cache/apt \
    --mount=type=cache,target=/var/lib/apt/lists \
    apt-get update && apt-get install -y --no-install-recommends \
    curl ca-certificates
Real-world impact
Cache mounts typically reduce rebuilds by 60 to 90%. A 3-minute pip install drops to 10 seconds when the packages are cached.

Secret mounts: injecting credentials without exposing them

The classic problem: your build needs a token to access a private registry, a Git repo, or an API. With the old builder, the secret ended up in an image layer. BuildKit solves this problem with secret mounts.

# syntax=docker/dockerfile:1

FROM python:3.12-slim

# The secret is mounted read-only, never stored in a layer
RUN --mount=type=secret,id=pip_conf,target=/etc/pip.conf \
    pip install -r requirements.txt

# Access to a private repo via SSH
RUN --mount=type=ssh \
    git clone [email protected]:private/repo.git
# Pass the secret to the build
docker buildx build \
  --secret id=pip_conf,src=$HOME/.pip/pip.conf \
  --ssh default=$SSH_AUTH_SOCK \
  -t myapp:latest .
Warning
Never use ARG or ENV to pass secrets. These values are visible with docker history and stored in the image metadata.

Parallel builds and distributed cache

BuildKit automatically parallelizes the independent stages of a Dockerfile. Combined with a distributed cache via a registry, builds become fast even on ephemeral CI runners.

# Build with distributed cache via a registry
docker buildx build \
  --cache-from type=registry,ref=registry.example.com/myapp:cache \
  --cache-to type=registry,ref=registry.example.com/myapp:cache,mode=max \
  -t myapp:latest \
  --push .

# Local cache for frequent builds
docker buildx build \
  --cache-from type=local,src=/tmp/buildcache \
  --cache-to type=local,dest=/tmp/buildcache,mode=max \
  -t myapp:latest .

The max mode exports all the intermediate layers, not just those of the final image. This maximizes cache hits for the build stages.

Premium Content

This advanced tutorial is reserved for premium members.

9,90€ / month
  • All advanced tutorials
  • New content every week
  • Progress tracking
  • Cancel anytime

Written by

Morgann Riu

Cybersecurity and Linux administration expert. I share my knowledge through free tutorials and training to help system administrators and developers secure their infrastructures.

Frequently asked questions

What is the difference between BuildKit and the classic Docker build?
BuildKit is the next-generation build engine integrated into Docker since version 23.0. It brings automatic parallelization of independent stages, cache mounts to speed up builds (pip, npm, apt), secret mounts to inject credentials without storing them in the layers, and a distributed cache system via registries. A classic build runs the instructions sequentially and does not have these optimizations.
What is the difference between Docker Compose v1 and v2?
Compose v2 is integrated directly as a Docker CLI plugin (docker compose without a hyphen). It is written in Go instead of Python, which makes it faster. It supports profiles to enable services per environment, healthcheck conditions in depends_on, deploy directives to limit resources, and watch mode for automatic reloading in development. Compose v1 (docker-compose with a hyphen) has been deprecated since July 2023.
Should you use Alpine, Distroless or scratch as a base image?
Alpine is a good general compromise: lightweight (about 5 MB), it has a shell and a package manager for debugging. Google's Distroless is ideal for production with high security constraints: no shell, no package manager, minimal attack surface. Scratch is reserved for static binaries (Go, Rust) compiled without any system dependency. In production, favor Distroless for interpreted applications and scratch for static binaries.
How do you integrate Docker vulnerability scanning into a CI/CD pipeline?
Integrate Trivy directly into your CI pipeline as a blocking step after the image build. Configure a severity threshold (for example --severity HIGH,CRITICAL --exit-code 1) to block the deployment if critical vulnerabilities are detected. Combine image scanning with configuration file scanning (trivy config .) to detect misconfigured Dockerfiles. Also schedule regular scans on already-deployed images, since new CVEs are published daily.
How much size reduction do you get with multi-stage builds?
The gains vary depending on the language and the dependencies, but they are generally considerable. A Go application can go from more than 800 MB (image with the full SDK) to less than 10 MB (static binary on scratch). A Node.js application typically goes from 1 GB to 150-200 MB by separating build dependencies from runtime dependencies. A Python application can be reduced from 900 MB to 200 MB by removing the build tools.
How do you manage Docker logs in production?
Avoid the default json-file driver without rotation, as it can fill up the disk. Configure rotation with max-size and max-file in daemon.json, or prefer the journald driver for native systemd integration. For a distributed production stack, use fluentd or fluent-bit to centralize logs to Elasticsearch, Loki or a cloud service. Your applications must write to stdout/stderr so that Docker captures the output correctly.

Share this tutorial

Did you enjoy this article?

Comments

Checklist Sécurité Linux

30 points essentiels pour sécuriser un serveur Linux. Recevez aussi les nouveaux tutoriels par email.

Pas de spam. Désabonnement en 1 clic.