BlogDevOps
DevOps

Docker Compose v2 Breaking Changes That Nobody Warned You About

Migrating from docker-compose (v1) to docker compose (v2) broke your deployment scripts, healthcheck-based depends_on, and build contexts. Here is every breaking change and how to fix each one.

M

Marcus Rodriguez

Lead DevOps Engineer specializing in CI/CD pipelines, container orchestration, and infrastructure automation.

February 5, 2026
16 min read

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.

M

Marcus Rodriguez

Lead DevOps Engineer specializing in CI/CD pipelines, container orchestration, and infrastructure automation.

Related Articles

Business Technology

Self-Hosting in 2026: The Complete Guide to Running Your Own Services

Why pay monthly SaaS fees when you can run the same (or better) services on your own hardware? This comprehensive guide covers self-hosting everything from email and file storage to Git repositories, project management, analytics, and monitoring. Learn about hardware selection, Docker Compose configurations, reverse proxy setup with Nginx, SSL certificates, backup strategies, and maintaining uptime.

Alex Thompson•42 min read
DevOps

CI/CD Pipeline Design Patterns in 2026: From Basic Builds to Advanced Deployment Strategies

A well-designed CI/CD pipeline is the backbone of modern software delivery. This comprehensive guide covers pipeline architecture patterns — from simple linear pipelines to complex multi-stage workflows with parallel testing, canary deployments, blue-green strategies, GitOps, security scanning, and infrastructure-as-code integration. Learn how to build pipelines that are fast, reliable, and secure.

Marcus Rodriguez•40 min read
Cybersecurity

Docker Security Best Practices in 2026: Hardening Containers from Build to Runtime

Containers are not sandboxes. A misconfigured Docker container gives attackers the same access as a root shell on the host. This comprehensive guide covers image security, build hardening, runtime protection, secrets management, network isolation, and monitoring — everything you need to run Docker securely in production.

Sarah Chen•38 min read

Ready to Transform Your Infrastructure?

Let's discuss how we can help you achieve similar results.