FastAPI in Docker With a Real Healthcheck So Your API Stops Failing on Startup Races
A production-minded FastAPI and Docker Compose guide that shows how to package the app, add a health endpoint, and use service health checks and startup conditions so your API does not race the database on boot.
The classic server bug: your API works fine locally, then fails in Compose because the app container starts faster than the database container. People call it “flaky Docker,” but most of the time it is just missing startup discipline.
Why depends_on is not enough by itself
Developers often assume that if the app service depends on the database service, the app will wait until the database is actually usable. That is not automatically true. “Started” is not the same thing as “ready.”
The Docker Compose docs explicitly talk about startup order and health-based dependency conditions because this problem is common.
Build the FastAPI app with a health endpoint
main.py:
from fastapi import FastAPI
app = FastAPI()
@app.get("/health")
def health():
return {"ok": True}A clean Dockerfile
FROM python:3.12-slim
WORKDIR /app
COPY requirements.txt .
RUN pip install --no-cache-dir -r requirements.txt
COPY . .
CMD ["uvicorn", "main:app", "--host", "0.0.0.0", "--port", "8000"]Simple is good here. You can optimize later.
Compose file with a real database healthcheck
services:
db:
image: postgres:16
environment:
POSTGRES_USER: app
POSTGRES_PASSWORD: app
POSTGRES_DB: app
healthcheck:
test: ["CMD-SHELL", "pg_isready -U app -d app"]
interval: 5s
timeout: 5s
retries: 10
api:
build: .
ports:
- "8000:8000"
depends_on:
db:
condition: service_healthyThat is the difference between “the database container exists” and “the database is accepting connections.”
Add an API healthcheck too
You can also let Compose verify your API:
api:
build: .
ports:
- "8000:8000"
depends_on:
db:
condition: service_healthy
healthcheck:
test: ["CMD", "curl", "-f", "http://localhost:8000/health"]
interval: 10s
timeout: 5s
retries: 5If your base image does not include curl, add it or use another check strategy.
Why this setup is worth the trouble
Without health-based startup control, teams often get:
- connection refused on boot
- migration scripts that run too early
- flaky integration tests
- “works on second restart” behavior
That kind of instability is especially poisonous because it trains people to retry blindly instead of fixing orchestration.
Common mistake: pushing retry logic everywhere
Application-level retry logic can be useful, but it should not be the only safety layer. If your orchestration can express readiness, use it. Otherwise every service grows its own startup superstition instead of benefiting from a shared platform rule.
Running the stack
Start it:
docker compose up --buildCheck health:
docker compose ps
docker compose logs -f api
docker compose logs -f dbTest the API:
curl http://localhost:8000/healthThe broader server lesson
A lot of “backend instability” is really startup coordination debt. The app may be fine. The database may be fine. The orchestration is the weak layer. Once you start using health checks and explicit readiness conditions, a big class of fake-random startup failures simply disappears.
That is why health checks are not a nice extra. They are one of the cleanest ways to make local development, CI, and production-like environments behave more honestly.