Docker Compose has become the standard tool for defining and running multi-container applications, and the 2026 release cycle has introduced transformative features that every developer needs to master. Whether you are building a microservices backend, a full-stack web application, or an AI inference pipeline, this Docker Compose tutorial walks you through every step – from installation to production-grade deployment – with complete working code and real-world best practices.
With Docker Compose V2 now fully integrated as a Docker CLI plugin and the release of Compose specification v5.0.0 in late 2025, the ecosystem has matured significantly. Container adoption rates continue to climb, with over 90% of CI/CD workflows on major platforms like GitHub Actions now relying on Docker Compose. This tutorial uses the latest stable versions available in March 2026 and covers everything from basic service definitions to advanced patterns like GPU passthrough, watch mode, and modular composition with the include directive.
Prerequisites and Environment Setup
Before diving into this Docker Compose tutorial, you need a properly configured development environment. The following table lists every tool and version required to follow along with this guide. All version numbers are current as of March 2026.
| Tool | Minimum Version | Recommended Version | Purpose |
|---|---|---|---|
| Docker Engine | 26.0 | 29.1+ | Container runtime |
| Docker Compose | 2.25.0 | 2.40+ | Multi-container orchestration |
| Docker Desktop | 4.30 | 4.40+ | GUI management (optional) |
| Node.js | 20 LTS | 22 LTS | Sample application backend |
| Python | 3.11 | 3.13+ | Sample worker service |
| Git | 2.40 | 2.47+ | Version control |
| Operating System | Ubuntu 22.04 / macOS 14 / Windows 11 | Ubuntu 24.04 / macOS 15 | Host platform |
You can verify your Docker installation and Compose version by running the following commands in your terminal. If Docker Compose returns a version below 2.25, upgrade before proceeding.
docker --version
# Expected: Docker version 29.1.x, build xxxxxxx
docker compose version
# Expected: Docker Compose version v2.40.x
docker info --format '{{.ServerVersion}}'
# Expected: 29.1.x
If you are running Linux without Docker Desktop, install the Compose plugin separately using the official repository. On macOS and Windows, Docker Desktop bundles Compose automatically. The key distinction in 2026 is that the legacy docker-compose (with a hyphen) binary is fully deprecated – all commands now use docker compose (with a space) as part of the Docker CLI plugin system.
Understanding Docker Compose Architecture in 2026
Docker Compose operates on a declarative model: you define your entire application stack in a single YAML file – typically named compose.yaml – and Compose handles the creation, networking, and lifecycle management of all containers. In 2026, the architecture has evolved with Compose V2’s plugin-based approach, which integrates directly into the Docker CLI rather than running as a standalone binary.
The Compose specification v5.0.0, released in December 2025 under the codename “Mont Blanc,” introduced several architectural changes. Most notably, the internal builder was removed in favor of delegation to Docker Bake, which aligns the build process with the standard docker build behavior. This means faster builds, better caching, and support for advanced BuildKit features like multi-platform builds out of the box.
At its core, a Docker Compose file defines three primary objects: services (your containers), networks (how containers communicate), and volumes (persistent data storage). Services can be built from Dockerfiles or pulled from registries. Networks isolate communication between service groups. Volumes ensure data survives container restarts. Understanding this triad is fundamental to every docker compose example in this tutorial.
Compose also manages the lifecycle of your application through a set of commands: docker compose up starts all services, docker compose down tears them down, and docker compose watch enables hot-reloading during development. The 2026 toolchain has optimized startup times by up to 40% compared to early V2 releases, making Compose suitable even for large stacks with dozens of services.
Step 1: Create the Project Structure
Every Docker Compose tutorial begins with a well-organized project structure. We are building a complete task management application with three services: a Node.js API, a Python background worker, and a PostgreSQL database. Create the following directory layout on your machine.
mkdir -p taskflow/{api,worker,db/init}
cd taskflow
# Create the main compose file
touch compose.yaml
# Create application files
touch api/server.js api/package.json api/Dockerfile
touch worker/main.py worker/requirements.txt worker/Dockerfile
touch db/init/schema.sql
touch .env
This structure separates each service into its own directory with a dedicated Dockerfile, following the monorepo pattern that has become standard for Docker Compose projects. The db/init folder holds SQL scripts that PostgreSQL will execute on first startup, and the .env file stores environment variables shared across services. Notice we use compose.yaml – not docker-compose.yml – which is the preferred filename in the 2026 Compose specification.
Step 2: Write Your First Docker Compose File
The docker compose file is the heart of your application. Here is the complete compose.yaml for our TaskFlow project. Each service is defined with its build context, port mappings, environment variables, health checks, and dependency chain. Study this file carefully – we will break down every section in the steps that follow.
name: taskflow
services:
api:
build:
context: ./api
dockerfile: Dockerfile
ports:
- "3000:3000"
environment:
- DATABASE_URL=postgres://taskuser:taskpass@db:5432/taskflow
- REDIS_URL=redis://cache:6379
- NODE_ENV=development
depends_on:
db:
condition: service_healthy
cache:
condition: service_started
networks:
- frontend
- backend
develop:
watch:
- action: sync
path: ./api
target: /app
ignore:
- node_modules/
- action: rebuild
path: ./api/package.json
healthcheck:
test: ["CMD", "curl", "-f", "http://localhost:3000/health"]
interval: 10s
timeout: 5s
retries: 3
start_period: 15s
worker:
build:
context: ./worker
dockerfile: Dockerfile
environment:
- DATABASE_URL=postgres://taskuser:taskpass@db:5432/taskflow
- REDIS_URL=redis://cache:6379
depends_on:
db:
condition: service_healthy
cache:
condition: service_started
networks:
- backend
profiles:
- with-worker
deploy:
replicas: 2
resources:
limits:
cpus: "0.50"
memory: 256M
restart: unless-stopped
db:
image: postgres:17-alpine
environment:
- POSTGRES_DB=taskflow
- POSTGRES_USER=taskuser
- POSTGRES_PASSWORD=taskpass
volumes:
- pgdata:/var/lib/postgresql/data
- ./db/init:/docker-entrypoint-initdb.d:ro
ports:
- "5432:5432"
networks:
- backend
healthcheck:
test: ["CMD-SHELL", "pg_isready -U taskuser -d taskflow"]
interval: 5s
timeout: 3s
retries: 5
start_period: 10s
cache:
image: redis:7-alpine
ports:
- "6379:6379"
volumes:
- redisdata:/data
networks:
- backend
command: redis-server --appendonly yes --maxmemory 128mb --maxmemory-policy allkeys-lru
networks:
frontend:
driver: bridge
backend:
driver: bridge
volumes:
pgdata:
driver: local
redisdata:
driver: local
This docker compose yml defines four services working together. The API server handles HTTP requests, the worker processes background tasks, PostgreSQL stores persistent data, and Redis provides caching and message queuing. The depends_on block with condition: service_healthy ensures the database is fully ready before the API starts – a critical pattern for avoiding connection errors on startup.
Step 3: Build the Node.js API Service
The API service is a lightweight Express.js server that exposes REST endpoints for task management. Create the api/package.json with the following contents. We use Express 5, released in late 2025, which brings native async error handling and improved routing performance.
// api/package.json
{
"name": "taskflow-api",
"version": "1.0.0",
"scripts": {
"start": "node server.js",
"dev": "node --watch server.js"
},
"dependencies": {
"express": "^5.1.0",
"pg": "^8.13.0",
"redis": "^4.7.0"
}
}
// api/server.js
const express = require('express');
const { Pool } = require('pg');
const { createClient } = require('redis');
const app = express();
app.use(express.json());
const pool = new Pool({
connectionString: process.env.DATABASE_URL,
});
const redis = createClient({ url: process.env.REDIS_URL });
redis.connect().catch(console.error);
app.get('/health', async (req, res) => {
try {
await pool.query('SELECT 1');
res.json({ status: 'healthy', timestamp: new Date().toISOString() });
} catch (err) {
res.status(503).json({ status: 'unhealthy', error: err.message });
}
});
app.get('/tasks', async (req, res) => {
const cached = await redis.get('tasks:all');
if (cached) return res.json(JSON.parse(cached));
const { rows } = await pool.query(
'SELECT * FROM tasks ORDER BY created_at DESC LIMIT 100'
);
await redis.setEx('tasks:all', 60, JSON.stringify(rows));
res.json(rows);
});
app.post('/tasks', async (req, res) => {
const { title, description, priority } = req.body;
const { rows } = await pool.query(
'INSERT INTO tasks (title, description, priority) VALUES ($1, $2, $3) RETURNING *',
[title, description || '', priority || 'medium']
);
await redis.del('tasks:all');
res.status(201).json(rows[0]);
});
app.patch('/tasks/:id', async (req, res) => {
const { status } = req.body;
const { rows } = await pool.query(
'UPDATE tasks SET status = $1, updated_at = NOW() WHERE id = $2 RETURNING *',
[status, req.params.id]
);
await redis.del('tasks:all');
res.json(rows[0]);
});
const PORT = process.env.PORT || 3000;
app.listen(PORT, '0.0.0.0', () => {
console.log(`TaskFlow API running on port ${PORT}`);
});
Now create the Dockerfile for the API service. This uses a multi-stage build pattern to minimize the final image size – a best practice that reduces attack surface and speeds up deployments.
# api/Dockerfile
FROM node:22-alpine AS builder
WORKDIR /app
COPY package.json ./
RUN npm install --production
FROM node:22-alpine
WORKDIR /app
RUN apk add --no-cache curl
COPY --from=builder /app/node_modules ./node_modules
COPY . .
EXPOSE 3000
USER node
CMD ["node", "server.js"]
The multi-stage build copies only production dependencies into the final image, excluding development tooling and build artifacts. The USER node directive ensures the container does not run as root – a security best practice that many developers overlook. The curl package is included solely for the health check defined in our docker compose file.
Step 4: Build the Python Worker Service
The background worker processes tasks asynchronously by polling the database for pending items. This pattern is common in production systems where long-running operations (email sending, report generation, image processing) must not block the API response cycle.
# worker/requirements.txt
psycopg2-binary==2.9.10
redis==5.2.1
# worker/main.py
import os
import time
import json
import psycopg2
import redis
DATABASE_URL = os.environ["DATABASE_URL"]
REDIS_URL = os.environ["REDIS_URL"]
def get_db_connection():
return psycopg2.connect(DATABASE_URL)
def get_redis_client():
return redis.from_url(REDIS_URL)
def process_pending_tasks():
conn = get_db_connection()
cache = get_redis_client()
cur = conn.cursor()
cur.execute("""
UPDATE tasks SET status = 'processing', updated_at = NOW()
WHERE id = (
SELECT id FROM tasks
WHERE status = 'pending'
ORDER BY priority DESC, created_at ASC
LIMIT 1
FOR UPDATE SKIP LOCKED
)
RETURNING id, title, priority;
""")
row = cur.fetchone()
if row:
task_id, title, priority = row
print(f"Processing task {task_id}: {title} (priority: {priority})")
time.sleep(2) # Simulate work
cur.execute(
"UPDATE tasks SET status = 'completed', updated_at = NOW() WHERE id = %s",
(task_id,)
)
conn.commit()
cache.delete("tasks:all")
print(f"Completed task {task_id}")
else:
print("No pending tasks found")
cur.close()
conn.close()
if __name__ == "__main__":
print("TaskFlow Worker started")
while True:
try:
process_pending_tasks()
except Exception as e:
print(f"Error processing task: {e}")
time.sleep(5)
Now create the worker Dockerfile. We use Python 3.13-slim as the base image for a balance between size and compatibility.
# worker/Dockerfile
FROM python:3.13-slim
WORKDIR /app
COPY requirements.txt .
RUN pip install --no-cache-dir -r requirements.txt
COPY . .
USER nobody
CMD ["python", "-u", "main.py"]
The FOR UPDATE SKIP LOCKED SQL pattern is critical when running multiple worker replicas. It ensures that two workers never pick up the same task simultaneously – a common pitfall in distributed task processing. Our docker compose file configures the worker service with replicas: 2 under the deploy key, and the profiles: ["with-worker"] setting means workers only start when you explicitly request the profile.
Step 5: Configure the Database and Initialize Schema
PostgreSQL 17, used in this Docker Compose tutorial, automatically executes any SQL or shell scripts placed in the /docker-entrypoint-initdb.d directory on first startup. Create the initialization script to set up our tasks table with proper indexes and constraints.
-- db/init/schema.sql
CREATE TABLE IF NOT EXISTS tasks (
id SERIAL PRIMARY KEY,
title VARCHAR(255) NOT NULL,
description TEXT DEFAULT '',
priority VARCHAR(20) DEFAULT 'medium'
CHECK (priority IN ('low', 'medium', 'high', 'critical')),
status VARCHAR(20) DEFAULT 'pending'
CHECK (status IN ('pending', 'processing', 'completed', 'failed')),
created_at TIMESTAMP WITH TIME ZONE DEFAULT NOW(),
updated_at TIMESTAMP WITH TIME ZONE DEFAULT NOW()
);
CREATE INDEX idx_tasks_status ON tasks(status);
CREATE INDEX idx_tasks_priority_created ON tasks(priority DESC, created_at ASC);
-- Seed data for testing
INSERT INTO tasks (title, description, priority) VALUES
('Set up CI/CD pipeline', 'Configure GitHub Actions for automated testing', 'high'),
('Write API documentation', 'Document all REST endpoints with OpenAPI', 'medium'),
('Optimize database queries', 'Add missing indexes and review slow queries', 'high'),
('Update dependencies', 'Run npm audit and update vulnerable packages', 'critical'),
('Add rate limiting', 'Implement rate limiting on public API endpoints', 'medium');
The schema uses CHECK constraints to enforce valid values for priority and status columns. The composite index on (priority DESC, created_at ASC) directly supports the worker’s query pattern, ensuring efficient task selection under load. The volume mount pgdata:/var/lib/postgresql/data in our compose file guarantees data persistence across container restarts.
Step 6: Launch, Test, and Interact with Your Stack
With all files in place, launch the application. The first run takes longer because Docker needs to pull base images and build your custom images. Subsequent starts use cached layers and complete in seconds.
# Start the core services (API, database, cache)
docker compose up --build -d
# Verify all services are running
docker compose ps
# Expected output:
# NAME IMAGE COMMAND SERVICE STATUS
# taskflow-api-1 taskflow-api "node server.js" api Up (healthy)
# taskflow-db-1 postgres:17-alpine "docker-entrypoint.s…" db Up (healthy)
# taskflow-cache-1 redis:7-alpine "docker-entrypoint.s…" cache Up
# Check the API health endpoint
curl http://localhost:3000/health
# {"status":"healthy","timestamp":"2026-03-23T10:15:42.123Z"}
# Create a task
curl -X POST http://localhost:3000/tasks
-H "Content-Type: application/json"
-d '{"title":"Learn Docker Compose","description":"Complete the tutorial","priority":"high"}'
# List all tasks
curl http://localhost:3000/tasks
# Start workers with the profile flag
docker compose --profile with-worker up -d worker
# Watch worker logs
docker compose logs -f worker
The --build flag forces a rebuild of custom images, ensuring your latest code changes are included. The -d flag runs containers in detached mode. Notice how the worker service only starts when you pass --profile with-worker – this is the profiles feature that enables optional services in your docker compose yml without cluttering the default startup. You can define multiple profiles for different environments: debug, monitoring, with-worker, or any custom grouping your project needs.
Step 7: Enable Watch Mode for Hot-Reloading Development
One of the most powerful features in the modern Docker Compose toolchain is watch mode, which automatically syncs file changes into running containers or triggers rebuilds when configuration files change. This eliminates the tedious cycle of manually restarting containers during development.
Our compose file already includes the develop.watch configuration for the API service. The sync action mirrors file changes from your host’s ./api directory into the container’s /app directory in real time, while excluding node_modules/. The rebuild action triggers a full container rebuild when package.json changes, ensuring new dependencies are installed automatically.
# Start watch mode
docker compose watch
# In another terminal, edit api/server.js
# The changes are synced automatically — no restart needed
# If you add a new dependency to package.json,
# the container rebuilds automatically
# Watch mode output:
# watch enabled for service "api"
# Syncing ./api to /app in service "api"
# Rebuilding service "api" (package.json changed)
Watch mode uses filesystem events rather than polling, making it efficient even with large codebases. In the 2026 Compose release, watch mode delegates builds to Docker Bake, which provides better parallelization and caching compared to the legacy internal builder. For teams migrating from tools like nodemon or air, watch mode provides a container-native alternative that works identically across all development machines regardless of host OS.
Step 8: Use Environment Variables and the .env File
Hardcoding credentials in your docker compose file is a common mistake that leads to security vulnerabilities and inflexible deployments. The proper approach uses a .env file for default values and environment-specific overrides. Docker Compose automatically loads variables from a .env file in the project root.
# .env
POSTGRES_DB=taskflow
POSTGRES_USER=taskuser
POSTGRES_PASSWORD=taskpass
REDIS_MAXMEMORY=128mb
API_PORT=3000
NODE_ENV=development
# Updated compose.yaml service using variables:
# db:
# image: postgres:17-alpine
# environment:
# - POSTGRES_DB=${POSTGRES_DB}
# - POSTGRES_USER=${POSTGRES_USER}
# - POSTGRES_PASSWORD=${POSTGRES_PASSWORD}
# Override for production — create compose.prod.yaml:
# docker compose -f compose.yaml -f compose.prod.yaml up -d
# Or use environment-specific .env files:
# docker compose --env-file .env.production up -d
The variable interpolation syntax ${VARIABLE} pulls values from the .env file or from your shell environment. Shell environment variables take precedence over .env file values, enabling CI/CD systems to inject secrets without modifying files. For production deployments, consider using Docker secrets or an external vault like HashiCorp Vault instead of plain-text environment files.
Step 9: Implement Networking and Service Communication
Docker Compose creates an isolated network for your application by default, but explicitly defining networks gives you fine-grained control over which services can communicate with each other. Our compose file defines two networks: frontend and backend.
The API service connects to both networks because it needs to receive external HTTP requests (frontend) and communicate with the database and cache (backend). The database and cache services only connect to the backend network, making them inaccessible from outside the Docker network. This network segmentation is a security best practice that limits the blast radius if any single container is compromised.
Service names in Docker Compose automatically resolve as DNS hostnames within shared networks. When the API connects to postgres://taskuser:taskpass@db:5432/taskflow, the hostname db resolves to the PostgreSQL container’s IP address on the backend network. This DNS-based service discovery means you never hardcode IP addresses and your configuration remains portable across environments.
| Network Pattern | Use Case | Services | External Access |
|---|---|---|---|
| Single default network | Simple applications, prototyping | All services | Via published ports |
| Frontend + Backend separation | Web applications with databases | API on both; DB on backend only | API only |
| Per-service isolation | Microservices with strict boundaries | Each pair gets own network | Gateway only |
| External network attachment | Shared services across projects | Reverse proxy, monitoring | Configurable |
| Host network mode | Performance-critical, low-latency | Single service | All ports exposed |
For production environments, consider using an overlay network if you need cross-host communication in Docker Swarm or Kubernetes. The bridge driver used in our docker compose example is optimized for single-host development and testing scenarios.
Step 10: Add the Include Directive for Modular Composition
As projects grow beyond a handful of services, a single compose file becomes unwieldy. The include directive, stabilized in Compose 2.24+, enables modular file composition by splitting services into separate files that are merged at runtime. This is a significant improvement over the older extends keyword and the multiple -f flag approach.
# compose.yaml (modular version)
name: taskflow
include:
- path: ./infra/compose.db.yaml
- path: ./infra/compose.cache.yaml
- path: ./infra/compose.monitoring.yaml
env_file: .env.monitoring
services:
api:
build:
context: ./api
ports:
- "3000:3000"
depends_on:
db:
condition: service_healthy
networks:
- frontend
- backend
# infra/compose.db.yaml
services:
db:
image: postgres:17-alpine
environment:
- POSTGRES_DB=${POSTGRES_DB}
- POSTGRES_USER=${POSTGRES_USER}
- POSTGRES_PASSWORD=${POSTGRES_PASSWORD}
volumes:
- pgdata:/var/lib/postgresql/data
networks:
- backend
healthcheck:
test: ["CMD-SHELL", "pg_isready -U ${POSTGRES_USER}"]
interval: 5s
timeout: 3s
retries: 5
volumes:
pgdata:
networks:
backend:
The include directive merges the referenced files into the main composition. Each included file is a valid standalone Compose file, meaning it can define its own volumes, networks, and services. This pattern enables infrastructure teams to maintain database and cache configurations independently from application developers – a crucial separation of concerns in organizations with platform engineering teams.
Common Pitfalls and How to Avoid Them
After building hundreds of Docker Compose configurations, certain mistakes appear repeatedly. Learning to recognize and prevent these pitfalls saves hours of debugging. Here are the most common issues developers encounter in 2026, along with their solutions.
Pitfall 1: Using depends_on without health checks. The basic depends_on only waits for a container to start, not for the service inside it to be ready. Your API will crash with connection refused errors if the database container starts but PostgreSQL has not finished initialization. Always use condition: service_healthy with a proper healthcheck block.
Pitfall 2: Running containers as root. By default, Docker containers run as the root user. If an attacker exploits a vulnerability in your application, they gain root access inside the container – and potentially to the host via privilege escalation. Always include USER node, USER nobody, or a custom non-root user in your Dockerfile.
Pitfall 3: Forgetting to persist data with volumes. Without a named volume, your database data lives in the container’s writable layer. Running docker compose down destroys this data permanently. Named volumes like pgdata:/var/lib/postgresql/data persist across container lifecycles. Use docker compose down -v only when you intentionally want to destroy volumes.
Pitfall 4: Exposing database ports to the host. Publishing port 5432 with ports: - "5432:5432" makes your database accessible from any network interface. In development this is convenient, but in production it is a critical security risk. Remove the ports mapping for database services in production and let services communicate via internal Docker networks only.
Pitfall 5: Using the latest tag for images. The latest tag is mutable and can change without warning. A docker compose pull might bring in a breaking change. Always pin specific versions like postgres:17-alpine or redis:7-alpine so your builds remain reproducible.
Pitfall 6: Ignoring .dockerignore files. Without a .dockerignore file, Docker sends your entire build context – including node_modules/, .git/, and .env files – to the daemon. This slows builds dramatically and risks leaking secrets into your images. Always create a .dockerignore with entries for node_modules, .git, .env, and any test or documentation directories.
Pitfall 7: Neglecting resource limits. Containers without resource limits can consume all available CPU and memory on the host, causing other services to crash. Always define deploy.resources.limits in your compose file, especially for worker services that might enter infinite loops or process large datasets.
Thorough Troubleshooting Guide
Even experienced developers encounter issues with Docker Compose. This troubleshooting section covers the eight most common problems, their root causes, and step-by-step solutions tested in March 2026.
| Issue | Symptom | Root Cause | Solution |
|---|---|---|---|
| Port already in use | Bind for 0.0.0.0:3000 failed: port is already allocated | Another process or container uses the same port | Run lsof -i :3000 to find the process, then stop it or change the port mapping |
| Container exits immediately | Status shows Exited (1) seconds after starting | Application crash, missing env vars, or wrong CMD | Check logs with docker compose logs servicename and verify Dockerfile CMD |
| Cannot connect between services | Connection refused or host not found | Services on different networks or wrong hostname | Verify both services share a network and use service name as hostname |
| Database data lost after restart | Tables missing, data empty | No named volume defined for data directory | Add a named volume for /var/lib/postgresql/data |
| Build cache not invalidated | Code changes not reflected in container | Docker layer caching serves stale layers | Use docker compose build --no-cache or reorder Dockerfile layers |
| Permission denied errors | EACCES: permission denied on mounted volumes | Host UID/GID mismatch with container user | Set user: "1000:1000" in compose or fix host directory permissions |
| Out of disk space | Build fails or containers cannot write | Accumulated images, volumes, and build cache | Run docker system prune -a --volumes to reclaim space |
| Compose file syntax error | yaml: line X: did not find expected key | Indentation error or invalid YAML syntax | Validate with docker compose config before running |
Troubleshooting tip 1: Always check logs first. Run docker compose logs -f --tail=50 to see real-time output from all services. Add a service name to filter: docker compose logs -f api.
Troubleshooting tip 2: Validate your compose file. Run docker compose config to parse and validate your compose.yaml. This catches syntax errors, undefined variables, and invalid configuration before you attempt to start services.
Troubleshooting tip 3: Inspect container state. Use docker compose exec api sh to open a shell inside a running container. From there, test network connectivity with ping db, verify environment variables with env | grep DATABASE, and check file permissions manually.
Troubleshooting tip 4: Reset everything cleanly. When debugging gets complicated, a clean slate helps: docker compose down -v --remove-orphans && docker compose up --build -d. The -v flag removes volumes and --remove-orphans cleans up containers from previous configurations. Use this as a last resort since it destroys all persistent data.
Advanced Tips for Production Docker Compose Deployments
While Docker Compose is often associated with development environments, it is increasingly used for production workloads on single-host deployments, edge computing nodes, and staging environments. These advanced tips help you bridge the gap between development convenience and production reliability.
Use multi-file composition for environment management. Create a base compose.yaml for shared configuration and layer environment-specific overrides: compose.prod.yaml removes port exposures, sets restart: always, and configures resource limits. Run with docker compose -f compose.yaml -f compose.prod.yaml up -d. This approach is cleaner than maintaining separate complete files for each environment.
Implement proper logging. Configure the logging driver in your compose file to forward container logs to a centralized system. The json-file driver with size limits prevents log files from consuming all disk space: logging: { driver: "json-file", options: { max-size: "10m", max-file: "3" } }. For production, switch to the fluentd or gelf driver to ship logs to Elasticsearch or Grafana Loki.
Use Docker Compose with GPU passthrough for AI workloads. The 2026 Compose specification supports GPU allocation through the deploy.resources.reservations.devices key. This enables running ML inference or training workloads alongside your application stack:
services:
inference:
image: your-ml-model:latest
deploy:
resources:
reservations:
devices:
- driver: nvidia
count: 1
capabilities: [gpu]
limits:
memory: 8G
Use build secrets for secure builds. When your Dockerfile needs access to private registries or API keys during build time, use BuildKit secrets instead of build arguments. Secrets are mounted as temporary files during the build and never persisted in image layers, preventing accidental credential leakage in your docker compose file.
Monitor with built-in stats. Run docker compose stats to see real-time CPU, memory, network, and disk I/O for all services. For more thorough monitoring, add Prometheus and Grafana as services in your composition – many teams include them behind a monitoring profile that can be activated when needed.
Complete Working Project: Running the Full Stack
Let us bring everything together and run the complete TaskFlow application. Follow these commands to clone, configure, launch, and test the entire stack from scratch. This is the leading docker compose example that demonstrates all concepts covered in this tutorial.
# 1. Create the project (if you haven't already)
mkdir -p taskflow/{api,worker,db/init} && cd taskflow
# 2. Create all files as shown in Steps 1-5 above
# 3. Create .dockerignore files
echo -e "node_modulesn.gitn*.mdn.env" > api/.dockerignore
echo -e "__pycache__n.gitn*.mdn.env" > worker/.dockerignore
# 4. Build and start the full stack
docker compose up --build -d
# 5. Wait for health checks to pass
docker compose ps --format "table {{.Name}}t{{.Status}}"
# 6. Test the API
curl -s http://localhost:3000/health | python3 -m json.tool
# 7. Create tasks via the API
for i in $(seq 1 5); do
curl -s -X POST http://localhost:3000/tasks
-H "Content-Type: application/json"
-d "{"title":"Task $i","priority":"high"}"
done
# 8. Start the workers with the profile
docker compose --profile with-worker up -d
# 9. Watch workers process tasks
docker compose logs -f worker
# Expected output:
# taskflow-worker-1 | TaskFlow Worker started
# taskflow-worker-1 | Processing task 6: Task 1 (priority: high)
# taskflow-worker-2 | Processing task 7: Task 2 (priority: high)
# taskflow-worker-1 | Completed task 6
# taskflow-worker-2 | Completed task 7
# 10. Verify tasks are completed
curl -s http://localhost:3000/tasks | python3 -m json.tool
# 11. Check resource usage
docker compose stats --no-stream
# 12. Tear down when finished
docker compose --profile with-worker down
The complete project demonstrates service orchestration with health check dependencies, background worker processing with the profiles feature, PostgreSQL data persistence via named volumes, Redis caching for improved API performance, multi-stage Docker builds for optimized images, and network segmentation for security. This is a production-ready foundation that you can extend with additional services like an Nginx reverse proxy, Prometheus monitoring, or Elasticsearch for full-text search.
Docker Compose Command Reference for 2026
Mastering the Docker Compose CLI is essential for efficient container management. The following table provides a quick reference for the most important commands available in Compose V2 as of March 2026. All commands use the docker compose (space, not hyphen) syntax.
| Command | Description | Common Flags |
|---|---|---|
docker compose up | Create and start containers | --build -d --force-recreate --scale |
docker compose down | Stop and remove containers, networks | -v --remove-orphans --timeout |
docker compose build | Build or rebuild services | --no-cache --pull --parallel |
docker compose watch | Watch for changes and sync/rebuild | --no-up |
docker compose logs | View output from containers | -f --tail=N --since --timestamps |
docker compose exec | Execute a command in a running container | -it --user --workdir |
docker compose ps | List containers | --format --services --status |
docker compose config | Validate and view the resolved config | --services --volumes --format |
docker compose pull | Pull service images | --quiet --include-deps |
docker compose stats | Display live resource usage | --no-stream --format |
Docker Compose vs Alternatives: When to Use What
Docker Compose excels in specific scenarios but is not the right tool for every situation. Understanding its boundaries helps you make informed architectural decisions. For local development with multi-container applications, Compose is unmatched in simplicity and speed. For single-host production deployments – such as small applications, edge nodes, or dedicated servers – Compose with proper health checks and restart policies provides a reliable foundation.
However, if you need automatic scaling across multiple hosts, rolling updates with zero downtime, or self-healing infrastructure, you should consider Kubernetes or Docker Swarm. Many teams use Docker Compose for development and Kubernetes for production, sharing the same container images built from their Compose-defined Dockerfiles. This dual approach has become the standard pattern in 2026, with tools like Kompose converting compose files to Kubernetes manifests.
For infrastructure-as-code beyond containers, Terraform handles cloud resource provisioning while Compose manages the application layer. These tools complement rather than compete with each other. The modern DevOps toolkit typically includes Terraform for infrastructure, Docker Compose for local development, and Kubernetes or ECS for production orchestration – a layered approach that balances developer experience with operational requirements.
Related Coverage
For more in-depth guides on related topics, explore these articles from our cloud computing hub:
- Docker vs Kubernetes 2026: The Leading Container Comparison – understand when to scale beyond Compose
- How to Deploy AWS Infrastructure with Terraform: Complete Tutorial (2026) – provision the cloud resources your containers run on
- How to Build a Full-Stack App with Next.js 15: Complete Tutorial (2026) – containerize a full-stack JavaScript application
- Cloud Cost Optimization: 7 Strategies That Actually Work – reduce costs when running containers in the cloud
- AWS vs Azure vs Google Cloud 2026: The Leading Cloud Platform Comparison – choose the right platform for your container workloads
- Kubernetes 2.0: Everything Developers Need to Know – the next step after mastering Compose
Frequently Asked Questions
What is the difference between docker-compose and docker compose?
The hyphenated docker-compose was the standalone V1 binary written in Python. It is fully deprecated as of 2024. The space-separated docker compose is the V2 plugin written in Go, integrated directly into the Docker CLI. In 2026, all Docker Desktop and Docker Engine installations include the V2 plugin by default. If you encounter tutorials using the hyphenated version, simply replace the hyphen with a space.
Can Docker Compose be used in production?
Yes, Docker Compose is suitable for production on single-host deployments, edge computing scenarios, and small-to-medium applications. For production use, ensure you set restart: always, define resource limits, configure proper logging, remove debug ports, and use health checks. For multi-host deployments requiring automatic scaling and self-healing, Kubernetes or Docker Swarm is more appropriate.
How do I update services without downtime?
Run docker compose up -d --no-deps --build servicename to rebuild and restart a single service without affecting others. For true zero-downtime deployments, you need a reverse proxy (like Nginx or Traefik) in front of your services combined with health checks. The proxy routes traffic only to healthy instances while the new container starts up.
What is the compose.yaml file size limit?
There is no hard file size limit for compose.yaml, but extremely large files become difficult to maintain. If your compose file exceeds 200 lines, consider using the include directive to split it into modular files. Most production projects keep the main compose file under 150 lines and delegate infrastructure services to included files.
How do I share volumes between services?
Define a named volume in the top-level volumes: section and reference it in multiple services. For example, both an API and a worker can mount the same shared-data volume. Be cautious about concurrent writes – use file locking or database-backed shared state when multiple containers need to write to the same volume simultaneously.
Does Docker Compose support GPU passthrough?
Yes, since Compose V2.21+ you can allocate GPUs using the deploy.resources.reservations.devices configuration. You need the NVIDIA Container Toolkit installed on the host. This feature is commonly used for ML model inference, video transcoding, and AI agent workloads running alongside traditional web services in the same composition.
How do I debug a container that exits immediately?
First, check the logs: docker compose logs servicename. If the container exits too fast to capture logs, override the entrypoint to keep it alive: docker compose run --entrypoint sh servicename. From the shell, manually run the start command to see the error output in real time. Common causes include missing environment variables, incorrect file paths, and syntax errors in application code.
What is the difference between volumes and bind mounts in Docker Compose?
Named volumes (e.g., pgdata:/var/lib/postgresql/data) are managed by Docker and stored in Docker’s internal storage. They offer better performance and portability. Bind mounts (e.g., ./api:/app) map a host directory directly into the container. Use bind mounts for development (so code changes are reflected in real time) and named volumes for production data that should persist independently of the host filesystem structure.
Marcus Chen
Marcus Chen is a Senior Tech Reporter at Tech Insider covering cloud computing, enterprise software, and the business of technology. Before joining TI, he spent five years at ZDNet covering digital transformation across European enterprises and three years at The Register reporting on cloud infrastructure. Marcus is known for his deep dives into cloud cost optimization and multi-cloud strategy. He holds a degree in Computer Science from Imperial College London and speaks regularly at KubeCon and CloudNative events.
View all articles