## Intro Upgrading Docker Compose is more than installing a new binary. A safe Docker Compose upgrade means moving from an observed problem to a verified result without losing data, breaking dependent services, or guessing at runtime behavior. This guide provides practical steps for Docker Compose upgrade, migration, rollback, version upgrade, and validation, with concrete commands and expected outputs. This article is written for developers, DevOps consultants, and technical startup teams who manage containerized applications in development, staging, or production. It focuses on the operational workflow: checking prerequisites, capturing the current state, applying a minimal change, verifying the result, and knowing how to recover if something goes wrong. The goal is operational safety. Observe before changing, limit the blast radius, use placeholders instead of secrets in configuration files, verify every step, and document recovery commands before you need them. Each section includes a practical example so you can follow along on your own systems. ## Version and Environment Inventory Before touching any running service, you need a clear inventory of the installed Docker Compose version, the active project state, and the deployment environment. This inventory helps you choose the right upgrade path and anticipate compatibility issues. ### Check the current Docker Compose version Run the following command to see the currently installed version: ```bash docker compose version ``` Example output: ```text Docker Compose version v2.24.5 ``` If you are using the older standalone `docker-compose` (v1), check with: ```bash docker-compose version ``` Example output: ```text docker-compose version 1.29.2, build 5becea4c ``` Knowing whether you are on v1 or v2 matters because the command syntax differs. Docker Compose v2 is a plugin invoked as `docker compose`, while v1 is a separate binary. Most modern systems use v2, but older servers may still have v1. The upgrade process from v1 to v2 involves installing the plugin and possibly adjusting scripts that call `docker-compose`. ### Inspect the running project state List the current containers and their status: ```bash docker compose ps ``` Example output: ```text NAME COMMAND SERVICE STATUS PORTS web-1 "docker-entrypoint.s…" web running 0.0.0.0:8080->80/tcp db-1 "docker-entrypoint.s…" db running 5432/tcp ``` This shows the service names, container names, and port mappings. Compare this with your `docker-compose.yml` to ensure the running state matches the intended configuration. Check recent logs to detect any ongoing issues: ```bash docker compose logs --tail 50 web ``` Look for error messages or warnings that might affect the upgrade. ### Identify volumes and data storage Data safety is critical during an upgrade. Determine where your application data is stored by inspecting the volumes: ```bash docker volume ls ``` Example output: ```text DRIVER VOLUME NAME local myapp_db_data local myapp_uploads ``` Then inspect a volume to see its mountpoint and labels: ```bash docker volume inspect myapp_db_data ``` Example output (partial): ```json [ { "CreatedAt": "2024-03-01T12:00:00Z", "Driver": "local", "Labels": { "com.docker.compose.project": "myapp", "com.docker.compose.volume": "db_data" }, "Mountpoint": "/var/lib/docker/volumes/myapp_db_data/_data", "Name": "myapp_db_data", "Options": null, "Scope": "local" } ] ``` If your `docker-compose.yml` uses a bind mount, the path will appear under `Mounts` in `docker inspect` for the container: ```bash docker inspect db-1 --format '{{ json .Mounts }}' ``` Example output: ```json [{"Type":"bind","Source":"/home/user/myapp/data","Destination":"/var/lib/postgresql/data","Mode":"rw","RW":true,"Propagation":"rprivate"}] ``` Make a note of all volumes and bind mounts. During an upgrade, containers may be recreated, and if data is stored on ephemeral container layers, it will be lost. ### Verify data persistence with a restart test Before any upgrade, perform a controlled restart test to confirm that data persists across container recreation. Stop and restart a service: ```bash docker compose stop db docker compose start db ``` Then check that the application can still read its data. For example, if it is a PostgreSQL database, run: ```bash docker compose exec db psql -U myuser -c "SELECT count(*) FROM mytable;" ``` Expected output: ```text count ------- 123 (1 row) ``` If the count is zero or the table is missing, the data was likely stored in the container layer, and you need to correct the volume configuration before proceeding. ## Safe Configuration Path A safe configuration path means editing your `docker-compose.yml` with a clear understanding of what each change does and having a rollback plan. Avoid making multiple unrelated changes at once. ### Backup the current configuration Make a timestamped copy of your current `docker-compose.yml` and any related `.env` files: ```bash cp docker-compose.yml docker-compose.yml.bak.$(date +%Y%m%d%H%M%S) ``` Example: ```text docker-compose.yml.bak.20250315120000 ``` Also backup any `.env` file: ```bash cp .env .env.bak.$(date +%Y%m%d%H%M%S) ``` This ensures you can restore the previous configuration quickly if needed. ### Update the Compose file version key (if applicable) For legacy Compose files, the top-level `version` key was used to specify the schema version. In Docker Compose v2, this key is obsolete but still accepted for backward compatibility. If you are migrating from v1 to v2, you can remove the `version` key or leave it; it will be ignored. Example old-style file: ```yaml version: '3.8' services: web: image: nginx:1.25 ports: - "8080:80" ``` You can safely delete the `version` line. The new file would start directly with `services:`. ### Validate configuration before applying Use the config command to parse and validate your Compose file: ```bash docker compose config ``` This prints the normalized configuration. If there are syntax errors, it will report them. Example of a successful output (truncated): ```yaml name: myapp services: web: image: nginx:1.25 ports: - mode: ingress target: 80 published: "8080" protocol: tcp networks: default: name: myapp_default ``` Check for any warnings or unexpected changes in the output. Also run: ```bash docker compose config --quiet ``` If this command exits with code 0 and no output, the configuration is valid. ### Manage environment variables safely Never hardcode secrets in `docker-compose.yml`. Use an `.env` file for environment variables. For example, in your Compose file: ```yaml services: db: image: postgres:16 environment: POSTGRES_PASSWORD: ${DB_PASSWORD} ``` Create an `.env` file with the actual value (but never commit this file to version control): ```text DB_PASSWORD=change_me_strong_password ``` Validate that the variable is substituted correctly: ```bash docker compose config | grep POSTGRES_PASSWORD ``` Expected output shows the actual value. To avoid exposing secrets in logs, consider using Docker secrets in production (Swarm) or external secret management. ### Apply the change with a rolling update or recreate For a simple change like updating an image tag, you can run: ```bash docker compose up -d --no-deps web ``` This recreates only the `web` service without touching dependencies. If you need to force recreation: ```bash docker compose up -d --force-recreate web ``` Observe the output: ```text [+] Running 1/1 ✔ Container myapp-web-1 Recreated ``` Then verify the service is healthy: ```bash docker compose ps ``` ### Example migration from v1 to v2 If you are on the old `docker-compose` binary, migrate to the v2 plugin. First, check if v2 is installed: ```bash docker compose version ``` If not installed, follow the official Docker installation guide for your OS. On Ubuntu, install the `docker-compose-plugin` package: ```bash sudo apt-get update sudo apt-get install docker-compose-plugin ``` After installation, update any scripts that call `docker-compose` to use `docker compose` (space instead of hyphen). Test with your project: ```bash cd /path/to/project docker compose up -d ``` ## Verification and Diagnostics After applying any configuration change or upgrade, verify that the application is functioning correctly. This involves checking service health, logs, and connectivity. ### Check service status and health Use `docker compose ps` to see the status: ```bash docker compose ps ``` Example: ```text NAME COMMAND SERVICE STATUS PORTS web-1 "docker-entrypoint.s…" web running (healthy) 0.0.0.0:8080->80/tcp db-1 "docker-entrypoint.s…" db running (healthy) 5432/tcp ``` The `(healthy)` indicator appears if the container has a healthcheck defined. If your services lack healthchecks, add them to your Compose file for better observability. Example healthcheck for a web service: ```yaml services: web: image: nginx:1.25 healthcheck: test: ["CMD", "curl", "-f", "http://localhost/"] interval: 30s timeout: 10s retries: 3 ``` After adding, recreate the service: ```bash docker compose up -d web ``` Then check again. ### Examine logs for errors View logs for specific services: ```bash docker compose logs --tail 100 web ``` Look for startup errors, failed connections, or configuration errors. For example, a database connection error might appear as: ```text web-1 | [error] 23#23: *1 connect() failed (111: Connection refused) while connecting to upstream, client: 172.18.0.1, server: localhost, request: "GET / HTTP/1.1", upstream: "http://172.18.0.2:5432/" ``` This indicates the web service cannot reach the database, likely due to a network or service name change. ### Test connectivity and service endpoints For web applications, use `curl` from the host or inside another container. From the host: ```bash curl -I http://localhost:8080 ``` Expected output: ```text HTTP/1.1 200 OK Server: nginx/1.25.3 Date: Sat, 15 Mar 2025 12:00:00 GMT Content-Type: text/html ``` If you need to test from within the network, run a temporary container: ```bash docker run --rm --network myapp_default curlimages/curl curl -s http://web:80 ``` Replace `myapp_default` with your project's network name (check `docker network ls`). ### Validate configuration against the running state Compare the current configuration with the deployed state: ```bash docker compose config --services ``` This lists all services defined. Then compare with `docker compose ps --services` to see which ones are running. Any discrepancy may indicate a problem. ### Check resource usage and performance Use `docker stats` to monitor resource consumption: ```bash docker stats --no-stream ``` Example output (partial): ```text CONTAINER ID NAME CPU % MEM USAGE / LIMIT MEM % NET I/O BLOCK I/O PIDS abc123def456 web-1 0.50% 25.5MiB / 1.95GiB 1.28% 1.2kB / 0B 0B / 0B 3 def456abc123 db-1 2.10% 102.4MiB / 1.95GiB 5.12% 850kB / 1.1MB 9.8MB / 12MB 27 ``` Unexpected high CPU or memory usage may indicate a misconfigured service after upgrade. ## Failure Modes and Recovery Every upgrade should be planned with failure in mind. Here are common failure modes and specific recovery steps. ### Configuration syntax error If `docker compose up` fails with a YAML error, revert to the backup configuration. Example error: ```text yaml: line 5: could not find expected ':' ``` Recovery: restore the backup file: ```bash cp docker-compose.yml.bak.YYYYMMDDHHMMSS docker-compose.yml ``` Then validate again: ```bash docker compose config --quiet ``` ### Incompatible image version If the new image version fails to start due to missing dependencies or changed entrypoint, check logs, then rollback the image tag. Example: you changed `image: myapp:2.0` to `myapp:3.0` and the container exits with error. Logs show: ```text web-1 | Error: Cannot find module 'express' ``` Recovery: change the image tag back to the previous version in `docker-compose.yml`, then run: ```bash docker compose up -d --force-recreate web ``` ### Service dependency failure If a dependent service fails to start because another service is unhealthy, check healthchecks and startup order. Compose v2 supports `depends_on` with conditions: ```yaml services: web: depends_on: db: condition: service_healthy ``` Ensure the db service passes its healthcheck. If not, inspect db logs and fix the underlying issue. ### Volume or data loss If after an upgrade the application reports missing data, check the volume mounts. Use `docker inspect` to see mount points: ```bash docker inspect db-1 --format '{{ json .Mounts }}' ``` If the volume is missing or points to a different location, restore data from backups. Always take volume backups before major changes. Example backup for a PostgreSQL volume: ```bash docker run --rm -v myapp_db_data:/data -v $(pwd):/backup alpine tar czf /backup/db_data_backup.tar.gz -C /data . ``` Restore: ```bash docker run --rm -v myapp_db_data:/data -v $(pwd):/backup alpine sh -c "cd /data && tar xzf /backup/db_data_backup.tar.gz" ``` ### Rollback procedure A general rollback procedure: 1. Stop the affected services: ```bash docker compose stop web db ``` 2. Restore the previous configuration from backup: ```bash cp docker-compose.yml.bak.TIMESTAMP docker-compose.yml ``` 3. Recreate the services with the old configuration: ```bash docker compose up -d --force-recreate ``` 4. Verify the application is healthy. ## Operations Checklist Use this checklist to ensure a safe Docker Compose upgrade or migration. Fill in the specific values for your environment. | Step | Command / Action | Expected Result | Notes | |------|------------------|-----------------|-------| | 1. Record current version | `docker compose version` | Output shows version (e.g., v2.24.5) | Note v1 vs v2 | | 2. Capture running state | `docker compose ps --services` | List of running services | Compare to config | | 3. Backup config and env | `cp docker-compose.yml docker-compose.yml.bak.$(date +%Y%m%d%H%M%S)` | File created | Also backup .env | | 4. Backup volumes (if any) | `docker run --rm -v VOLUME:/data -v $(pwd):/backup alpine tar czf /backup/volume_backup.tar.gz -C /data .` | Tar archive created | Replace VOLUME with actual volume name | | 5. Validate config | `docker compose config --quiet` | Exit code 0, no output | Fix any errors | | 6. Apply change | `docker compose up -d` or specific service | Services recreated | Use `--no-deps` to limit | | 7. Check status | `docker compose ps` | All services running, healthy if healthchecks defined | Look for restarts | | 8. Check logs | `docker compose logs --tail 50 SERVICE` | No fatal errors | Investigate warnings | | 9. Test functionality | `curl -I http://localhost:PORT` or appropriate command | HTTP 200 or expected response | Check critical endpoints | | 10. Monitor resources | `docker stats --no-stream` | CPU and memory within normal range | Watch for leaks | | 11. Confirm data persistence | Restart test: `docker compose stop` then `start` | Data intact | If lost, fix volumes | | 12. Document rollback plan | Write down restore commands | Clear steps saved | Test rollback in staging if possible | ### Example checklist filled for a sample project Assume a project named `myapp` with services `web` and `db`, using a named volume `myapp_db_data`, and upgrading image tag from `web:1.0` to `web:2.0`. 1. Current version: `docker compose version` returns `Docker Compose version v2.24.5`. 2. Running services: `docker compose ps --services` returns `web` and `db`. 3. Backup config: `cp docker-compose.yml docker-compose.yml.bak.20250315120000`. 4. Backup volume: `docker run --rm -v myapp_db_data:/data -v $(pwd):/backup alpine tar czf /backup/myapp_db_data_backup.tar.gz -C /data .` 5. Validate config: `docker compose config --quiet` returns no error. 6. Apply change: edit image tag to `web:2.0`, then `docker compose up -d web`. 7. Check status: `docker compose ps` shows `web` running and healthy. 8. Check logs: `docker compose logs --tail 50 web` shows startup success. 9. Test functionality: `curl -I http://localhost:8080` returns `HTTP/1.1 200 OK`. 10. Monitor resources: `docker stats --no-stream` shows web using 30MB RAM, normal. 11. Confirm data persistence: `docker compose stop` then `docker compose start`, then `docker compose exec db psql -U myuser -c "SELECT count(*) FROM mytable;"` returns 123. 12. Document rollback: `cp docker-compose.yml.bak.20250315120000 docker-compose.yml && docker compose up -d --force-recreate`. ## Conclusion Upgrading and migrating Docker Compose is a routine but risky operation. A systematic approach reduces downtime and data loss. By inventorying your environment, backing up configuration and data, validating changes, and verifying functionality after each step, you maintain control over the process. Remember to version-scope every recommendation, observe before changing, and always have a rollback plan. Start with a low-risk verification on a staging environment or a non-critical service, record the current state, apply the change, and compare the result against the expected signal. With these practices, Docker Compose upgrades become predictable and safe, even in production. Use the checklist as a living document that evolves with your infrastructure and team experience. For further learning, explore Docker's official documentation on Compose file reference, networking, and volumes, and consider automating these steps with CI/CD pipelines in your development workflow.