Docker Compose v2 replaced the standalone Python-based docker-compose binary with a Go-based plugin integrated into the Docker CLI. The command changed from docker-compose (with a hyphen) to docker compose (with a space). Docker positioned this as a seamless upgrade, but in practice, it introduced several breaking changes that silently alter behavior without producing obvious errors. Your deployment scripts might appear to work while actually doing something different than before.
This guide covers every breaking change we have encountered in production migrations, with before-and-after examples and tested fixes for each one.
Breaking Change 1: The Command Itself
Every script, CI/CD pipeline, Makefile, and alias that uses docker-compose must be updated to docker compose. This is more than a cosmetic change — the old binary is no longer maintained and may not be installed on newer systems.
# Old (v1)
docker-compose up -d
docker-compose logs -f
docker-compose exec app bash
# New (v2)
docker compose up -d
docker compose logs -f
docker compose exec app bash
If you have dozens of scripts, a quick fix is to create a symlink or alias, but this is a temporary measure — update the actual scripts:
# Temporary compatibility (add to ~/.bashrc or ~/.zshrc)
alias docker-compose='docker compose'
# Better: update scripts using sed
find . -name "*.sh" -exec sed -i 's/docker-compose/docker compose/g' {} +
Breaking Change 2: Container Naming Convention
Docker Compose v1 named containers using the pattern directoryname_servicename_1 with underscores. Docker Compose v2 changed this to directoryname-servicename-1 with hyphens. This breaks any script, monitoring rule, or external service that references containers by name.
# v1 naming
myproject_web_1
myproject_db_1
myproject_redis_1
# v2 naming
myproject-web-1
myproject-db-1
myproject-redis-1
If you have scripts that use container names directly (for log collection, backup scripts, health checks), update them. Alternatively, specify explicit container names in your docker-compose.yml to maintain consistency across versions:
services:
web:
image: myapp:latest
container_name: myproject-web # Explicit name, same in v1 and v2
db:
image: postgres:16
container_name: myproject-db
Breaking Change 3: depends_on with Health Checks
In Docker Compose v1, depends_on only controlled start order — it started service B after service A, but did not wait for service A to be "ready." You needed tools like wait-for-it.sh or dockerize to wait for database readiness.
Docker Compose v2 introduced a condition parameter for depends_on that actually waits for health checks. However, this syntax is different from v1 and can catch you off guard if you expected the old behavior:
services:
web:
image: myapp:latest
depends_on:
db:
condition: service_healthy
redis:
condition: service_started # Just start order, like v1 behavior
db:
image: postgres:16
healthcheck:
test: ["CMD-SHELL", "pg_isready -U postgres"]
interval: 5s
timeout: 5s
retries: 5
start_period: 10s
redis:
image: redis:7-alpine
healthcheck:
test: ["CMD", "redis-cli", "ping"]
interval: 5s
timeout: 3s
retries: 3
The service_healthy condition requires a healthcheck to be defined on the dependency. If you add condition: service_healthy but forget the healthcheck definition, the dependent service will never start and docker compose will wait indefinitely.
Available conditions are: service_started (start order only, v1 behavior), service_healthy (wait for healthcheck to pass), and service_completed_successfully (wait for the dependency to run and exit with code 0, useful for migration or setup containers).
Breaking Change 4: Profiles
Docker Compose v2 introduced profiles, which let you selectively enable services. Services assigned to a profile are not started by default — they only start when you explicitly activate their profile. If you add profiles to existing services without understanding this, those services will silently stop being started.
services:
web:
image: myapp:latest
# No profile = always started
db:
image: postgres:16
# No profile = always started
debug:
image: busybox
profiles:
- debug
# Only started when: docker compose --profile debug up
monitoring:
image: prometheus
profiles:
- monitoring
# Only started when: docker compose --profile monitoring up
To start services with specific profiles: docker compose --profile debug --profile monitoring up -d. Or set the COMPOSE_PROFILES environment variable: COMPOSE_PROFILES=debug,monitoring docker compose up -d.
Breaking Change 5: Build Context and Dockerfile Path
Docker Compose v2 changed how it resolves relative paths for build contexts and Dockerfile locations. In v1, paths were relative to the docker-compose.yml file. In v2, some path resolution edge cases changed, particularly with nested directories and symlinks.
# This works the same in v1 and v2
services:
web:
build:
context: ./app
dockerfile: Dockerfile
# This might behave differently
services:
web:
build:
context: ../shared-app
dockerfile: docker/Dockerfile.production
The safest approach is to always use explicit, fully relative paths from the docker-compose.yml location and avoid symlinks in build contexts. If your Dockerfile is not in the build context root, use an absolute path within the context:
services:
web:
build:
context: .
dockerfile: ./docker/Dockerfile.production
args:
- NODE_ENV=production
Breaking Change 6: Environment Variable Interpolation
Docker Compose v2 is stricter about environment variable interpolation. In v1, an undefined variable with no default would silently result in an empty string. In v2, it may produce a warning or error depending on the configuration.
# v1: silently uses empty string if DATABASE_URL is not set
# v2: may warn or error
services:
web:
environment:
- DATABASE_URL=${DATABASE_URL}
# Safe: provide a default value
services:
web:
environment:
- DATABASE_URL=${DATABASE_URL:-postgres://localhost:5432/mydb}
Always provide default values using the ${VAR:-default} syntax or ensure all required variables are set in your .env file.
Breaking Change 7: Network Behavior
Docker Compose v2 handles network creation differently. In v1, stopping and removing containers with docker-compose down also removed the default network. In v2, network lifecycle is managed more carefully, and you might encounter issues with stale networks or network naming conflicts.
# Explicit network configuration is more reliable across versions
services:
web:
networks:
- app-network
db:
networks:
- app-network
networks:
app-network:
driver: bridge
If you encounter "network already exists" errors after a failed deployment, clean up manually with docker network prune or remove the specific network with docker network rm networkname.
Migration Checklist
When migrating from Docker Compose v1 to v2, follow this checklist to avoid surprises:
1. Update all scripts from docker-compose to docker compose. 2. Update any container name references from underscores to hyphens, or add explicit container_name to services. 3. Review depends_on usage and add healthchecks with conditions if you need startup order guarantees. 4. Check environment variable definitions for missing defaults. 5. Test build contexts with relative paths, especially if using nested directories. 6. Run docker compose config to validate your compose file — this shows the fully resolved configuration. 7. Test the full lifecycle: docker compose up -d, verify all services start, docker compose down, verify clean shutdown.
ZeonEdge helps teams migrate Docker workflows, optimize compose configurations, and implement production-grade container deployments. Learn more about our DevOps services.
Marcus Rodriguez
Lead DevOps Engineer specializing in CI/CD pipelines, container orchestration, and infrastructure automation.