Spring Boot Docker depends_on Without Healthchecks: A Production Nightmare
Learn why Spring Boot Docker depends_on without healthchecks fails in production.
20+ years shipping production Java in banking & fintech. Everything here is grounded in real deployments.
- ✓Docker 20.10+ and Docker Compose 2.0+ installed
- ✓Basic Spring Boot 3.x project with JPA and MySQL dependencies
- ✓Familiarity with application.properties or application.yml configuration
- ✓At least 6 months of production Spring Boot experience
• Docker's depends_on only waits for container start, not service readiness • Spring Boot apps crash if DB/Redis aren't ready despite depends_on • Use healthchecks (curl, wget, or custom scripts) to ensure real readiness • For complex apps, use orchestration tools like Kubernetes or Docker Compose with condition
Think of depends_on like a parent telling a child to wait for their sibling to wake up before playing. The sibling might be awake but still groggy and unable to play. Docker's depends_on checks if the container started, not if the service inside is actually ready to accept connections. You need a healthcheck — like the sibling saying "I'm fully awake and ready to go" — before the dependent container tries to connect.
Docker Compose's depends_on directive is one of the most misunderstood features in container orchestration, especially for Spring Boot applications. On the surface, it seems straightforward: wait for another container to start before launching this one. But in production, this naive assumption leads to race conditions, application crashes, and hours of debugging. I've seen teams spend entire sprints chasing intermittent startup failures, only to discover that their MySQL container was "running" but not accepting connections when their Spring Boot app tried to initialize the HikariCP connection pool. The problem is fundamental: Docker's depends_on checks if a container process has started, not if the service inside is ready to serve traffic. For a Spring Boot app that depends on databases, Redis caches, or message brokers, this distinction is critical. In this article, we'll dissect exactly why depends_on fails, show you the production incident that cost a fintech company $50k in downtime, and provide battle-tested solutions using healthchecks, wait-for-it scripts, and Spring Boot's own retry mechanisms. By the end, you'll never trust depends_on alone again.
The Problem: depends_on Only Checks Container State
Docker's depends_on is often misunderstood as a readiness check. In reality, it only ensures that the specified container has started — meaning the container's main process is running. For a Spring Boot application connecting to MySQL, Redis, or RabbitMQ, this is insufficient. The target service might be running its initialization routines, loading data into memory, or establishing internal connections. Consider a typical docker-compose.yml for a Spring Boot app with MySQL. The payment-service depends on mysql, but MySQL's container starts in under a second while the actual server initialization takes 10-30 seconds depending on data volume. Spring Boot's HikariCP, configured with a default connection timeout of 30 seconds, will attempt to connect immediately. If mysql isn't ready, the application fails with a fatal exception and exits. In production, this creates a restart loop: the orchestrator restarts the container, but mysql still isn't ready, so it fails again. The only thing that saves you is luck — if mysql happens to be ready by the time Spring Boot retries (which it won't with default settings).
What the Official Docs Won't Tell You
Docker's official documentation states that depends_on "expresses dependency between services" and "service dependencies cause the following behaviors" — but it never explicitly says it checks readiness. The Compose specification version 3.8+ introduced the condition form for depends_on, allowing service_healthy as a condition. However, many teams don't use it because it requires defining a healthcheck on the dependency. The dirty secret is that even with condition: service_healthy, Docker Compose doesn't retry the healthcheck indefinitely. If the healthcheck fails for the configured retries, the dependent service still starts and fails. For Spring Boot specifically, the documentation doesn't address the race condition with HikariCP. Spring Boot 3.2's auto-configuration assumes the database is available at startup for schema validation. If you use spring.jpa.hibernate.ddl-auto=validate, the app fails fast if the database isn't ready. The recommended approach from Spring team — using spring.datasource.hikari.initialization-fail-timeout=-1 — is buried in release notes and rarely mentioned in tutorials. This gap between documentation and reality causes countless production incidents.
Implementing Healthchecks for Spring Boot Dependencies
Healthchecks are the only reliable way to ensure service readiness in Docker Compose. For each dependency your Spring Boot app needs, define a healthcheck that verifies the service is actually accepting connections. For MySQL, use mysqladmin ping — it's lightweight and built into the official image. For Redis, use redis-cli ping. For RabbitMQ, use rabbitmq-diagnostics check_port_connectivity or a custom curl to the management API. The key is to make the healthcheck specific to the service's readiness criteria. MySQL's healthcheck should verify the server is accepting TCP connections, not just that the process is running. Redis's healthcheck should ensure it's ready to accept commands after loading the dataset. For custom services like a Spring Boot-based dependency, expose a Spring Boot Actuator health endpoint and curl it. The healthcheck configuration includes test (the command), interval (how often to check), timeout (max time for check), retries (failures before marking unhealthy), and start_period (grace period during startup). Without start_period, the healthcheck might fail during normal initialization and kill the container prematurely.
Wait-for-it Scripts: The Legacy Approach
Before Docker Compose supported healthchecks natively, the standard solution was a wait-for-it.sh script. This shell script polls a TCP port until it's open, then executes the main command. While this approach works, it has significant drawbacks compared to native healthchecks. First, it only checks if the port is open, not if the service is ready. A MySQL port can be open before the server is fully initialized. Second, it adds complexity to your Dockerfile and entrypoint. Third, it doesn't integrate with Docker's orchestration features. However, wait-for-it scripts are still useful in scenarios where you can't modify the docker-compose.yml (e.g., in some CI/CD pipelines) or when using older Docker versions that don't support condition: service_healthy. The script works by looping and sleeping until the specified host and port accept a TCP connection. For Spring Boot, you'd wrap the java -jar command with wait-for-it. The script is available on GitHub and has been battle-tested for years. I've used it in production environments where Docker Compose wasn't available and we relied on raw docker run commands. The key is to set a timeout to avoid infinite loops.
Spring Boot Retry and Connection Pool Configuration
Even with proper healthchecks, you should configure Spring Boot to handle transient connection failures gracefully. The HikariCP connection pool, which Spring Boot uses by default, has several settings that can prevent catastrophic failures. The most important is spring.datasource.hikari.initialization-fail-timeout. By default, HikariCP fails fast if it can't establish an initial connection. Setting this to -1 makes it wait indefinitely, which is useful when combined with a healthcheck. However, a better approach is to set a reasonable timeout and let Spring Boot retry. For this, use spring.datasource.hikari.connection-timeout (default 30 seconds) and spring.datasource.hikari.max-lifetime. Additionally, Spring Boot 3.2+ supports spring.datasource.hikari.initialization-fail-timeout=-1 combined with spring.jpa.properties.hibernate.boot.allow_jdbc_metadata_access=false to defer database connection until first request. For JPA applications, you can disable schema validation at startup with spring.jpa.hibernate.ddl-auto=none and use Flyway or Liquibase for migrations, which have their own retry logic. The combination of healthchecks and Spring Boot retry configuration creates a robust startup sequence.
Advanced Orchestration: Beyond Docker Compose
While healthchecks solve the basic problem, complex microservice architectures require more sophisticated orchestration. Docker Compose is fine for development and small deployments, but for production with multiple Spring Boot services, you should consider Kubernetes or Docker Swarm. Kubernetes has built-in liveness and readiness probes that serve the same purpose as healthchecks but with more granularity. Liveness probes check if the container should be restarted (e.g., deadlock detection), while readiness probes check if the service can accept traffic. For Spring Boot, you can expose the Actuator health endpoint as both probes. Kubernetes also supports init containers, which are containers that run to completion before the main container starts. You can use an init container to wait for dependencies using the same wait-for-it pattern. Docker Swarm has similar features with healthcheck policies and restart conditions. The key insight is that container orchestration is not just about startup order — it's about ongoing health monitoring. A database might be healthy at startup but crash later. Healthchecks and probes ensure the orchestrator can respond to failures.
Testing Your Container Startup Sequence
Many teams only test their container startup in ideal conditions — local development with all services running on the same machine. This misses the race conditions that occur in production. To properly test your startup sequence, simulate realistic scenarios: delayed database startup, database crash during initialization, network latency between containers, and resource constraints. Use Docker Compose profiles or testcontainers for integration tests. Testcontainers is a Java library that provides disposable containers for testing, and it supports waiting strategies that mimic healthchecks. For example, you can use a WaitStrategy that waits for a specific log message or a successful HTTP response. In your CI/CD pipeline, include a test that starts all services with delayed dependencies and verifies the Spring Boot app eventually becomes healthy. Use docker-compose with healthchecks and validate the startup order with docker ps --filter. Also test failure scenarios: stop a dependency container and verify the Spring Boot app handles it gracefully (retry, circuit breaker, etc.).
Production Checklist: Container Startup Best Practices
Based on years of production experience, here's a checklist for ensuring reliable Spring Boot container startup. First, always define healthchecks for stateful services (databases, caches, message brokers). Use condition: service_healthy in depends_on. Second, configure Spring Boot to defer database connections until first request using initialization-fail-timeout=-1. Third, use Flyway or Liquibase for schema migrations with retry logic. Fourth, implement liveness and readiness probes if using Kubernetes. Fifth, add retry logic in your application for transient failures using Spring Retry or resilience4j. Sixth, test your startup sequence with Testcontainers, including failure scenarios. Seventh, set resource limits (CPU, memory) in Docker Compose to prevent resource contention during startup. Eighth, use Docker's restart policy to handle crashes gracefully. Ninth, log startup sequence with timestamps to debug issues. Tenth, monitor container startup time in production and alert if it exceeds thresholds. This checklist has been battle-tested across hundreds of production deployments and prevents the most common startup failures.
The $50k Payment Gateway Outage
- Never trust depends_on for production services
- Always implement healthchecks for stateful services
- Add retry logic in Spring Boot for database connections
- Use different startup configurations for dev vs production
docker compose psdocker compose logs mysql | tail -20| File | Command / Code | Purpose |
|---|---|---|
| docker-compose-naive.yml | version: "3.8" | The Problem |
| docker-compose-healthcheck.yml | version: "3.8" | What the Official Docs Won't Tell You |
| docker-compose-full-healthchecks.yml | version: "3.8" | Implementing Healthchecks for Spring Boot Dependencies |
| Dockerfile-with-wait-for-it | FROM eclipse-temurin:17-jre-alpine | Wait-for-it Scripts |
| application-retry.yml | spring: | Spring Boot Retry and Connection Pool Configuration |
| kubernetes-deployment.yaml | apiVersion: apps/v1 | Advanced Orchestration |
| StartupTest.java | @Testcontainers | Testing Your Container Startup Sequence |
| docker-compose-production.yml | version: "3.8" | Production Checklist |
Key takeaways
Interview Questions on This Topic
Explain why Docker's depends_on is insufficient for Spring Boot applications in production.
Frequently Asked Questions
20+ years shipping production Java in banking & fintech. Everything here is grounded in real deployments.
That's Spring Boot. Mark it forged?
5 min read · try the examples if you haven't