Docker Compose is easy to adopt for the wrong reason. A team sees one YAML file, two commands, and a working local stack, then starts treating Compose as either a toy or a small Kubernetes. Both readings miss its strongest operating shape. Compose works best when compose.yaml is the local contract for an application: which services exist, how they are built or pulled, which ports and volumes matter, which dependencies must become healthy first, which helper services are optional, and where the boundary ends.
That boundary is now more explicit than old docker-compose.yml habits suggest. The Compose Specification defines an application as services connected by networks, volumes, configs, and secrets, while warning that implementations may support different attributes and should warn or reject unsupported ones depending on mode.[1] Docker's service reference is just as revealing: a Compose file must declare a top-level services map, each service can use an image and runtime arguments, build is optional, and deploy support is optional if the platform does not implement it.[2] In other words, the file is not magic. It is a model that a specific implementation turns into containers.
That makes this an adoption note rather than a cheer for simpler YAML. Compose is a good migration target when the team needs a repeatable local stack, a narrow self-hosted service, a CI smoke-test environment, or a developer onboarding path that stops living in a README. It is a weak target when the team actually needs scheduling, rolling updates, autoscaling, multi-node service discovery, or cluster policy. If the file stays honest about that distinction, Compose is durable. If it starts hiding production orchestration wishes inside local defaults, it becomes technical debt with indentation.
Migrate the stack, not the deployment platform
The first migration question is not "Can this run under Compose?" Most containerized applications can be made to start somehow. The better question is whether the stack has a small enough shape that a single application model helps people reason about it. A web service, worker, database, cache, object-store emulator, migration job, and debug UI can make sense in one compose.yaml. A fleet of regionally scaled workloads, rollout policies, identity sidecars, ingress controllers, and autoscalers usually cannot.
The Compose service model should be read as a set of ownership statements. services.web.build.context says the image comes from local source. services.db.image says the database is a runtime dependency, not part of the app build. ports names which host-visible surface is intentional. volumes separates disposable containers from data that must survive docker compose down. configs and secrets make configuration and sensitive values first-class concepts in the model, even when a local implementation provides them differently from a swarm or cloud platform.[1][2]
That is the migration value. A new developer should be able to open the file and learn the system's local dependency graph without reading five wiki pages. CI should be able to launch the same dependency shape for integration tests. An operator running a small internal tool should be able to see where state lives before touching upgrades. Compose should replace "run these commands in this order" with a versioned stack contract.
The anti-pattern is the mega-file. Once compose.yaml becomes a dumping ground for every production dream, nobody knows which parts are real. Keep the first file boring: app services, local dependencies, named volumes, health checks, and explicit ports. Put optional tools behind profiles. Put large subdomains behind include only when the ownership boundary is already clear. A Compose migration succeeds when the file gets more legible over time, not when it accumulates every knob Docker can parse.
Health checks are the difference between order and readiness
The most common Compose mistake is treating start order as readiness. Docker's startup-order docs make the distinction directly. depends_on can express service_started, service_healthy, or service_completed_successfully; a dependency marked service_healthy must pass its healthcheck before the dependent service is created.[3] The official example uses PostgreSQL with pg_isready, a 10-second interval, 5 retries, a 30-second start period, and a 10-second timeout before web is allowed to start against db.[3]
That example is more than syntax. It is a reliability boundary. If a web app starts before Postgres accepts connections, the failure will usually appear in the app logs. If a migration container exits early because Redis is still booting, the failure becomes a script problem. If a test suite races a database initialization path, developers learn to rerun tests instead of fixing the system. depends_on with condition: service_healthy turns that timing assumption into configuration.
Do not overstate it. Compose health checks are not a replacement for application-level retries, database migration discipline, or production readiness probes. They are a local contract saying "this service should not be considered available to its neighbor until this command succeeds." That is enough to remove a large class of onboarding and CI flakiness. It also makes real dependencies visible. If api cannot start without db, redis, and migrate, the YAML should say that instead of outsourcing the fact to tribal memory.
The strongest rule is to health-check the thing clients actually need. For Postgres, pg_isready is better than "container process exists." For an HTTP service, a small /healthz endpoint is better than "port is open." For a one-shot setup container, service_completed_successfully is a more honest dependency than leaving the app to discover that setup did not happen.[3] Compose is not giving you a production control plane here. It is giving you a readable local failure boundary.
Profiles keep optional tools from becoming mandatory services
Profiles are where Compose can stay compact without lying. Docker's profile guide says services can be assigned to profiles through the profiles attribute, and that services without a profiles attribute are always enabled. Its own tip is the practical rule: core application services should not be assigned profiles, so they are always enabled and automatically started.[4]
Use that rule aggressively. A database, message broker, and app process are probably core. A database admin UI, Mailpit, Jaeger, local S3 browser, fake payment gateway, load generator, or benchmark worker is probably optional. Put optional tools under names such as debug, observability, admin, or bench, then require a developer to ask for them with --profile.
This reduces two kinds of confusion. First, docker compose up remains the smallest viable stack. Second, helper tools stop looking like production dependencies. If a service is present only because one team occasionally needs it, hiding it behind a profile is cleaner than leaving every laptop to start it and every new engineer to wonder whether the app depends on it.
Profiles also help with cost and noise. A local observability stack can be valuable, but not every branch needs it. A browser automation sidecar may be useful for end-to-end tests, but not for editing a CSS bug. Compose adoption should lower the default cognitive load. Profiles are one of the few features that let a single file serve multiple workflows without turning the default workflow into a parade.
Watch and include are power tools, not excuses
Compose Watch is a useful sign that the project has kept moving beyond "start some containers." Docker's Watch docs say the watch attribute, available in Compose 2.22.0 and later, can update running services as code changes; rules can sync, rebuild, or restart based on paths, targets, ignores, and initial sync behavior.[5] The docs also draw an important boundary: Watch is designed for services built from local source with build, and it does not track changes for services that rely only on a prebuilt image.[5]
That is exactly the right scope. Watch is strongest for local development loops where bind mounts are too blunt. Syncing source files while ignoring node_modules/, then rebuilding when package.json changes, is a better contract than mounting the entire project and hoping the host and container agree about native artifacts.[5] It lets the Compose file describe how code reaches the container, not merely that a directory is mounted.
include solves a different problem. Docker's include docs say it requires Compose 2.20.0 and later, loads another Compose file as its own application model with its own project directory, copies resource definitions into the current model, warns on resource-name conflicts, and does not try to merge conflicting resources.[6] That is a governance feature disguised as convenience.
Use include when a subdomain has its own owner: a shared observability sidecar, a common local identity service, a reusable test dependency, or a platform-provided emulator. Do not use it to hide complexity nobody wants to maintain. The fact that included files keep their own relative paths and environment defaults is useful because it preserves ownership boundaries.[6] The fact that conflicts warn instead of magically merge is also useful because it prevents two teams from silently defining the same resource differently.[6]
The conservative rule is simple: watch should make the edit-run loop faster, and include should make ownership clearer. If either feature makes it harder to predict what docker compose up will do, the file has crossed from contract into maze.
Know when the orchestrator boundary has arrived
Compose should not be stretched into a scheduler because schedulers already have a different job. Kubernetes Deployments, for example, describe desired state for Pods and ReplicaSets; the Deployment controller changes actual state toward that desired state at a controlled rate, creates new ReplicaSets during updates, and gradually scales old and new ReplicaSets during rollout.[7] That is not a local stack concern. It is a reconciliation system.
This is the cleanest production boundary. If the application needs multi-node placement, controlled rollouts, pod replacement, horizontal scaling, cluster service discovery, admission policy, network policy, or tenant-level resource governance, do not ask Compose to become that system. Use Compose for local development, smoke tests, or small single-host operation, then translate the production contract into Kubernetes, Nomad, systemd units, or another real runtime with the right control plane.
The independent Okteto documentation is useful because it shows how the ecosystem reads this boundary from the other side. Okteto implements and extends the Compose Specification so developers can use Docker Compose applications in Kubernetes without dealing directly with Kubernetes manifest complexity.[8] That is not evidence that Compose and Kubernetes are the same thing. It is evidence that Compose can be a developer-facing model while a different platform owns deployment behavior.
A mature team can live with both. Compose defines the local stack. Kubernetes manifests, Helm charts, Kustomize overlays, or platform templates define the cluster state. The discipline is to avoid pretending that one file is the other. Port numbers, environment variables, image names, and health endpoints should line up. Rollout semantics, placement, ingress, secrets integration, and policy should live where the runtime can enforce them.
A practical migration path
Start with one compose.yaml that can boot the minimum useful application. Use canonical service names: web, api, worker, db, redis, migrate, not internal nicknames. Prefer named volumes for state that must survive. Put all host-visible ports in one place and remove any port that no human or test actually needs. Add health checks before optional tools. If the app still needs a README sequence after that, the Compose file is not done.
Next, split optionality with profiles. Make docker compose up boring and reliable. Add docker compose --profile debug up only for debug UI and local inspection tools. Add docker compose --profile observability up only if tracing, metrics, or log exploration is needed during the current task. Keep helper services out of the core path unless the app fails without them.[4]
Then improve the edit loop. Add develop.watch only to services built from local source, and write explicit ignore rules for dependency directories or generated artifacts that should not cross the host/container boundary.[5] If a dependency change requires a rebuild, say so in the watch rule rather than expecting developers to know when to restart by hand.
Finally, document the exit criteria. Compose is still the right center if the stack is single-host, small, inspectable, and mostly about reproducible startup. It is the wrong center when failure recovery, rollout control, service routing, policy enforcement, and scale become the main work. A good migration plan names that threshold in advance.
Docker Compose remains valuable because it refuses to be glamorous. It gives teams a shared local application model with enough structure for services, networks, volumes, configs, secrets, health, profiles, watches, and includes. The adoption win is not that YAML replaces operations. The win is that the local stack stops being folklore. Keep compose.yaml as the contract, and Compose will stay useful long after the first successful up.
Sources
- Compose Specification, "The Compose Specification" - application model, services, networks, volumes, configs, secrets, and implementation-support caveats.
- Docker Docs, "Define services in Docker Compose" -
servicesas the required top-level map, service definitions,build, and optionaldeploybehavior. - Docker Docs, "Control startup and shutdown order in Compose" -
depends_on,service_healthy,service_completed_successfully, and the PostgreSQL health-check example. - Docker Docs, "Using profiles with Compose" -
profilesattribute, always-enabled services, profile-name rules, and the guidance to keep core services unprofiled. - Docker Docs, "Use Compose Watch" -
develop.watch, Compose 2.22.0 requirement, sync/rebuild behavior, ignore rules, and build-vs-image boundary. - Docker Docs, "Use include to modularize Compose files" - Compose 2.20.0 requirement, included application models, project-directory behavior, and conflict warnings.
- Kubernetes Docs, "Deployments" - desired-state control, Pods and ReplicaSets, rollout behavior, and controller-managed updates.
- Okteto Docs, "Docker Compose Reference" - independent platform documentation showing Compose as a developer-facing model implemented and extended for Kubernetes development workflows.
- Wikimedia Commons, "Shipping containers in a port (Unsplash).jpg" - real Port of Barcelona photograph used as the article image.