
Docker Syntax Compose Services
- 8 installs
- 9 repo stars
- Updated July 8, 2026
- openaec-foundation/docker-claude-skill-package
Helps with devops & ci/cd tasks.
About
docker-syntax-compose-services is a Claude Code skill for devops & ci/cd. It helps solo builders move faster with AI-assisted development.
- docker-syntax-compose-services
- DevOps & CI/CD
- AI-coding skill
Docker Syntax Compose Services by the numbers
- 8 all-time installs (skills.sh)
- Ranked #1,044 of 1,435 DevOps & CI/CD skills by installs in the Skillselion catalog
- Data as of Aug 4, 2026 (Skillselion catalog sync)
npx skills add https://github.com/openaec-foundation/docker-claude-skill-package --skill docker-syntax-compose-servicesAdd your badge
Show developers this skill is listed on Skillselion. Paste this into your README.
| Installs | 8 |
|---|---|
| repo stars | ★ 9 |
| Last updated | July 8, 2026 |
| Repository | openaec-foundation/docker-claude-skill-package ↗ |
What it does
Helps with devops & ci/cd tasks.
Files
docker-syntax-compose-services
Quick Reference
Service Definition Structure
services:
service-name:
image: registry/image:tag # Container image
build: ./path # Build from Dockerfile
command: ["executable", "arg"] # Override CMD
entrypoint: ["executable"] # Override ENTRYPOINT
ports: # Port mappings
- "8080:80"
environment: # Environment variables
KEY: value
volumes: # Data mounts
- data:/app/data
depends_on: # Service dependencies
db:
condition: service_healthy
healthcheck: # Health monitoring
test: ["CMD", "curl", "-f", "http://localhost"]
deploy: # Resource limits and replicas
resources:
limits:
memory: 512M
restart: unless-stopped # Restart policyCritical Warnings
NEVER use depends_on without condition: service_healthy when your service requires the dependency to be fully ready. The default service_started condition only waits for the container to start, NOT for the application inside to be ready.
NEVER set container_name on services you intend to scale. Container names must be unique -- setting a fixed name prevents docker compose up --scale.
NEVER hardcode secrets in environment. ALWAYS use Docker secrets or .env files with interpolation for sensitive values.
NEVER expose ports to all interfaces ("8080:80") in production. ALWAYS bind to a specific interface ("127.0.0.1:8080:80") unless the service must be publicly accessible.
ALWAYS combine restart: always or restart: unless-stopped with deploy.resources.limits to prevent a crash-looping container from consuming all system resources.
ALWAYS declare named volumes in the top-level volumes: section. Anonymous volumes are destroyed on docker compose down.
---
Image and Build
Image Source
services:
web:
image: nginx:1.25-alpine # Tag-based
image: redis@sha256:0ed5d592... # Digest-pinned
image: registry.example.com:5000/app # Private registryBuild Configuration
services:
app:
build:
context: . # Build context directory
dockerfile: prod.Dockerfile # Custom Dockerfile path
target: production # Multi-stage target
args:
GIT_COMMIT: ${GIT_COMMIT} # Build arguments
cache_from:
- type=gha # GitHub Actions cache
secrets:
- db_password # Build-time secrets
platforms:
- linux/amd64
- linux/arm64When both build and image are set, pull_policy determines precedence. Without pull_policy, Compose attempts pulling before building.
---
Command and Entrypoint
| Form | Syntax | Shell Processing |
|---|---|---|
| String | command: bundle exec thin -p 3000 | Passed to /bin/sh -c |
| List (exec) | command: ["php", "-d", "memory=-1"] | Executed directly |
Set to null to use image default. Set to [] or '' to clear.
---
Ports
Short vs Long Syntax Comparison
| Feature | Short Syntax | Long Syntax |
|---|---|---|
| Basic mapping | "8080:80" | target: 80, published: "8080" |
| Interface bind | "127.0.0.1:8080:80" | host_ip: 127.0.0.1 |
| Protocol | "6060:6060/udp" | protocol: udp |
| Port range | "9090-9091:8080-8081" | Not supported |
| Named port | Not supported | name: web |
| App protocol | Not supported | app_protocol: http |
| Random host port | "3000" | Omit published |
Short Syntax
ports:
- "8080:80" # HOST:CONTAINER
- "127.0.0.1:8001:8001" # Bind to localhost
- "9090-9091:8080-8081" # Port range
- "6060:6060/udp" # UDP protocol
- "3000" # Random host portLong Syntax
ports:
- name: web
target: 80
published: "8080"
host_ip: 127.0.0.1
protocol: tcp
app_protocol: http
mode: hostALWAYS use long syntax when you need named ports or explicit interface binding for clarity.
---
Environment Variables
Precedence (Highest to Lowest)
1. docker compose run -e CLI flag 2. Shell interpolation in environment/env_file 3. environment attribute (static values) 4. env_file attribute 5. Dockerfile ENV directive
Configuration
environment:
RACK_ENV: development # Map syntax
DB_PASSWORD: ${DB_PASSWORD:?Required} # Fail if unset
env_file:
- path: ./default.env
required: false # Don't error if missingALWAYS use ${VAR:?message} for required variables to fail fast with a clear error.
---
Volumes
Short vs Long Syntax
volumes:
# Short syntax
- db-data:/var/lib/postgresql/data # Named volume
- ./config:/app/config:ro # Bind mount, read-only
# Long syntax
- type: volume
source: db-data
target: /var/lib/data
volume:
nocopy: true
- type: bind
source: ./config
target: /app/config
read_only: true
bind:
create_host_path: true
- type: tmpfs
target: /tmp
tmpfs:
size: 100MALWAYS use long syntax for production configurations -- it makes mount type, access mode, and options explicit.
---
depends_on
Condition Comparison Table
| Condition | Waits For | Requires | Use Case |
|---|---|---|---|
service_started | Container started | Nothing | Non-critical dependencies |
service_healthy | Healthcheck passes | healthcheck on target | Databases, APIs that need warmup |
service_completed_successfully | Exit code 0 | Service exits | Migrations, seed scripts |
Configuration
depends_on:
db:
condition: service_healthy
restart: true # Restart when dependency updates
migration:
condition: service_completed_successfully
required: false # Warning instead of error if missing
redis:
condition: service_startedALWAYS use condition: service_healthy for database dependencies. A started container does NOT mean the database is accepting connections.
---
Healthcheck
Pattern Template
healthcheck:
test: ["CMD-SHELL", "<check-command>"]
interval: 30s # Time between checks
timeout: 10s # Max time for single check
retries: 3 # Failures before unhealthy
start_period: 30s # Grace period at startup
start_interval: 5s # Interval during start_periodCommon Healthcheck Commands
| Service | Test Command |
|---|---|
| PostgreSQL | pg_isready -U postgres |
| MySQL | mysqladmin ping -h localhost |
| Redis | redis-cli ping |
| HTTP API | curl -f http://localhost:8080/health |
| TCP port | nc -z localhost 5432 |
Set test: NONE to disable a healthcheck inherited from the image.
---
Deploy and Resource Limits
deploy:
replicas: 3
resources:
limits:
cpus: '0.50'
memory: 512M
pids: 100
reservations:
cpus: '0.25'
memory: 256M
devices:
- capabilities: [gpu]
driver: nvidia
count: 1
restart_policy:
condition: on-failure
delay: 5s
max_attempts: 3
window: 120sALWAYS set resources.limits.memory for every production service. Without limits, a memory leak in one container can crash the entire host.
---
Restart Policies
| Policy | Behavior |
|---|---|
"no" | Never restart (default). ALWAYS quote -- unquoted no is YAML boolean false |
always | Restart unconditionally, including after daemon restart |
on-failure[:max] | Restart only on non-zero exit. Optional max retries (on-failure:3) |
unless-stopped | Like always, but NOT after manual docker stop |
ALWAYS use unless-stopped for production services -- it respects manual stops while surviving daemon restarts.
---
Security Configuration
services:
app:
read_only: true # Read-only root filesystem
user: "1000:1000" # Non-root user
cap_drop:
- ALL # Drop all capabilities
cap_add:
- NET_BIND_SERVICE # Add back only what's needed
security_opt:
- no-new-privileges:trueALWAYS drop all capabilities with cap_drop: [ALL] and add back only what the service requires. NEVER use privileged: true unless absolutely necessary.
---
Profiles
services:
app: # No profile = ALWAYS enabled
image: myapp
debug-tools:
image: debug-toolkit
profiles: [debug] # Only with --profile debug
monitoring:
profiles: [monitoring, production]Activate with: docker compose --profile debug up or COMPOSE_PROFILES=debug.
Services WITHOUT profiles are ALWAYS started. ALWAYS assign profiles to development-only or optional services.
---
Extends
services:
web:
extends:
file: common-services.yml
service: webapp
environment:
API_KEY: ${API_KEY} # Local values override extendedLocal attributes ALWAYS override extended values. Relative paths in extended files are automatically converted.
---
Additional Attributes
| Attribute | Purpose | Key Constraint |
|---|---|---|
container_name | Fixed container name | Prevents scaling |
hostname | Container hostname | RFC 1123 compliant |
platform | Target platform | Format: os[/arch[/variant]] |
pull_policy | Image pull strategy | always, never, missing, build |
logging | Log driver and options | Driver must be available |
labels | Container metadata | Reverse-DNS notation recommended |
init: true | PID 1 init process | Proper signal forwarding |
stop_grace_period | Time before SIGKILL | Default: 10s |
---
Reference Links
- references/attributes.md -- Complete service attribute reference with all options and syntax variants
- references/examples.md -- Common service configurations for web, database, cache, and worker services
- references/anti-patterns.md -- Service configuration mistakes with explanations and corrections
Official Sources
- https://docs.docker.com/compose/compose-file/05-services/
- https://docs.docker.com/compose/compose-file/build/
- https://docs.docker.com/compose/compose-file/deploy/
- https://docs.docker.com/compose/how-tos/environment-variables/
Service Configuration Anti-Patterns
Common mistakes in Docker Compose service configuration with explanations and corrections.
---
AP-001: depends_on Without Health Conditions
Problem: depends_on in short form only waits for the container to start, NOT for the application to be ready. A database container starts in milliseconds, but PostgreSQL may need seconds to initialize.
# WRONG -- app starts before database accepts connections
services:
app:
depends_on:
- db
db:
image: postgres:16# CORRECT -- app waits for database to pass healthcheck
services:
app:
depends_on:
db:
condition: service_healthy
db:
image: postgres:16
healthcheck:
test: ["CMD-SHELL", "pg_isready -U postgres"]
interval: 10s
timeout: 5s
retries: 5
start_period: 30sWhy: Without condition: service_healthy, race conditions cause intermittent startup failures. The app connects before the database is ready, crashes, and must be manually restarted.
---
AP-002: Hardcoded Secrets in Environment
Problem: Secrets in plain text in compose.yaml get committed to version control.
# WRONG -- credentials in source control
services:
db:
environment:
POSTGRES_PASSWORD: "super-secret-password"# CORRECT -- use interpolation with required check
services:
db:
environment:
POSTGRES_PASSWORD: ${POSTGRES_PASSWORD:?Database password is required}Or use Docker secrets:
# BEST -- secrets mounted as files, never in environment
services:
db:
secrets:
- db_password
environment:
POSTGRES_PASSWORD_FILE: /run/secrets/db_password
secrets:
db_password:
file: ./secrets/db_password.txtWhy: Environment variables appear in docker inspect, process listings, and logs. Secrets are mounted as files with restricted permissions.
---
AP-003: container_name on Scalable Services
Problem: container_name sets a fixed name. Container names must be unique, so scaling fails.
# WRONG -- cannot run docker compose up --scale web=3
services:
web:
image: nginx
container_name: my-nginx# CORRECT -- let Compose manage container names
services:
web:
image: nginxWhy: Compose generates unique names like project-web-1, project-web-2. A fixed container_name makes the second instance fail with a name conflict.
---
AP-004: No Resource Limits
Problem: Without memory limits, a single container with a memory leak can consume all host memory and crash other containers or the host itself.
# WRONG -- no limits, can consume unlimited resources
services:
app:
image: myapp
restart: always# CORRECT -- explicit limits prevent resource exhaustion
services:
app:
image: myapp
restart: unless-stopped
deploy:
resources:
limits:
cpus: '1.0'
memory: 512M
reservations:
cpus: '0.25'
memory: 128MWhy: restart: always combined with no limits creates a crash loop that consumes increasing resources. Each restart may leak more memory until the host is unresponsive.
---
AP-005: Anonymous Volumes for Persistent Data
Problem: Anonymous volumes are recreated on docker compose down and data is permanently lost.
# WRONG -- data lost when containers are removed
services:
db:
image: postgres
volumes:
- /var/lib/postgresql/data# CORRECT -- named volume persists across compose down/up
services:
db:
image: postgres
volumes:
- db-data:/var/lib/postgresql/data
volumes:
db-data:Why: Anonymous volumes have random names and are not tracked. docker compose down removes them. Named volumes declared in the top-level volumes: section survive docker compose down (unless --volumes flag is used).
---
AP-006: Exposing Ports to All Interfaces
Problem: Default port mapping binds to 0.0.0.0, making the service accessible from any network interface including public ones.
# WRONG -- accessible from any network interface
ports:
- "8080:80"
- "5432:5432"# CORRECT -- bind to localhost for local-only access
ports:
- "127.0.0.1:8080:80"
- "127.0.0.1:5432:5432"Why: On a server with a public IP, "5432:5432" exposes PostgreSQL to the internet. ALWAYS bind to 127.0.0.1 for services that should only be accessed locally or through a reverse proxy.
---
AP-007: Using version: Field
Problem: The version field is deprecated and ignored by modern Compose. It provides no value and confuses users about compatibility.
# WRONG -- deprecated, provides no functionality
version: "3.8"
services:
web:
image: nginx# CORRECT -- version field is not needed
services:
web:
image: nginxWhy: The unified Compose Specification replaced legacy versions 2.x and 3.x. Modern Compose ignores this field entirely.
---
AP-008: Running as Root Without Necessity
Problem: Containers run as root by default, giving a compromised process unnecessary privileges.
# WRONG -- runs as root
services:
app:
image: myapp# CORRECT -- non-root user, minimal capabilities
services:
app:
image: myapp
user: "1000:1000"
read_only: true
cap_drop:
- ALL
cap_add:
- NET_BIND_SERVICE
security_opt:
- no-new-privileges:true
tmpfs:
- /tmpWhy: If an attacker exploits a vulnerability in the application, running as root gives them full control over the container. Combined with read_only: true and dropped capabilities, the attack surface is minimized.
---
AP-009: No Healthcheck on Dependency Services
Problem: Without a healthcheck, there is no way to use condition: service_healthy in depends_on. Services that depend on this service can only use service_started, which is unreliable.
# WRONG -- no healthcheck, dependents cannot wait for readiness
services:
redis:
image: redis:7
app:
depends_on:
- redis# CORRECT -- healthcheck enables reliable dependency ordering
services:
redis:
image: redis:7
healthcheck:
test: ["CMD", "redis-cli", "ping"]
interval: 10s
timeout: 5s
retries: 3
app:
depends_on:
redis:
condition: service_healthyWhy: EVERY service that other services depend on MUST have a healthcheck. Without it, dependent services start before the dependency is actually ready to handle requests.
---
AP-010: Using restart: "no" Without Quotes
Problem: YAML interprets bare no as boolean false, which is not the same as the string "no".
# WRONG -- YAML parses this as boolean false
restart: no# CORRECT -- quoted string
restart: "no"Why: Most Compose implementations handle this gracefully, but it is technically incorrect YAML. ALWAYS quote "no" to ensure correct parsing.
---
AP-011: Mixing Network Isolation with network_mode
Problem: Using network_mode: host bypasses Docker networking entirely. Port mappings, service discovery, and network isolation stop working.
# WRONG -- host networking breaks port mapping and isolation
services:
app:
network_mode: host
ports:
- "8080:80" # Ignored with host networking
networks:
- backend # Incompatible with network_mode# CORRECT -- use Docker networks for isolation
services:
app:
ports:
- "127.0.0.1:8080:80"
networks:
- backendWhy: network_mode: host shares the host's network stack. The ports and networks attributes are silently ignored. Service-to-service DNS resolution stops working.
---
AP-012: Not Using Profiles for Dev-Only Services
Problem: Debug and monitoring tools run in every environment, consuming resources unnecessarily.
# WRONG -- debug tools always start
services:
app:
image: myapp
phpmyadmin:
image: phpmyadmin
mailhog:
image: mailhog/mailhog# CORRECT -- debug tools behind profile
services:
app:
image: myapp
phpmyadmin:
image: phpmyadmin
profiles: [debug]
mailhog:
image: mailhog/mailhog
profiles: [debug]Why: Services without profiles ALWAYS start. In production, dev tools waste resources and create security risks. Use docker compose --profile debug up only when needed.
---
AP-013: Bind Mounts With Absolute Host Paths
Problem: Absolute host paths make compose files non-portable across machines and operating systems.
# WRONG -- path exists only on this specific machine
volumes:
- /home/alice/project/data:/app/data# CORRECT -- relative path, portable
volumes:
- ./data:/app/dataWhy: Relative paths resolve from the Compose file location, making the project work on any machine. Absolute paths break when cloned to a different location or OS.
---
AP-014: Logging Without Size Limits
Problem: Without log rotation, container logs grow indefinitely until the disk is full.
# WRONG -- unlimited log growth
services:
app:
image: myapp# CORRECT -- bounded log size with rotation
services:
app:
image: myapp
logging:
driver: json-file
options:
max-size: "10m"
max-file: "3"Why: A busy application can generate gigabytes of logs. Without max-size and max-file, logs fill the disk, causing all services on the host to fail.
---
AP-015: Not Using init for Worker Processes
Problem: Without init: true, the application runs as PID 1. PID 1 does not receive default signal handling, causing zombie processes and delayed shutdowns.
# WRONG -- app is PID 1, poor signal handling
services:
worker:
command: ["python", "worker.py"]# CORRECT -- init process handles signals and reaps zombies
services:
worker:
command: ["python", "worker.py"]
init: trueWhy: The init process (tini) forwards signals properly and reaps zombie child processes. Without it, docker compose stop may wait the full grace period before sending SIGKILL.
---
Official Sources
- https://docs.docker.com/compose/compose-file/05-services/
- https://docs.docker.com/compose/compose-file/deploy/
- https://docs.docker.com/compose/how-tos/environment-variables/
Service Attributes Reference
Complete reference for all Docker Compose service attributes. Organized by category.
---
Image and Build
image
Specifies the container image in OCI format.
image: redis # Latest tag
image: redis:7.2-alpine # Specific tag
image: redis@sha256:0ed5d5928d47... # Digest-pinned
image: registry.example.com:5000/myapp:latest # Private registrybuild
Defines how to create Docker images from source. String shorthand or object with granular control.
# String shorthand
build: ./dir
# Full object
build:
context: ./dir # Build context (required)
dockerfile: custom.Dockerfile # Alternate Dockerfile (relative to context)
dockerfile_inline: | # Inline Dockerfile (mutually exclusive with dockerfile)
FROM baseimage
RUN some command
args: # Build arguments
GIT_COMMIT: cdc3b19
# or list:
# - GIT_COMMIT=cdc3b19
target: production # Multi-stage build target
cache_from: # Cache sources
- alpine:latest
- type=local,src=path/to/cache
- type=gha
cache_to: # Cache destinations
- user/app:cache
- type=local,dest=path/to/cache
no_cache: false # Disable builder cache
secrets: # Build-time secrets
- server-certificate # Short syntax
- source: server-certificate # Long syntax
target: cert
uid: "103"
gid: "103"
mode: 0440
ssh: # SSH agent forwarding
- default
- myproject=~/.ssh/myproject.pem
platforms: # Multi-platform builds
- "linux/amd64"
- "linux/arm64"
additional_contexts: # Named contexts
resources: /path/to/resources
app: docker-image://my-app:latest
source: https://github.com/user/repo.git
network: host # Build network mode (host, none)
shm_size: "2gb" # Shared memory size
ulimits: # Build-time ulimits
nproc: 65535
nofile:
soft: 20000
hard: 40000
extra_hosts: # /etc/hosts entries during build
- "somehost=162.242.195.82"
labels: # Image labels
com.example.description: "Accounting webapp"
tags: # Additional image tags
- "myimage:mytag"
- "registry/username/myrepos:my-other-tag"
privileged: true # Privileged build mode
provenance: true # Build provenance attestation
sbom: true # Software Bill of Materials
entitlements: # Build entitlements
- network.host
- security.insecure---
Command and Entrypoint
command
Overrides the image CMD.
command: bundle exec thin -p 3000 # String (shell form)
command: /bin/sh -c 'echo "hello $$HOSTNAME"' # Shell with escaping
command: ["php", "-d", "zend_extension=/path"] # List (exec form)nulluses image default[]or''clears the command
entrypoint
Overrides the image ENTRYPOINT.
entrypoint: /code/entrypoint.sh # String form
entrypoint: ["php", "-d", "memory_limit=-1", "vendor/bin/phpunit"] # List form---
Networking
ports
Short Syntax: [HOST:]CONTAINER[/PROTOCOL]
ports:
- "3000" # Container port only (random host port)
- "8000:8000" # HOST:CONTAINER
- "9090-9091:8080-8081" # Port range
- "127.0.0.1:8001:8001" # Bind to specific interface
- "6060:6060/udp" # UDP protocol
- "[::1]:6001:6001" # IPv6Long Syntax
ports:
- name: web # Port name (optional)
target: 80 # Container port (required)
published: "8080" # Host port
host_ip: 127.0.0.1 # Interface to bind
protocol: tcp # tcp or udp
app_protocol: http # Application protocol hint
mode: host # host or ingressexpose
Internal port exposure without host publishing.
expose:
- "3000"
- "8080-8085/tcp"networks (service-level)
networks:
- frontend
- backend
# With options
networks:
backend:
aliases:
- db-alias
ipv4_address: 172.16.238.10
ipv6_address: 2001:3984:3989::10
interface_name: eth1
mac_address: "02:42:ac:11:00:02"
driver_opts:
com.docker.network.endpoint.dnsnames: myservice
gw_priority: 100
priority: 1000
link_local_ips:
- 169.254.0.10network_mode
network_mode: "host"
network_mode: "none"
network_mode: "service:other_service"
network_mode: "container:container_id"dns, dns_search, dns_opt
dns:
- 8.8.8.8
- 9.9.9.9
dns_search:
- example.com
dns_opt:
- use-vc
- no-tld-queryextra_hosts
extra_hosts:
- "somehost=162.242.195.82"
- "myhostv6=[::1]"links and external_links
links:
- db
- db:database # With alias
external_links:
- redis
- database:mysql---
Environment
environment
# Map syntax
environment:
RACK_ENV: development
SHOW: "true"
# List syntax
environment:
- RACK_ENV=development
- USER_INPUT # Pass-through from host shellenv_file
env_file: .env # Single file
env_file: # Multiple files (later overrides earlier)
- ./default.env
- ./override.env
env_file: # With options (Compose 2.24.0+)
- path: ./default.env
required: false # Don't error if missing
format: raw # No interpolation (Compose 2.30.0+).env File Parsing Rules
- Format:
VAR[=[VAL]] - Comments: lines starting with
# - Double-quoted values support interpolation and escape sequences (
\n,\r,\t,\\) - Single-quoted values are literal (no interpolation)
- Later files override earlier files for matching keys
---
Volumes and Storage
volumes (service-level)
Short Syntax: SOURCE:TARGET[:ACCESS_MODE]
volumes:
- /host/path:/container/path # Bind mount
- volume-name:/data # Named volume
- /host/path:/container/path:ro # Read-only
- /host/path:/container/path:rw # Read-write (default)Long Syntax
volumes:
- type: volume # Named volume
source: db-data
target: /data
volume:
nocopy: true # Don't copy container data to volume
subpath: sub # Mount subdirectory
- type: bind # Bind mount
source: /var/run/postgres.sock
target: /var/run/postgres.sock
bind:
propagation: rprivate
create_host_path: true # Create path if missing
read_only: true
- type: tmpfs # Temporary filesystem
target: /temp
tmpfs:
size: 1G
mode: 0755tmpfs
tmpfs:
- /data:mode=755,uid=1009,gid=1009
- /runvolumes_from
volumes_from:
- service_name
- service_name:ro
- container:container_name:rw---
Dependencies and Health
depends_on
Short Syntax
depends_on:
- db
- redisLong Syntax
depends_on:
db:
condition: service_healthy # Wait for healthcheck
restart: true # Restart when dependency updates
redis:
condition: service_started # Wait for start only
migration:
condition: service_completed_successfully # Wait for exit code 0
required: false # Warning if service missinghealthcheck
healthcheck:
test: ["CMD", "curl", "-f", "http://localhost"] # Exec form
test: ["CMD-SHELL", "pg_isready -U postgres"] # Shell form
test: curl -f http://localhost || exit 1 # String (shell)
interval: 1m30s # Time between checks (default: 30s)
timeout: 10s # Max time for check (default: 30s)
retries: 3 # Failures before unhealthy (default: 3)
start_period: 40s # Grace period at startup (default: 0s)
start_interval: 5s # Interval during start_period (default: 5s)Disable inherited healthcheck: test: NONE
---
Deploy
deploy:
mode: replicated # replicated (default), global
replicas: 6
resources:
limits:
cpus: '0.50'
memory: 50M
pids: 1
reservations:
cpus: '0.25'
memory: 20M
devices:
- capabilities: ["gpu"] # or "nvidia-compute"
driver: nvidia
count: 2 # or "all"
# device_ids: ["0", "1"] # Mutually exclusive with count
restart_policy:
condition: on-failure # none, on-failure, any (default)
delay: 5s # Default: 0
max_attempts: 3 # Default: unlimited
window: 120s # Default: immediate
placement:
constraints:
- node.labels.disktype==ssd
preferences:
- spread: node.labels.zone
update_config:
parallelism: 2
delay: 10s
failure_action: pause # continue, rollback, pause (default)
monitor: 30s
max_failure_ratio: 0.1
order: stop-first # stop-first (default), start-first
rollback_config:
parallelism: 0 # 0 = all at once
delay: 0s
failure_action: pause
monitor: 0s
max_failure_ratio: 0
order: stop-first
endpoint_mode: vip # vip (virtual IP), dnsrr (round-robin DNS)
labels:
com.example.description: "Service label (not container)"---
Restart Policy (Service-Level)
restart: "no" # ALWAYS quote -- unquoted no = YAML false
restart: always
restart: on-failure
restart: on-failure:3 # With max retries
restart: unless-stopped---
Logging
logging:
driver: syslog # json-file (default), syslog, journald, etc.
options:
syslog-address: "tcp://192.168.0.42:123"
max-size: "10m"
max-file: "3"---
Labels and Annotations
labels:
com.example.description: "Accounting webapp"
com.example.department: "Finance"
# or list syntax:
labels:
- "com.example.description=Accounting webapp"
label_file:
- ./app.labels
annotations:
com.example.foo: bar---
Container Identity
container_name: my-web-container # Prevents scaling
hostname: my-host # RFC 1123 compliant
domainname: example.com---
User and Working Directory
user: "1000:1000"
working_dir: /app---
Terminal and Init
stdin_open: true # Equivalent to -i flag
tty: true # Allocate pseudo-TTY
init: true # Run init process for signal forwarding---
Security
privileged: true # Full host access (AVOID in production)
cap_add:
- ALL
cap_drop:
- NET_ADMIN
- SYS_ADMIN
security_opt:
- label=user:USER
- label=role:ROLE
- no-new-privileges:true
group_add:
- mail
- root
read_only: true # Read-only root filesystem---
Sysctls, Ulimits, and Shared Memory
sysctls:
net.core.somaxconn: 1024
net.ipv4.tcp_syncookies: 0
ulimits:
nproc: 65535
nofile:
soft: 20000
hard: 40000
shm_size: "2gb"---
Process Namespacing
pid: "host"
ipc: "shareable" # or "service:other_service"
uts: "host"
userns_mode: "host"
cgroup: "host" # or "private"
cgroup_parent: /custom/cgroup
pids_limit: 100 # -1 for unlimited---
Devices
devices:
- "/dev/ttyUSB0:/dev/ttyUSB0"
- "/dev/sda:/dev/xvda:rwm"
device_cgroup_rules:
- 'c 1:3 mr'
- 'a 7:* rmw'
gpus:
- driver: nvidia
count: 2
# or
gpus: all---
Profiles
profiles: ["frontend", "debug"]Valid names: [a-zA-Z0-9][a-zA-Z0-9_.-]+. Services without profiles are ALWAYS enabled.
---
Extends
extends:
file: common.yml # From another file
service: webapp
# Within same file
extends: webapp---
Platform and Pull Policy
platform: linux/arm64/v8 # Format: os[/arch[/variant]]
pull_policy: always # always, never, missing (default), build
# daily, weekly, every_<duration>---
Stop and Lifecycle
stop_signal: SIGUSR1 # Default: SIGTERM
stop_grace_period: 1m30s # Grace period before SIGKILL
post_start:
- command: ./startup.sh
user: root
privileged: true
working_dir: /app
environment:
- VAR=value
pre_stop:
- command: ./cleanup.sh---
Secrets and Configs (Service-Level)
secrets
secrets:
- server-certificate # Short syntax
secrets:
- source: server-certificate # Long syntax
target: server.cert
uid: "103"
gid: "103"
mode: 0o440configs
Same syntax as secrets -- short form (name only) or long form with source, target, uid, gid, mode.
---
Miscellaneous Attributes
| Attribute | Type | Purpose |
|---|---|---|
attach | bool | Control log collection (default true) |
runtime | string | OCI runtime (e.g., runc) |
scale | int | Default container count |
isolation | string | Container isolation technology |
mac_address | string | MAC address assignment |
storage_opt | map | Storage driver options |
credential_spec | string | Windows credential spec |
use_api_socket | bool | Access container engine API |
---
Official Sources
- https://docs.docker.com/compose/compose-file/05-services/
- https://docs.docker.com/compose/compose-file/build/
- https://docs.docker.com/compose/compose-file/deploy/
Service Configuration Examples
Production-ready service configurations for common use cases. All examples follow best practices: healthchecks, resource limits, security hardening, and named volumes.
---
Web Server (Nginx Reverse Proxy)
services:
nginx:
image: nginx:1.25-alpine
ports:
- "127.0.0.1:80:80"
- "127.0.0.1:443:443"
volumes:
- type: bind
source: ./nginx/conf.d
target: /etc/nginx/conf.d
read_only: true
- type: bind
source: ./nginx/ssl
target: /etc/nginx/ssl
read_only: true
- nginx-cache:/var/cache/nginx
healthcheck:
test: ["CMD-SHELL", "curl -f http://localhost/health || exit 1"]
interval: 30s
timeout: 5s
retries: 3
start_period: 10s
deploy:
resources:
limits:
cpus: '0.50'
memory: 256M
restart: unless-stopped
read_only: true
tmpfs:
- /var/run
- /tmp
cap_drop:
- ALL
cap_add:
- NET_BIND_SERVICE
- CHOWN
- SETGID
- SETUID
depends_on:
app:
condition: service_healthy
networks:
- frontend
volumes:
nginx-cache:
networks:
frontend:---
Application Server (Node.js)
services:
app:
build:
context: .
dockerfile: Dockerfile
target: production
args:
NODE_ENV: production
ports:
- "127.0.0.1:3000:3000"
environment:
NODE_ENV: production
DATABASE_URL: postgresql://${DB_USER}:${DB_PASSWORD}@db:5432/${DB_NAME}
REDIS_URL: redis://redis:6379
env_file:
- path: .env
required: true
healthcheck:
test: ["CMD-SHELL", "curl -f http://localhost:3000/health || exit 1"]
interval: 30s
timeout: 10s
retries: 3
start_period: 30s
start_interval: 5s
deploy:
replicas: 2
resources:
limits:
cpus: '1.0'
memory: 512M
reservations:
cpus: '0.25'
memory: 128M
restart: unless-stopped
init: true
read_only: true
tmpfs:
- /tmp
cap_drop:
- ALL
user: "1000:1000"
depends_on:
db:
condition: service_healthy
redis:
condition: service_healthy
migration:
condition: service_completed_successfully
networks:
- frontend
- backend
networks:
frontend:
backend:---
Database (PostgreSQL)
services:
db:
image: postgres:16-alpine
volumes:
- db-data:/var/lib/postgresql/data
- type: bind
source: ./init-scripts
target: /docker-entrypoint-initdb.d
read_only: true
environment:
POSTGRES_DB: ${DB_NAME:?Database name required}
POSTGRES_USER: ${DB_USER:?Database user required}
POSTGRES_PASSWORD: ${DB_PASSWORD:?Database password required}
healthcheck:
test: ["CMD-SHELL", "pg_isready -U ${DB_USER} -d ${DB_NAME}"]
interval: 10s
timeout: 5s
retries: 5
start_period: 30s
deploy:
resources:
limits:
cpus: '2.0'
memory: 1G
reservations:
cpus: '0.50'
memory: 256M
restart: unless-stopped
shm_size: 256M
cap_drop:
- ALL
cap_add:
- CHOWN
- FOWNER
- SETGID
- SETUID
- DAC_READ_SEARCH
networks:
- backend
volumes:
db-data:
networks:
backend:---
Database (MySQL/MariaDB)
services:
mysql:
image: mysql:8.0
volumes:
- mysql-data:/var/lib/mysql
environment:
MYSQL_ROOT_PASSWORD: ${MYSQL_ROOT_PASSWORD:?Root password required}
MYSQL_DATABASE: ${DB_NAME}
MYSQL_USER: ${DB_USER}
MYSQL_PASSWORD: ${DB_PASSWORD}
healthcheck:
test: ["CMD-SHELL", "mysqladmin ping -h localhost -u root -p${MYSQL_ROOT_PASSWORD}"]
interval: 10s
timeout: 5s
retries: 5
start_period: 30s
deploy:
resources:
limits:
cpus: '2.0'
memory: 1G
restart: unless-stopped
cap_drop:
- ALL
cap_add:
- CHOWN
- FOWNER
- SETGID
- SETUID
- DAC_OVERRIDE
networks:
- backend
volumes:
mysql-data:
networks:
backend:---
Cache (Redis)
services:
redis:
image: redis:7-alpine
command: ["redis-server", "--maxmemory", "256mb", "--maxmemory-policy", "allkeys-lru"]
volumes:
- redis-data:/data
healthcheck:
test: ["CMD", "redis-cli", "ping"]
interval: 10s
timeout: 5s
retries: 3
start_period: 10s
deploy:
resources:
limits:
cpus: '0.50'
memory: 300M
restart: unless-stopped
read_only: true
cap_drop:
- ALL
networks:
- backend
volumes:
redis-data:
networks:
backend:---
Background Worker (Celery-style)
services:
worker:
build:
context: .
target: production
command: ["celery", "-A", "app", "worker", "--loglevel=info", "--concurrency=4"]
environment:
CELERY_BROKER_URL: redis://redis:6379/0
CELERY_RESULT_BACKEND: redis://redis:6379/1
DATABASE_URL: postgresql://${DB_USER}:${DB_PASSWORD}@db:5432/${DB_NAME}
env_file:
- .env
deploy:
replicas: 2
resources:
limits:
cpus: '1.0'
memory: 512M
reservations:
cpus: '0.25'
memory: 128M
restart: unless-stopped
init: true
read_only: true
tmpfs:
- /tmp
cap_drop:
- ALL
user: "1000:1000"
depends_on:
redis:
condition: service_healthy
db:
condition: service_healthy
networks:
- backend
networks:
backend:---
Scheduled Tasks (Cron Worker)
services:
scheduler:
build:
context: .
target: production
command: ["celery", "-A", "app", "beat", "--loglevel=info"]
environment:
CELERY_BROKER_URL: redis://redis:6379/0
deploy:
replicas: 1
resources:
limits:
cpus: '0.25'
memory: 128M
restart: unless-stopped
init: true
cap_drop:
- ALL
user: "1000:1000"
depends_on:
redis:
condition: service_healthy
networks:
- backend
networks:
backend:---
Database Migration (One-Shot)
services:
migration:
build:
context: .
target: production
command: ["python", "manage.py", "migrate", "--noinput"]
environment:
DATABASE_URL: postgresql://${DB_USER}:${DB_PASSWORD}@db:5432/${DB_NAME}
depends_on:
db:
condition: service_healthy
restart: "no"
cap_drop:
- ALL
user: "1000:1000"
networks:
- backend
networks:
backend:Use depends_on: migration: condition: service_completed_successfully in the application service to wait for migrations.
---
Debug Tools (Profile-Gated)
services:
pgadmin:
image: dpage/pgadmin4:latest
profiles: [debug]
ports:
- "127.0.0.1:5050:80"
environment:
PGADMIN_DEFAULT_EMAIL: admin@local.dev
PGADMIN_DEFAULT_PASSWORD: ${PGADMIN_PASSWORD:-admin}
volumes:
- pgadmin-data:/var/lib/pgadmin
deploy:
resources:
limits:
cpus: '0.50'
memory: 512M
restart: unless-stopped
depends_on:
db:
condition: service_healthy
networks:
- backend
mailhog:
image: mailhog/mailhog:latest
profiles: [debug]
ports:
- "127.0.0.1:8025:8025"
deploy:
resources:
limits:
cpus: '0.25'
memory: 128M
restart: unless-stopped
networks:
- backend
volumes:
pgadmin-data:
networks:
backend:Activate with: docker compose --profile debug up
---
GPU-Enabled Service (Machine Learning)
services:
ml-worker:
build:
context: .
dockerfile: Dockerfile.gpu
deploy:
resources:
limits:
cpus: '4.0'
memory: 8G
reservations:
cpus: '2.0'
memory: 4G
devices:
- capabilities: [gpu]
driver: nvidia
count: 1
environment:
NVIDIA_VISIBLE_DEVICES: all
CUDA_VISIBLE_DEVICES: "0"
volumes:
- model-data:/app/models
restart: unless-stopped
networks:
- backend
volumes:
model-data:
networks:
backend:---
Full-Stack Composition
Complete example combining web, app, database, cache, worker, and migration services.
name: myapp
services:
nginx:
image: nginx:1.25-alpine
ports:
- "127.0.0.1:80:80"
volumes:
- ./nginx/conf.d:/etc/nginx/conf.d:ro
healthcheck:
test: ["CMD-SHELL", "curl -f http://localhost/health || exit 1"]
interval: 30s
timeout: 5s
retries: 3
deploy:
resources:
limits:
memory: 256M
restart: unless-stopped
depends_on:
app:
condition: service_healthy
networks:
- frontend
app:
build:
context: .
target: production
healthcheck:
test: ["CMD-SHELL", "curl -f http://localhost:3000/health || exit 1"]
interval: 30s
timeout: 10s
retries: 3
start_period: 30s
deploy:
replicas: 2
resources:
limits:
cpus: '1.0'
memory: 512M
restart: unless-stopped
init: true
env_file: [.env]
depends_on:
db:
condition: service_healthy
redis:
condition: service_healthy
migration:
condition: service_completed_successfully
networks:
- frontend
- backend
worker:
build:
context: .
target: production
command: ["celery", "-A", "app", "worker", "-l", "info"]
deploy:
replicas: 2
resources:
limits:
memory: 512M
restart: unless-stopped
init: true
env_file: [.env]
depends_on:
db:
condition: service_healthy
redis:
condition: service_healthy
networks:
- backend
migration:
build:
context: .
target: production
command: ["python", "manage.py", "migrate", "--noinput"]
env_file: [.env]
depends_on:
db:
condition: service_healthy
restart: "no"
networks:
- backend
db:
image: postgres:16-alpine
volumes:
- db-data:/var/lib/postgresql/data
env_file: [.env]
healthcheck:
test: ["CMD-SHELL", "pg_isready -U postgres"]
interval: 10s
timeout: 5s
retries: 5
start_period: 30s
deploy:
resources:
limits:
memory: 1G
restart: unless-stopped
shm_size: 256M
networks:
- backend
redis:
image: redis:7-alpine
command: ["redis-server", "--maxmemory", "256mb", "--maxmemory-policy", "allkeys-lru"]
volumes:
- redis-data:/data
healthcheck:
test: ["CMD", "redis-cli", "ping"]
interval: 10s
timeout: 5s
retries: 3
deploy:
resources:
limits:
memory: 300M
restart: unless-stopped
networks:
- backend
volumes:
db-data:
redis-data:
networks:
frontend:
backend:---
Official Sources
- https://docs.docker.com/compose/compose-file/05-services/
- https://docs.docker.com/compose/compose-file/build/
- https://docs.docker.com/compose/compose-file/deploy/