Deployment
Every earlier lesson ran Bookshelf, conceptually, with flask run or
app.run(debug=True) — perfect for development, and completely wrong for serving real
traffic. This lesson covers what actually changes between "runs on my machine" and "runs in
production," in a way that applies no matter which hosting platform Bookshelf eventually lands on.
Why the Dev Server Isn't for Production
Flask's built-in development server exists purely to make local development convenient — live reloading, a helpful debugger page on errors, no separate process to configure. Flask itself is explicit that it was never meant to run production traffic: starting it prints its own warning to that effect. Concretely, it's single-threaded by default (one request is handled at a time), and it isn't hardened against the kind of malformed or hostile requests a server exposed to the public internet inevitably receives.
A Real WSGI Server: Gunicorn
Production Flask apps run behind a proper WSGI application server instead — software built specifically to run a WSGI app (which is exactly what a Flask app is) reliably, handling many requests concurrently. Gunicorn is the most common choice for Flask on Linux-based hosting.
pip install gunicorn
The Production Request Path
A typical production setup adds one more piece in front of Gunicorn: a reverse proxy like Nginx. Understanding the shape of this, conceptually, matters more than any single command:
Nginx sits at the front, receiving every incoming request first. For most requests, it simply forwards them along to Gunicorn, which runs Bookshelf's actual Flask code. But Nginx also takes on one job directly, without ever involving Flask at all: serving static files — exactly the CSS, JavaScript, and images covered back in Static Files & Assets. A web server built for exactly this is dramatically faster at serving a plain file than routing that same request through Gunicorn and Flask just to hand back a CSS file unchanged.
The Pre-Deployment Checklist
Regardless of where Bookshelf ultimately gets hosted, this same checklist applies every time:
| Item | Why it matters |
|---|---|
debug=False | Flask's debugger, if left on, can expose source code and even allow arbitrary code execution to anyone who can trigger an error page — see Security Best Practices |
A real SECRET_KEY from the environment | Never a hardcoded string committed to source control — sessions and CSRF tokens depend on it staying secret |
FLASK_ENV=production | Ensures Flask and its extensions behave with production-appropriate defaults rather than development ones |
| Migrations applied | flask db upgrade run against the production database before traffic hits it — see Migrations with Flask-Migrate |
Running with Gunicorn
Assuming Bookshelf uses the app factory pattern, Gunicorn is pointed at the factory call itself:
gunicorn -w 4 'app:create_app()'
-w 4 tells Gunicorn to start 4 separate worker processes, each capable of handling
requests independently and in parallel — a rough starting point is commonly
(2 × CPU cores) + 1, tuned from there based on actual load. This is also exactly
why a shared cache backend like Redis matters, as covered in Caching:
each of these 4 workers is a completely separate process with its own memory.
Where Flask Apps Actually Get Hosted
There's no single correct hosting platform — these are the three broad, legitimate paths people actually take, each with real tradeoffs:
| Approach | Tradeoff |
|---|---|
| A self-managed VPS/cloud server | Full control over Gunicorn, Nginx, and every setting — but you own patching, monitoring, and every piece of the setup yourself |
| A PaaS (Railway/Render/Heroku-style platforms) | Handles the server, the process management, and often TLS certificates for you — less control, but dramatically less to set up and maintain |
| Containerizing with Docker | An increasingly standard middle ground — package the app once, run the same image consistently across a VPS, a PaaS, or a container-orchestration platform |
All three are legitimate, widely used choices for a real Flask app — which one fits best depends on how much operational control is worth taking on versus handing off, not on any of them being objectively "more correct" than the others.