Most people learn just enough Compose to start a database and a web server.
1. Service profiles
Profiles let you include optional services only when they are needed. A debugging dashboard or a load generator can be tagged with profiles: [tools] and started with docker compose --profile tools up, leaving the default start-up lean.
2. Health checks with conditional dependencies
A plain depends_on only controls start order. Combined with a healthcheck and condition: service_healthy, it ensures that an API does not start until its database is genuinely ready to accept connections.
api:
depends_on:
db:
condition: service_healthy
db:
image: postgres:17
healthcheck:
test: ["CMD-SHELL", "pg_isready -U postgres"]
interval: 5s
retries: 103. Watch mode
docker compose watch monitors your source tree and either synchronises changed files into a running container, rebuilds the image or restarts the service, according to rules you define under develop.watch. It offers a fast feedback loop without the performance penalties sometimes associated with bind mounts.
4. Multiple Compose files
Passing several files with -f merges them in order. A base file can describe the shared architecture while an override adds development-only conveniences. Compose automatically applies compose.override.yaml when it exists.
5. The include element
For larger systems, include imports another Compose project as a self-contained module, preserving its own relative paths. This is an elegant way for separate teams to own separate parts of a stack.
6. Variable interpolation
Values such as ${TAG:-latest} are substituted from the shell or from a .env file, with sensible defaults. Interpolation keeps one file usable across many environments.
7. Scaling
Stateless services can be replicated with docker compose up --scale worker=4. Compose distributes the containers on the project network, and the internal DNS returns every replica's address.
8. Secrets and configs
Sensitive and non-sensitive files can be mounted into services in a controlled, read-only manner, avoiding the temptation to bake them into images.
9. Restart policies
Policies such as unless-stopped instruct the Docker Engine to revive containers after a crash or host reboot, which is valuable for single-server deployments.
10. Resource limits
Under deploy.resources you can cap CPU and memory, preventing one misbehaving container from starving the rest of the machine.
Choose features according to operational intent
Profiles, health checks and watch rules solve different problems and should remain conceptually distinct. Combining them without an explicit purpose creates configuration that is impressive to read but difficult to reason about.
Document which capabilities depend on a recent Compose release. Teams should validate the minimum supported plugin version in their development instructions rather than assume that every workstation interprets newly introduced attributes identically.
Combine features around a real workflow
Docker compose features are most useful when they solve a concrete problem in your application. Adding them for a stated reason produces a configuration that a teammate can understand.
Build the workflow incrementally. Then introduce the health check and test startup with a fresh disposable database. Add the administration profile last and verify both the default startup and the explicitly enabled profile. This order gives you a working reference after each change. If a new option changes behaviour unexpectedly, you can compare two small configurations instead of investigating a large file in which many unrelated features arrived together.
Use profiles to express optional work
Profiles are useful for tools such as an administration interface, a test helper or a development-only service. They let the core application remain easy to start without requiring every helper to run. Name profiles according to the task they enable so that the startup command is self-explanatory. Document which services belong to the default workflow and which require explicit selection. A new contributor should not have to inspect every service definition to discover why a tool mentioned in the setup guide is missing from the running project.
Be careful with dependencies across optional services. A core service should not unexpectedly require a helper that is disabled in the normal workflow. Test the combinations your documentation supports and inspect the resolved model with the selected profiles. Keep the number of combinations manageable. If every service has several overlapping profile requirements, a small project can become difficult to reason about. Profiles should make a workflow easier to choose, rather than becoming a substitute for deciding which parts of the application genuinely belong together.
Understand what health checks actually prove
A useful health check measures a condition that matters to a dependency. A process existing inside a container is weaker evidence than a database accepting a suitable connection. At the same time, a check should not perform expensive work or change important data every few seconds. Choose a lightweight test, give startup an appropriate grace period and inspect the check output when it fails. The check runs in the container environment, so the required executable and configuration must be available there rather than only on your workstation.
Health information and recovery are separate concerns. Marking a container unhealthy does not mean every application problem will be repaired automatically. A dependent service may need retry logic, an operator may need to investigate logs and a persistent data issue may require a specific repair. Use health checks to expose a meaningful state and startup dependencies to coordinate initial readiness. Keep restart policies aligned with the process lifecycle. An application that repeatedly crashes because of an invalid configuration needs a corrected setting, not simply a faster restart loop.
Keep overrides and watch rules easy to inspect
Multiple files can separate shared structure from development conveniences, but the merged result is the configuration that runs. Use the same file order when validating and starting the project. Inspect the resolved model after changing mounts, ports or environment values, because a seemingly small override can affect a service's behaviour substantially. Keep development-only settings, such as a debugger listener or live source mount, clearly located. A reviewer should be able to identify the intended deployment configuration without mentally combining an unexplained collection of files.
Watch rules also deserve a specific purpose. Some file changes can be synchronised into a development container, while changes to dependencies or image construction may require rebuilding. Choose rules that reflect the application's real reload behaviour and exclude unnecessary generated files. Verify the complete loop: edit a file, observe the update and make a request that exercises the changed code. A successful file copy alone does not prove that the application reloaded it. Document the expected behaviour so that developers know when an explicit rebuild is still required.
Test resource and scaling assumptions
Resource limits help establish expectations, but a declaration is only useful if it is supported and enforced in your chosen environment. Observe the workload under realistic conditions and inspect resource use before selecting limits. A limit that is too low can cause failures during compilation or startup even if ordinary requests are cheap. Keep the application's own connection pools, worker counts and cache sizes consistent with the available resources. Container settings do not automatically tune every process inside the image.
Scaling a service creates additional containers, but it does not automatically solve shared state or request distribution. Check whether the application writes to local files, requires a unique background worker or publishes a fixed host port. These assumptions can prevent multiple replicas from behaving correctly. Test a small replica count with realistic requests before treating scaling as an operational plan. For applications requiring broader scheduling and recovery across machines, evaluate those responsibilities explicitly rather than assuming a replica flag supplies a complete cluster platform.
Frequently asked questions
Which docker compose feature should a beginner learn first?
Start with services, networks and volumes, then learn configuration validation and logs. Add health checks when startup readiness matters. These foundations explain most small projects and make advanced features easier to evaluate in context.
Does an unhealthy container always restart automatically?
Do not assume that health status alone triggers the recovery you need. Check the runtime behaviour and restart policy, and investigate the failing check. Persistent configuration or data problems require a specific correction rather than repeated restarts.
Are profiles the same as separate environments?
Profiles select optional services. Separate files and environment values can change other aspects of configuration. Choose the mechanism that matches the difference you need, then validate the resulting project with the same options used for startup.
Can I scale every service safely?
Only if the application supports multiple instances and the configuration avoids conflicting ports or exclusive local state. Test storage, background work and connection handling before increasing replicas. Some components are intentionally designed to remain a single instance.
Conclusion
The strongest docker compose features make an existing workflow clearer and more repeatable. Use profiles for optional tasks, health checks for meaningful readiness and overrides for deliberate environment differences. Verify watch behaviour, resource assumptions and replica compatibility with the application itself. A feature earns its place when you can explain the problem it solves and demonstrate the resulting behaviour from a clean, documented project startup.



