The depends_on: key controls the order in which Compose starts services, but it has a sharp edge that catches almost everyone the first time. The short form depends_on: [db, redis] and the long form with condition: service_started only wait for the container to be created and started, not for the service inside that container to be ready to accept connections. For a database, that means your app may begin trying to connect before the database has finished initializing. The fix is to add a healthcheck: to the dependency and use condition: service_healthy instead.
A healthcheck is a per-service test that Docker runs inside the container on a schedule. For Postgres, a typical healthcheck uses pg_isready -U postgres; for a web service you might curl a health endpoint. The schema accepts test (the command to run), interval (how often to run it), timeout, retries (consecutive failures before marking unhealthy), and start_period (an initial grace window during which failures don't count). The defaults are 30s interval, 30s timeout, 3 retries, and no start period, which is too aggressive for slow-starting databases, so tune these for your workload. Once a service has a healthcheck, dependents can wait on it via depends_on: db: condition: service_healthy, and even better, the dependent itself should retry its connection on startup rather than assuming a single attempt is enough.
Beyond service_healthy, Compose offers service_completed_successfully for one-shot init or migration containers. You mark a service as a one-off with restart: 'no', give it a command that exits when its work is done, and then other services depend on it with the completed-successfully condition. This is the cleanest pattern for running database migrations or seed scripts before the main app boots. Restart policies are also part of service health: restart: always restarts no matter how the container exited, on-failure[:N] only restarts on a non-zero exit with an optional retry cap, and unless-stopped restarts always except when you explicitly stopped it. For local development, unless-stopped is usually the right default because it survives reboots but lets you take a service down deliberately.