How to Deploy a FastAPI Application from GitHub
A production FastAPI release needs a pinned Python version, repeatable dependency install, an explicit ASGI start command, environment values, and a lightweight health route.
A FastAPI deployment fails surprisingly often for reasons that have nothing to do with FastAPI: the process listens only on localhost, the Python version drifts, a secret is compiled into the repository, or a database migration runs from every replica.
This guide builds a small deployment contract that works on any capable application platform.
Start with a boring, explicit application
Use an import path that another engineer can discover. If the application lives at app/main.py, a minimal entry point looks like this:
from fastapi import FastAPI
app = FastAPI()
@app.get("/healthz", include_in_schema=False)
async def healthz() -> dict[str, str]:
return {"status": "ok"}
The production command can then be expressed without guesswork:
uvicorn app.main:app --host 0.0.0.0 --port $PORT
0.0.0.0 matters inside a hosted runtime: binding to 127.0.0.1 accepts traffic only from the same container or machine. The platform-provided port matters because the router needs to know where the process listens.
Make the repository reproducible
Commit the dependency declaration and lock file used by your package manager. Pin the Python major and minor version through the mechanism your platform supports. A local environment silently using a newer interpreter can produce a build that cannot start in production.
A useful repository shape is small:
app/
__init__.py
main.py
tests/
test_health.py
pyproject.toml
uv.lock
README.md
The README should contain the local start command, production start command, required environment-variable names, and test command. Do not put secret values in it.
Connect GitHub with the narrowest access available
Select the repository and production branch intentionally. Automatic deployment is convenient when the branch is protected by review and tests; it is reckless when anyone can push directly.
For pull requests, use an isolated preview environment only if the application can safely connect to preview data. A preview URL pointing at the production database is not isolated. Prefer separate credentials and a disposable database or schema.
Separate build, release, and runtime work
These are different phases:
- Build: install locked dependencies and create an immutable application artifact.
- Release: run a one-off schema migration when the artifact is ready.
- Runtime: start the API process and serve requests.
Do not place alembic upgrade head in the command executed by every web replica. Two replicas can race on the same migration, and a slow migration can prevent healthy application processes from starting. Use a single release job or a deliberately controlled migration step.
Database changes also need backward compatibility during a rolling release. Add a nullable column before requiring it, deploy code that can handle both states, backfill separately, then enforce the constraint in a later release.
Put configuration outside the repository
Store database URLs, signing keys, mail credentials, and third-party tokens as protected runtime values. Expose only variable names in documentation and .env.example.
Validate required settings at startup. A service that starts with an empty signing key is worse than one that refuses to boot. Be careful with logging: configuration validation errors should name the missing variable, not print the secret-bearing configuration object.
Health checks should answer a narrow question
/healthz should be fast and prove that the process can answer requests. A separate readiness check may verify critical initialization before the router sends traffic. Do not call every external dependency from a frequent liveness check; a mail-provider outage should not cause all API processes to restart.
Test shutdown as well as startup. When the platform sends a termination signal, the process needs enough time to stop accepting work and finish or cancel in-flight requests. Long work belongs in a background worker with durable job state, not an unbounded HTTP request.
Files, databases, and background work
Assume the application filesystem is replaceable unless your service explicitly includes persistent storage. Upload user files to object storage and record ownership in the database. A file written beside the source code may vanish during a deploy or exist on only one replica.
Use a connection pool sized across all replicas. Four application processes each configured for 20 connections can overwhelm a small database even when request traffic is low.
For emails, exports, or model jobs that can outlive a request, record the requested operation before publishing a small job message. Give the worker an idempotency key so a retry cannot send the same customer email twice.
A release drill before real traffic
- Deploy a known version and call
/healthzthrough the public URL. - Confirm a real authenticated request reaches the database.
- Trigger one expected application error and find it in the logs using a request identifier.
- Deploy a deliberately unhealthy version and confirm traffic stays on or returns to the last healthy release.
- Restore a recent database backup to a separate destination and run a representative query.
- Rotate a non-critical credential and verify that the running service picks up the replacement through the documented restart path.
What “done” looks like
The GitHub button is not the deployment. You are done when a reviewed commit creates a repeatable artifact, configuration remains private, one controlled step handles migrations, health determines traffic, failures are visible, and both application and data have tested recovery paths. That contract lets the platform automate the routine parts without hiding what your team still owns.
Frequently asked questions
What command should start a FastAPI application?
A common command is uvicorn app.main:app --host 0.0.0.0 --port $PORT, adjusted for the application's import path.
Should database migrations run in the web start command?
No. Run migrations as one controlled release task so replicas do not race and migration failure does not repeatedly restart the web process.
Can FastAPI uploads be stored on the application filesystem?
Treat the runtime filesystem as replaceable unless persistent storage is explicitly attached. Durable uploads should normally use object storage.