The build logs show green. The container image is pushed. The deployment status changes to "Running." Then the health checks fail, the pod never becomes ready, and your app returns 502 errors. The container is running — you can see it in the dashboard — but the ingress can't reach it.
This is the number one FastAPI deployment failure on any containerised platform. The fix is a two-character change in your Uvicorn startup command, but it's not obvious if you've only ever run uvicorn main:app --reload on localhost.
The problem: binding to localhost
By default, Uvicorn binds to 127.0.0.1. This works on your local machine because your browser and the FastAPI server are on the same network interface. Inside a container, 127.0.0.1 is unreachable from outside the container — the ingress controller (Kong, Nginx, Traefik) lives on a different network namespace and tries to connect via the pod's IP, not localhost.
When the health check runs, it sends a request to http://. If Uvicorn is bound to 127.0.0.1, the connection is refused, the health check fails, Kubernetes restarts the container, and the cycle repeats.
The fix: bind to 0.0.0.0
Change your Uvicorn start command from:
uvicorn main:app --host 127.0.0.1 --port 8000to:
uvicorn main:app --host 0.0.0.0 --port 8000Or set the PORT environment variable and let Uvicorn bind to 0.0.0.0 automatically:
uvicorn main:app --host 0.0.0.0 --port ${PORT:-8000}Now the server listens on all network interfaces. The ingress controller can reach the pod, health checks succeed, and your app becomes ready.
Adding a health check endpoint
Even with the correct bind address, health checks fail if the endpoint doesn't exist or takes too long to respond. Kubernetes sends a GET request to a configured path (default /health) and expects a 200 response within a few seconds. If the response is slow (e.g. the endpoint queries a database), the check times out and the pod is marked unhealthy.
Add a lightweight health check to your FastAPI app:
from fastapi import FastAPI
app = FastAPI()
@app.get("/health")
def health_check():
return {"status": "ok"}This returns instantly with no database queries, no external API calls, no blocking operations. The ingress hits this endpoint every few seconds; if it returns 200, the pod stays healthy.
Deploying with the API
Here's the full deployment payload for a FastAPI app with a health check:
curl -X POST https://api.pandastack.io/v1/projects \
-H "Authorization: Bearer $PANDASTACK_TOKEN" \
-H "Content-Type: application/json" \
-d '{
"slug": "container",
"name": "fastapi-app",
"repositoryName": "yourorg/fastapi-app",
"branch": "main",
"autoDeploy": true,
"startCommand": "uvicorn main:app --host 0.0.0.0 --port 8000",
"healthCheckPath": "/health",
"env": [
{ "name": "PORT", "value": "8000" }
]
}'The healthCheckPath field tells Kubernetes where to send health probes. If your app uses a different path (e.g. /api/health or /ping), update it here.
Using pandastack.json instead
For a repository that others might deploy (a template, a starter, an open-source tool), commit the configuration:
{
"type": "container",
"name": "fastapi-app",
"language": "python",
"startCommand": "uvicorn main:app --host 0.0.0.0 --port 8000",
"healthCheckPath": "/health",
"env": [
{ "key": "PORT", "value": "8000" },
{
"key": "DATABASE_URL",
"description": "PostgreSQL connection string"
}
]
}Now a deploy button works out of the box:
[](https://dashboard.pandastack.io/deploy?repo=yourorg/fastapi-app)The platform reads pandastack.json, pre-fills the start command and health check path, and prompts for DATABASE_URL.
Debugging slow health checks
If the health check endpoint exists but the pod still cycles, the endpoint is probably too slow. Common causes:
- 1Database queries in the health check. If
/healthrunsSELECT 1against PostgreSQL, and the database is unreachable or slow, the check times out. Move database checks to a separate/readinessendpoint and keep/healthinstant.
- 1Waiting for external dependencies. Don't ping third-party APIs from the health check. Kubernetes doesn't care if Stripe is down — it cares if your app is running.
- 1Slow imports. If
main:appimports a heavy library (ML model, image processing) at module load, the health check times out before the app finishes starting. Lazy-load expensive dependencies inside route handlers, not at the top level.
A good health check returns in under 100ms. If it takes longer, factor out the expensive parts.
Separating liveness and readiness probes
Kubernetes supports two probe types:
- Liveness probe: checks if the app is running. If this fails, Kubernetes restarts the container.
- Readiness probe: checks if the app is ready to serve traffic. If this fails, the pod is removed from the load balancer but not restarted.
For most apps, a single /health endpoint works for both. For apps with slow startup (database migrations, cache warming), use two endpoints:
@app.get("/health")
def liveness():
# Always returns 200 if the process is alive
return {"status": "ok"}
@app.get("/ready")
async def readiness():
# Check database connectivity
try:
await db.execute("SELECT 1")
return {"status": "ready"}
except Exception:
return {"status": "not ready"}, 503Configure the probes in pandastack.json (not currently supported — defaults to a single health check) or in Kubernetes YAML if you're deploying with kubectl instead of the platform.
What happens if you don't set a health check
The platform uses a default health check path (/) or falls back to TCP port checks (the port is open → the app is healthy). For FastAPI, the root path usually returns 404 (no route defined), so the check fails. Always set healthCheckPath explicitly.
Using a Dockerfile instead of a start command
If your app uses a Dockerfile, the health check and port binding still matter. Example:
FROM python:3.11-slim
WORKDIR /app
COPY requirements.txt .
RUN pip install --no-cache-dir -r requirements.txt
COPY . .
EXPOSE 8000
CMD ["uvicorn", "main:app", "--host", "0.0.0.0", "--port", "8000"]The --host 0.0.0.0 is still required. EXPOSE 8000 documents the port but doesn't bind Uvicorn — the CMD does.
Deploy with:
curl -X POST https://api.pandastack.io/v1/projects \
-H "Authorization: Bearer $PANDASTACK_TOKEN" \
-H "Content-Type: application/json" \
-d '{
"slug": "container",
"name": "fastapi-docker",
"repositoryName": "yourorg/fastapi-docker",
"branch": "main",
"autoDeploy": true,
"language": "docker",
"dockerfilePath": "Dockerfile",
"healthCheckPath": "/health"
}'The build system detects the Dockerfile, runs docker build, pushes the image to Google Artifact Registry, and deploys it via Helm. The health check still runs against /health.
The CLI workflow
For local testing before deploying:
panda login
panda projects create \
--repo yourorg/fastapi-app \
--branch main \
--type container \
--start-command "uvicorn main:app --host 0.0.0.0 --port 8000"The CLI reads pandastack.json if it exists, or you can pass parameters directly. After the first deploy, redeploy with:
panda projects deploy <project-id>Watch logs (note: CLI log streaming is currently unreliable — use the dashboard Logs tab for real-time output):
panda projects logs <project-id>Summary: the two-line checklist
- 1Bind to
0.0.0.0, not127.0.0.1. - 2Add a
/healthendpoint that returns 200 in under 100ms.
If both are true, your FastAPI app deploys cleanly. If either is missing, you get the "Running but unreachable" failure mode.
The platform provisions SSL certificates automatically, routes traffic through Kong ingress, and handles Helm chart generation. You write the FastAPI app and the health check; the platform does the rest.
PandaStack's container builds use rootless BuildKit in ephemeral Kubernetes pods (no host Docker socket), push images to Google Artifact Registry, and deploy via Helm with gVisor sandboxing on free-tier apps for extra isolation. Free-tier apps scale to zero after inactivity (sub-second cold start when traffic returns). Paid tiers run on stable nodes with no scale-to-zero.
References
- [FastAPI deployment guide](https://fastapi.tiangolo.com/deployment/docker/)
- [Uvicorn settings](https://www.uvicorn.org/settings/)
- [Kubernetes liveness and readiness probes](https://kubernetes.io/docs/tasks/configure-pod-container/configure-liveness-readiness-startup-probes/)
- [PandaStack container projects](https://docs.pandastack.io/projects/containers/)