Back to BlogDocker compose · docker

Performance Optimization for Docker Compose in Local and CI/CD

2025-12-29

Docker Compose performance directly impacts development velocity and CI/CD pipeline efficiency. Slow container startup, lengthy image builds, and inefficient resource usage create friction in development workflows and bottleneck deployment pipelines. Understanding and applying performance optimization techniques for both local development and CI/CD environments ensures your containerized applications run efficiently and developers remain productive.

Understanding Performance Bottlenecks

Docker Compose performance issues typically fall into several categories:

  • Build performance: Time to create images from Dockerfiles
  • Startup performance: Time to start containers and services
  • Runtime performance: Container execution efficiency
  • I/O performance: File system and network throughput
  • Image size: Download and storage overhead

Each area requires specific optimization strategies to achieve optimal performance.

Image Layer Caching Optimization

Docker caches image layers during builds. Proper layer ordering maximizes cache hits:

Inefficient layer ordering:

FROM node:18-alpine
WORKDIR /app
COPY . .
RUN npm install
CMD ["npm", "start"]

Every code change invalidates the npm install layer.

Optimized layer ordering:

FROM node:18-alpine
WORKDIR /app
COPY package*.json ./
RUN npm install
COPY . .
CMD ["npm", "start"]

Dependencies install only when package files change, not on every code modification.

BuildKit for Faster Builds

Enable BuildKit for improved build performance:

export DOCKER_BUILDKIT=1
docker-compose build

Or set it permanently:

# docker-compose.yml
version: '3.8'

services:
  app:
    build:
      context: .
    environment:
      - DOCKER_BUILDKIT=1

BuildKit provides:

  • Parallel build step execution
  • Better caching mechanisms
  • Faster dependency resolution
  • Smaller intermediate images

Parallel Service Building

Build multiple services simultaneously:

docker-compose build --parallel

This significantly reduces total build time when you have multiple independent services:

version: '3.8'

services:
  frontend:
    build: ./frontend
  
  backend:
    build: ./backend
  
  worker:
    build: ./worker

All three services build concurrently rather than sequentially.

Image Pull Optimization

Pull base images before building to avoid repeated pulls:

docker-compose pull
docker-compose build

For CI/CD, combine both operations:

docker-compose build --pull

This ensures base images are current while leveraging cached layers from previous builds.

Minimizing Build Context

Reduce build context size with .dockerignore:

# .dockerignore
node_modules/
.git/
.env*
*.log
coverage/
.cache/
dist/
build/
*.md
.DS_Store

Smaller build contexts transfer faster to the Docker daemon, especially important for remote Docker hosts or CI/CD environments.

Efficient Base Image Selection

Choose appropriate base images:

Bloated approach:

FROM ubuntu:22.04
RUN apt-get update && apt-get install -y \
    node npm git curl

Optimized approach:

FROM node:18-alpine

Alpine-based images are significantly smaller (typically 5-10x smaller than full distributions), reducing pull times and storage requirements.

Multi-Stage Build for Size Reduction

Minimize final image size:

# Build stage
FROM node:18 AS builder
WORKDIR /app
COPY package*.json ./
RUN npm ci
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
CMD ["node", "dist/index.js"]

The final image excludes build tools and source files, containing only runtime necessities.

Service Startup Parallelization

Docker Compose starts services with no dependencies in parallel by default. Optimize dependency chains:

Sequential startup (slow):

services:
  cache:
    image: redis:alpine
  
  database:
    image: postgres:14
    depends_on:
      - cache
  
  api:
    image: myapp/api
    depends_on:
      - database

Parallel startup (fast):

services:
  cache:
    image: redis:alpine
  
  database:
    image: postgres:14
  
  api:
    image: myapp/api
    depends_on:
      - cache
      - database

The API depends on both cache and database, but cache and database start simultaneously.

Reducing Container Startup Time

Optimize application startup within containers:

Slow startup:

CMD ["npm", "start"]

This starts npm which then starts node, adding overhead.

Fast startup:

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

Direct node execution eliminates the npm wrapper overhead.

Image Preloading for CI/CD

In CI/CD pipelines, preload commonly used images:

# .gitlab-ci.yml or similar
before_script:
  - docker pull node:18-alpine
  - docker pull postgres:14
  - docker pull redis:alpine
  - docker-compose build

Subsequent builds benefit from cached base images.

Leveraging Docker Layer Caching in CI

Configure CI systems to preserve Docker layer cache:

GitHub Actions:

- name: Set up Docker Buildx
  uses: docker/setup-buildx-action@v2

- name: Build with cache
  uses: docker/build-push-action@v4
  with:
    context: .
    cache-from: type=gha
    cache-to: type=gha,mode=max

GitLab CI:

build:
  stage: build
  image: docker:latest
  services:
    - docker:dind
  variables:
    DOCKER_DRIVER: overlay2
  before_script:
    - docker login -u $CI_REGISTRY_USER -p $CI_REGISTRY_PASSWORD $CI_REGISTRY
  script:
    - docker-compose build --pull
    - docker-compose push
  cache:
    paths:
      - .docker-cache/

Optimizing Docker Daemon Configuration

Configure Docker daemon for better performance:

/etc/docker/daemon.json:

{
  "storage-driver": "overlay2",
  "log-driver": "json-file",
  "log-opts": {
    "max-size": "10m",
    "max-file": "3"
  },
  "max-concurrent-downloads": 10,
  "max-concurrent-uploads": 10
}

The overlay2 storage driver provides better performance than older drivers on most systems.

Efficient Service Dependency Management

Minimize dependency chains for faster parallel startup:

Poor design:

services:
  a:
    image: service:a
  b:
    image: service:b
    depends_on: [a]
  c:
    image: service:c
    depends_on: [b]
  d:
    image: service:d
    depends_on: [c]

All services start sequentially.

Better design:

services:
  a:
    image: service:a
  b:
    image: service:b
  c:
    image: service:c
  d:
    image: service:d
    depends_on: [a, b, c]

Services a, b, and c start in parallel; only d waits for all three.

Reducing Image Pulls in CI/CD

Use image digests for consistent caching:

services:
  app:
    image: myapp/service@sha256:abc123...

Digest references never change, ensuring consistent cache behavior across CI runs.

Optimizing Docker Compose Commands

Use efficient command patterns:

Slow approach:

docker-compose down
docker-compose up --build

Fast approach:

docker-compose up --build --force-recreate

Single command reduces overhead of stopping and removing containers separately.

Utilizing Docker Build Cache Arguments

Pass cache sources for distributed builds:

services:
  app:
    build:
      context: .
      cache_from:
        - myregistry/app:latest
        - myregistry/app:develop

Docker checks these images for usable cached layers during builds.

Minimizing Container Restarts

Configure appropriate restart policies:

services:
  app:
    image: myapp:latest
    restart: "no"  # For CI/CD tests

In CI/CD, avoid restart policies that cause containers to restart on failure, wasting time in failed test runs.

Optimizing Volume Mounts

For local development, optimize volume mount performance:

macOS/Windows - Delegated mode:

services:
  app:
    image: node:18-alpine
    volumes:
      - ./src:/app/src:delegated

Delegated mode prioritizes container performance over immediate host visibility of changes.

Linux - Direct mounts:

services:
  app:
    image: node:18-alpine
    volumes:
      - ./src:/app/src

Linux doesn't need consistency hints; direct mounts are optimal.

Selective Service Starting

Start only needed services:

# Start only specific services
docker-compose up api database

# Start with dependencies
docker-compose up --no-deps api

Reduces startup time when you don't need the entire stack.

Optimizing Container Size

Remove unnecessary files in the same layer they're created:

RUN apt-get update && \
    apt-get install -y package && \
    apt-get clean && \
    rm -rf /var/lib/apt/lists/*

Cleanup in the same RUN statement prevents bloating the image.

Fast Container Image Builds

Combine related operations:

Inefficient:

RUN apt-get update
RUN apt-get install -y curl
RUN apt-get install -y git
RUN apt-get clean

Efficient:

RUN apt-get update && \
    apt-get install -y curl git && \
    apt-get clean && \
    rm -rf /var/lib/apt/lists/*

Single layer is faster to build and smaller in size.

Parallel Container Operations

Execute container commands in parallel:

docker-compose exec -d service1 /app/task.sh &
docker-compose exec -d service2 /app/task.sh &
wait

Background execution with wait completes tasks faster than sequential execution.

Using Minimal Init Systems

For PID 1 efficiency, use lightweight init systems:

FROM node:18-alpine
RUN apk add --no-cache tini
ENTRYPOINT ["/sbin/tini", "--"]
CMD ["node", "server.js"]

Tini handles signal forwarding and zombie reaping with minimal overhead.

Registry Mirror Configuration

Use registry mirrors to accelerate image pulls:

/etc/docker/daemon.json:

{
  "registry-mirrors": [
    "https://mirror.example.com"
  ]
}

Mirrors closer to your location reduce pull times significantly.

Optimizing for CI/CD Pipelines

Structure CI/CD workflows for speed:

# Optimized CI pipeline structure
stages:
  - build
  - test
  - deploy

build:
  stage: build
  script:
    - docker-compose build --parallel
  artifacts:
    paths:
      - docker-compose.yml

test:
  stage: test
  dependencies:
    - build
  script:
    - docker-compose up -d
    - docker-compose exec -T api npm test
    - docker-compose down

Parallel builds and explicit dependencies minimize total pipeline time.

Reusing Built Images in CI

Tag and reuse images across CI stages:

# Build stage
docker-compose build
docker-compose push

# Test stage
docker-compose pull
docker-compose up -d

Pulling prebuilt images is faster than rebuilding in each stage.

Optimizing Healthcheck Intervals

Set appropriate healthcheck frequencies:

services:
  api:
    image: myapp/api
    healthcheck:
      test: ["CMD", "curl", "-f", "http://localhost/health"]
      interval: 30s
      timeout: 10s
      retries: 3
      start_period: 40s

Overly frequent healthchecks waste CPU; too infrequent delays readiness detection. Balance based on your needs.

Reducing Compose File Parse Time

Simplify complex Compose files:

Slow:

services:
  app:
    environment:
      - VAR1=${VAR1}
      - VAR2=${VAR2}
      # ... 50 more variables

Fast:

services:
  app:
    env_file:
      - ./app.env

Env files reduce parse time for large environment configurations.

Container Cleanup Automation

Remove unused containers and images regularly:

# Remove stopped containers
docker-compose rm -f

# Remove unused images
docker image prune -f

# Complete cleanup
docker system prune -af --volumes

In CI/CD, cleanup prevents disk space issues that slow subsequent builds.

Optimizing Network Configuration

Use default networks when possible:

Slower:

services:
  app:
    image: myapp:latest
    networks:
      - custom1
      - custom2
      - custom3

networks:
  custom1:
  custom2:
  custom3:

Faster:

services:
  app:
    image: myapp:latest

Default networks have less overhead than multiple custom networks.

Efficient Log Management

Prevent log accumulation:

services:
  app:
    image: myapp:latest
    logging:
      driver: json-file
      options:
        max-size: "10m"
        max-file: "3"

Unlimited logs consume disk space and slow container operations.

Pre-pulling Images Locally

For local development, pre-pull images:

docker-compose pull

Subsequent docker-compose up commands start faster without pulling images.

Using Composed Image Names

Avoid rebuilding unchanged services:

services:
  app:
    image: myapp/service:${VERSION}
    build: .

Specify both image and build. If the image exists, Compose skips building.

Optimizing Container Entrypoints

Use exec form for faster startup:

Slower:

ENTRYPOINT npm start

Faster:

ENTRYPOINT ["npm", "start"]

Exec form avoids shell overhead.

Caching Package Managers

Cache package manager data:

# For Node.js
FROM node:18-alpine
WORKDIR /app
COPY package*.json ./
RUN npm ci --cache /tmp/npm-cache
COPY . .
# For Python
FROM python:3.11-slim
WORKDIR /app
COPY requirements.txt .
RUN pip install --cache-dir /tmp/pip-cache -r requirements.txt
COPY . .

Caching reduces repeated downloads during development.

Minimizing Service Downtime

Use rolling updates:

docker-compose up -d --scale api=3 --no-recreate
docker-compose up -d --scale api=3 --force-recreate

This updates containers gradually rather than stopping all simultaneously.

Performance Monitoring

Measure and track performance:

# Time entire stack startup
time docker-compose up -d

# Time specific operations
time docker-compose build
time docker-compose pull

Regular measurements identify performance regressions.

Best Practices for Local Development

Use volume mounts for code: Avoid rebuilding for code changes

services:
  app:
    build: .
    volumes:
      - ./src:/app/src

Implement hot reload: Enable application-level hot reloading

services:
  app:
    image: node:18-alpine
    command: npm run dev
    volumes:
      - ./src:/app/src

Limit service scope: Only run services you're actively working on

docker-compose up api database

Best Practices for CI/CD

Cache aggressively: Utilize all available caching mechanisms

Parallelize everything possible: Build and test in parallel

Use specific image tags: Avoid latest for predictable builds

Clean up after builds: Prevent disk space accumulation

Optimize for incremental builds: Structure builds to maximize cache hits

Use build artifacts: Share built images between pipeline stages

Monitor build times: Track and alert on build time increases

Prune regularly: Schedule cleanup jobs to maintain system performance

Docker Compose performance optimization requires attention to image building, caching strategies, service startup patterns, and runtime efficiency. By implementing layer caching optimization, utilizing BuildKit, enabling parallel operations, minimizing image sizes, and configuring appropriate healthchecks and logging, you can dramatically improve both local development velocity and CI/CD pipeline speed. Regular performance monitoring and continuous optimization ensure your containerized workflows remain fast and efficient.

We use cookies to improve your experience and analyse site traffic. See our Privacy Policy.