You write a shiny compose.yml, run docker compose up -d, and everything starts at once. Postgres is still initializing when your web app tries to connect. The migration container runs before the database accepts queries. Redis is listed as a dependency, but your cache-dependent service crashes three times before it finds the port open.
This is the startup order problem — and it is one of the most common pain points in homelab Docker deployments.
Docker Compose gives you the tools to solve it, but the defaults (depends_on without conditions) only guarantee container start order, not service readiness. This guide covers every option from basic to advanced: depends_on conditions, healthcheck-based gating, init container patterns, and the classic wait-for-it script approach.
depends_on — What It Actually Does and Does Not Do
The simplest form of depends_on tells Compose to start one container after another:
|
|
Without any condition, depends_on only guarantees that the db container is started before app. It does not wait for Postgres to be ready to accept connections. On a fast machine with local volumes, this usually works. On a homelab with spinning disks or during first boot when databases initialize, it fails reliably.
Supported Compose plugins provide conditional depends_on; do not infer the plugin version from the Engine version or a file-format version. Validate with docker compose config:
|
|
Three conditions are available:
service_started— the default; starts after the dependency container is runningservice_healthy— waits until the dependency passes its healthcheckservice_completed_successfully— waits until the dependency runs and exits with code 0 (init container pattern)
The service_healthy and service_completed_successfully conditions are what turn depends_on from a simple ordering hint into a real readiness gate.
Healthcheck-Based Startup Ordering
The most reliable pattern for production is pairing depends_on with healthcheck on the dependency service. Docker runs the healthcheck command periodically and marks the container as healthy or unhealthy. The dependent service waits until healthy before starting.
Here is a complete example with Postgres, Redis, and a web application:
|
|
Key decisions in this config:
start_period: 30sgives Postgres time to initialize on first boot before healthcheck failures count towardretries. Without this, a cold Postgres init could exhaust retries before the server is ready.interval: 5sbalances quick detection with reasonable overhead — every 5 seconds is fine for homelab, even with many containers.restart: unless-stoppedensures the application retries if it fails while waiting for dependencies. Compose starts the app, the healthcheck is still pending, the app crashes, Docker restarts it, and eventually the healthcheck passes.
Starting Additional Dependencies by Service Type
Homelab stacks often need MariaDB or MySQL instead of Postgres:
|
|
MariaDB’s official image bundles healthcheck.sh with --innodb_initialized — safer than a raw MySQL ping because it also verifies InnoDB recovery completed.
Init Container Pattern in Docker Compose
Kubernetes has native init containers: they run to completion before the main pod starts. Docker Compose has no built-in init container type, but you can simulate the pattern cleanly with depends_on and condition: service_completed_successfully.
Use this for:
- Database schema migrations (Alembic, Flyway, Prisma)
- Seed data loading
- Terraform / Pulumi provisioning
- File permission setup on shared volumes
Here is a real example with Alembic migrations for a FastAPI app:
|
|
The chain is: postgres becomes healthy → migrations runs alembic upgrade head and exits 0 → app starts knowing the schema is current.
For multi-step init, chain multiple init services:
|
|
The wait-for-it and wait-for Scripts
Not every image ships with a healthcheck. For third-party images you cannot modify, the wait-for-it.sh script (or the simpler wait-for) is a practical fallback.
wait-for-it
Available at vishnubob/wait-for-it, this bash script blocks until a TCP port is open:
|
|
The script polls db:5432 until it accepts a TCP connection, then executes the command.
wait-for (Efficient Python Alternative)
The wait-for script is a shorter, POSIX-sh alternative that works on Alpine:
|
|
Important: Port-open checks have a race condition. The TCP port can be open before the service is ready (Postgres immediately opens the port, but may reject queries for several seconds during crash recovery). Healthchecks are strictly better. Use port-waiting only when you cannot add a healthcheck.
Advanced Patterns for Complex Stacks
Chained Dependencies
|
|
Conditional startup with profiles
Profiles select optional services; they do not make an enabled application’s required dependency disappear. An always-enabled app depending on a migration service hidden behind an inactive profile can produce an invalid Compose model. Put a required migration in the normal dependency graph, or put the whole optional application group in the same profile. See Compose profiles.
For a manually managed migration, run and check the migration explicitly before starting the application; remove the automatic dependency only if the deployment procedure now enforces that ordering. A profile is not an error-handling or migration-rollback mechanism.
Diagnose unexpected health state
A health check reports the result of its configured probe. Inspect its command, output, and timing before recreating anything:
|
|
Replace container_name with the inspected container. A probe can succeed while the application is unusable if it tests the wrong condition. Correct the probe and apply the configuration in a maintenance window with docker compose up -d. Do not delete volumes or restart the whole Docker daemon to clear a supposed Compose health cache.
Summary and Best Practices
Here is a decision tree for Docker Compose startup ordering in your homelab:
| Situation | Pattern | Condition |
|---|---|---|
| Dependencies start fast (local Alpine images, no init) | depends_on: (bare) |
service_started |
| Database needs to accept queries | healthcheck + depends_on condition |
service_healthy |
| Migration must run before app starts | Init container | service_completed_successfully |
| Third-party image, no healthcheck | wait-for-it.sh |
Port check |
| Complex multi-service dependency chain | Chained healthchecks | service_healthy each |
Start with healthchecks and depends_on condition: service_healthy. This covers 80% of homelab scenarios. Add the init container pattern for database migrations. Use wait-for-it only as a last resort for images you cannot modify.
Set reasonable start_period values — 30-60 seconds for databases, 10 seconds for caches. Use restart: unless-stopped on all services that have dependencies so they retry automatically after a crash.
Your Compose stack will boot reliably every time, whether it is the first deploy on bare metal or a restart after a power outage.