CalcSnippets Search
Server 3 min read

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_healthy

That 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: 5

If 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 --build

Check health:

docker compose ps
docker compose logs -f api
docker compose logs -f db

Test the API:

curl http://localhost:8000/health

The 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.

Sources

Keep reading

Related guides