2026-07-27
Make Docker Compose dependencies optional without weakening correctness
Keep correctness-critical services required while making proven telemetry, cache, and debugging dependencies optional in Docker Compose.
An optional dependency is not a service you would prefer not to run. It is a service whose absence the application has already been designed and tested to tolerate. Docker Compose can select that service out of a topology or relax a startup dependency requirement. It cannot make a database outage safe, reconnect an exporter, or decide what to do with data that could not be delivered.
Start by classifying dependencies by correctness, not convenience. Databases, migrations, authorization or policy engines, durable queues when a write requires them, and anything needed to preserve a transactional invariant stay required. A cache, telemetry collector, or debugging tool may be optional only when the application has a tested behavior for both its absence and a later failure.
Model the two Compose controls separately
Profiles select services in the active application model. Services with no profile are enabled by default, which is why the correctness-critical path should remain unprofiled. Enabling a profile adds its services to that model. Explicitly targeting a profiled service starts that service and its declared dependencies, rather than every service with the same profile. Docker documents both behaviors in its profiles guide.
Long-form depends_on answers a different question. required: false tells Compose to warn rather than fail when the dependency is not started or available. It was added in Docker Compose 2.20.0. condition: service_healthy makes a passing healthcheck the startup condition. The depends_on reference describes both fields.
Neither setting creates degraded application behavior. Treat profiles as topology selection and required: false as startup dependency handling.
Here is a compact shape for an application that can genuinely run without telemetry:
services:
app:
image: ghcr.io/example/app@sha256:<app-image-digest>
environment:
OTEL_EXPORTER_OTLP_ENDPOINT: http://telemetry:4318
depends_on:
telemetry:
condition: service_healthy
required: false
telemetry:
image: otel/opentelemetry-collector-contrib:<pinned-version>
profiles:
- observability
healthcheck:
test: ["CMD", "/otelcol-contrib", "--version"]
interval: 5s
timeout: 2s
retries: 5Replace the placeholders with an immutable image reference and a healthcheck that proves the collector is ready for the application protocol. The sample is a configuration pattern, not an executed application. It is only appropriate when app can start without resolving telemetry, can continue when the collector disappears, and exposes that degraded state to operators.
Do not add restart: true to imply runtime supervision. In long-form depends_on, it applies when Compose explicitly updates the dependency, not when the container runtime automatically restarts a container after it dies. That boundary is also documented in the services reference.
Define the application's absence contract
Startup absence and failure after startup need the same explicit application contract. This is architecture work, not a behavior Compose supplies:
| Concern | Contract to define and test |
|---|---|
| Connection attempts | Bounded attempts and timeouts that cannot block application readiness indefinitely |
| Degraded state | A visible health, metric, log, or alert signal that distinguishes normal operation from missing telemetry |
| Data handling | Documented loss, buffering, retry, and backpressure policy, including bounds and ownership |
| Recovery | Reconnection behavior or an operator recovery procedure after the dependency returns |
If a request cannot be processed correctly without the service, it is required. required: false must not become a way to hide a failing database, policy decision, migration, or durable write path.
Validate every supported topology
Validate the rendered model before starting it. Docker Compose documents docker compose config --quiet as validation without printed configuration in the config command reference. Then inspect the active service list and exercise each topology independently.
# Default topology: only unprofiled services.
docker compose config --quiet
docker compose config --services
docker compose up -d --wait
docker compose ps
docker compose logs --no-log-prefix app
docker compose down
# Profile-enabled topology: application plus telemetry.
docker compose --profile observability config --quiet
docker compose --profile observability config --services
docker compose --profile observability up -d --wait
docker compose --profile observability ps
docker compose --profile observability down
# Targeted operational service: check its dependency closure explicitly.
docker compose up -d --wait telemetry
docker compose ps
docker compose --profile observability downThe final command is deliberately separate from enabling the profile. Targeted-service behavior is part of the operator interface, so test it rather than inferring it from profile behavior.
Use a verification matrix that includes application-specific probes, not only container status:
| Scenario | Compose evidence | Application evidence |
|---|---|---|
| Default topology | config --quiet, config --services, up -d --wait, ps, and logs |
Readiness and a request that succeeds without telemetry |
| Observability profile | The same commands with --profile observability |
Telemetry path is usable without changing correctness behavior |
| Targeted telemetry | up -d --wait telemetry, then ps |
The expected service closure, with no accidental full-profile startup |
| Telemetry unavailable at startup | Start with it absent or unhealthy; inspect Compose output and logs | Bounded startup, visible degraded state, and declared loss or buffering behavior |
| Telemetry fails after startup | Stop it after the app is ready; inspect ps and logs |
Continued correctness, bounded retries, and the expected operator signal |
| Recovery | Restore the dependency and inspect its health | Reconnection or the documented operator recovery path |
For the last three rows, make the application probe specific to its contract. A running container does not prove that retries are bounded, a buffer is finite, or data-loss behavior is acceptable.
What this Compose fixture established
Local evidence was executed on 2026-07-27 with Docker Compose v5.3.1. The fixture used pinned alpine:3.23 content, an always-enabled app, and telemetry in the observability profile. app declared telemetry with condition: service_healthy and required: false.
| Check | Observed result |
|---|---|
| Default rendered model | config --quiet passed; config --services returned only app |
| Profile-rendered model | --profile observability config --quiet passed; services were telemetry and app |
| Default startup | up -d --wait started only app |
| Explicit telemetry target | up -d --wait telemetry started only telemetry, not app |
| Profile startup | --profile observability up -d --wait started both services |
| Failure and recovery | After telemetry was stopped, app remained running; restarting telemetry returned it to healthy |
The Alpine loop proves Compose topology, startup, and runtime container behavior only. It does not prove any application's degraded mode, bounded retries, event loss policy, or reconnection behavior.
Keep the distinction intact in reviews: Compose can make a service absent from a model and can relax a startup requirement. The application still owns whether absence is safe.