Deployment
Premier League Predictor: FastAPI & PostgreSQL
Chapter 11 · Deployment
Every earlier chapter ran with uvicorn main:app --reload — a genuinely fine development server, and a genuinely wrong choice for real production traffic.
Running With Multiple Workers
Gunicorn manages several separate Uvicorn worker processes — real request-handling capacity beyond what one process provides, with automatic restarts if a worker crashes.
fastapi-food-tracker-1 course names the exact same fact: mounting the frontend via StaticFiles on the same FastAPI app since Chapter 1 means the frontend and API have shared one origin in every environment, development included — there was never a cross-origin situation for CORS middleware to solve.
A Full-Circle Caveat on Chapter 1's Own Favorite Feature
Chapter 1 named automatic interactive docs at /docs as a genuine advantage this course's own reference-implementation choice earns for free. Before a real deployment, that feature deserves an honest second look — it lets anyone browsing it try real API calls, including writes, directly against the live database:
POST/PATCH/DELETE routes behind any login (Chapter 3's own honest warning). Disabling /docs conditionally, as above, keeps Chapter 1's own advantage intact during development while making its production exposure a deliberate choice.
PostgreSQL Connections at Scale: A Real Math Problem
Unlike the FastAPI/SQLite sibling course — which has no real connection pool at all — SQLAlchemy's create_engine() against PostgreSQL genuinely does pool connections by default. That's a real advantage this course's own stack choice earns... but it introduces a different, equally real problem once gunicorn enters the picture.
database.py's own engine = create_engine(DATABASE_URL) runs once per Python process. With -w 4, gunicorn starts four completely separate OS processes, each importing database.py independently and creating its own engine with its own connection pool — they don't share one pool between them. SQLAlchemy's real defaults are pool_size=5 plus max_overflow=10, meaning a single worker can open up to 15 real connections to PostgreSQL under load. Four workers at that default: up to 60 real connections, all from one deployed app — a meaningful fraction of PostgreSQL's own default max_connections of 100, before counting anything else (an admin tool, a monitoring agent, a second app on the same database) that might also be connecting.
A concrete, sized fix rather than an abstract warning — capping each worker's own pool explicitly:
Four workers at pool_size=5, max_overflow=2: up to 7 connections per worker, 28 total at genuine peak — comfortably under the default 100-connection ceiling, with real headroom left for anything else connecting to the same database.
os.getenv("DATABASE_URL", "postgresql://postgres:@localhost:5432/pl_predictor") fallback has no password at all — fine for a local development database nobody outside the machine can reach, a real security problem for anything actually deployed. A real, strong DATABASE_URL — with a genuine username and password, and pointed at the real production database host — must be set as an actual environment variable on the deployment host; the local fallback should never be what's actually running in production.
Schema Changes After Deployment
Chapter 2's own Base.metadata.create_all(bind=engine) creates tables that don't yet exist — it does not alter a table that already exists. Adding a new column to Fixture after this app is already deployed and running against real data would need a genuine, manual ALTER TABLE, or a real migration tool like Alembic, neither of which this course builds. That's an honest, deliberate scope boundary for a personal project at this size, not an oversight — worth knowing about before it's actually needed rather than discovering it the hard way mid-season.
TLS Termination
Gunicorn itself doesn't handle TLS certificates — a reverse proxy (nginx, the same pattern this site's own Nginx In Depth course already covers) sits in front of it, terminating HTTPS and forwarding plain HTTP internally.
A Production Checklist
- Run with
gunicorn+ Uvicorn workers, never--reload. - Decide deliberately whether
/docsshould be public, and disable it if not. - Set a real
DATABASE_URLwith genuine credentials as an environment variable — never the local, passwordless fallback. - Size
pool_size/max_overflowagainst worker count and PostgreSQL's ownmax_connections, not the unmodified defaults. - Know that a schema change after deployment needs a manual
ALTER TABLEor a real migration tool —create_allalone won't do it. - Put a reverse proxy in front for TLS termination.
Where This Course Is Headed
One chapter left: a capstone tying every route and page built across this course into one complete, working predictor.
Hands-On Exercises
Explain why this course never needed CORS configuration anywhere, tracing the reason back to a decision made in Chapter 1.
📄 View solutionExplain why four gunicorn workers running with SQLAlchemy's default pool_size/max_overflow settings can collectively open up to 60 connections to PostgreSQL, not 15, and calculate the real total after applying this chapter's own pool_size=5, max_overflow=2 fix.
📄 View solutionExplain what Base.metadata.create_all actually does and doesn't do, and describe what would genuinely need to happen to add a new column to the Fixture table after this app is already deployed and running.
📄 View solutionChapter 11 Quick Reference
- Run with: gunicorn -k uvicorn.workers.UvicornWorker -w 4, never --reload in production
- No CORS anywhere: StaticFiles has served the frontend same-origin since Chapter 1, same as the FastAPI/SQLite sibling course
- Full-circle caveat: /docs, Chapter 1's own praised feature, should be a deliberate production decision, especially given no route in this course is auth-gated
- Real connection math: 4 workers × unmodified defaults (pool_size=5 + max_overflow=10) = up to 60 real PostgreSQL connections; the same 4 workers at pool_size=5/max_overflow=2 = up to 28
- Never deploy the local fallback DATABASE_URL — it has no password
- Schema changes after deployment need a manual ALTER TABLE or a real migration tool — create_all alone can't do it
- TLS: a reverse proxy in front, same pattern as this site's own Nginx In Depth course
- Next chapter: Capstone