Learning Goals
3 min- Prepare a Flask app for production — gunicorn, requirements.txt, env vars.
- Push to GitHub.
- Deploy to one free host (Render, Fly.io, or PythonAnywhere).
- Debug the inevitable "500 in prod but works locally".
Warm-Up · Pre-Flight Checklist
5 min- ✅
requirements.txt— every pip dependency pinned. - ✅
app.run(debug=True)only insideif __name__ == "__main__". - ✅
SECRET_KEY, DB path read from environment. - ✅ A WSGI server (
gunicorn) instead of the dev server. - ✅ Static files served by Flask is fine for small apps — large traffic moves to a CDN.
"Deployment" is just: same code, different config, different process supervisor. Once you've done it once, every future deployment is the same shape.
New Concept · gunicorn, env, host
14 minrequirements.txt
pip freeze > requirements.txt # typical contents: Flask==3.0.3 gunicorn==22.0.0 bcrypt==4.2.0
gunicorn
Flask's built-in dev server is single-threaded and warns it's not for production. gunicorn is a real WSGI server:
pip install gunicorn # Locally test it gunicorn -w 2 -b 0.0.0.0:8000 app:app # -w 2 = 2 worker processes # app:app = filename:Flask-instance
Pick env vars carefully
import os app.config["SECRET_KEY"] = os.environ["FLASK_SECRET"] DB_PATH = os.environ.get("DB_PATH", "blog.db")
Three friendly hosts
Render (render.com) - Free tier with cold starts - "Web Service" → connect GitHub → Python → start: gunicorn app:app - Set env vars in dashboard - Persistent disk (paid) for SQLite Fly.io (fly.io) - Free allowance; CLI-driven (fly launch + fly deploy) - flyctl detects Flask, writes a Dockerfile - Volume mount for SQLite persistence PythonAnywhere (pythonanywhere.com) - Easiest for absolute beginners; web UI, no Docker - "Web app" → Flask → point to wsgi.py - SQLite lives on their disk by default
The deployment shape
1. push code to GitHub 2. create new service on host 3. point host at your repo + branch 4. set env vars (FLASK_SECRET at minimum) 5. set start command (gunicorn app:app) 6. wait 60 s for first build 7. visit the URL 8. read logs when something is wrong
Walk-Through · Deploy the Blog to Render
12 min- Push to GitHub.
git init git add app.py blog_db.py templates/ static/ requirements.txt git commit -m "blog v1" git branch -M main gh repo create my-flask-blog --public --source=. --push # or use the web UI
- Sign in to render.com → New → Web Service → Connect repo.
- Settings:
Environment : Python 3 Build command : pip install -r requirements.txt Start command : gunicorn app:app Instance type : Free Environment variables FLASK_SECRET = <paste a 64-hex secret>
- Click Deploy. Watch the build log. Two minutes later you have a URL like
https://my-flask-blog.onrender.com. - Visit the URL on your phone 🎉
Common 500-in-prod culprits
- FLASK_SECRET not set →
KeyErroron import. - SQLite file lost after each deploy on ephemeral disks. Use a volume (paid on Render) or migrate to Postgres.
- Static URLs hardcoded as
/static/...work, buturl_for("static", ...)is safer. - Logs are gold — every host shows them; read them first.
Deploy Yours
13 minDeploy your blog to one of the three hosts. Tweet / DM the URL to a friend.
If you own a domain, point a subdomain (e.g., blog.yourname.com) at your deployment. Each host has a one-page guide.
Hook a webhook so every push to main triggers a redeploy. (Most hosts do this by default; just confirm.)
Mini-Challenge · From SQLite to Postgres
8 minSQLite files don't survive on free ephemeral disks. Swap to Postgres on the host (Render & Fly both offer one). Change sqlite3.connect to psycopg.connect behind a tiny get_conn wrapper. The rest of your code shouldn't need to change much — most SQL is portable.
Recap
3 minProduction = gunicorn + env vars + a host. Push, configure, deploy, read logs. Once. Future apps follow the same shape. Tomorrow we step back into the "why" — data ethics.
Homework
4 minGet your blog live. Send the URL to your teacher / classmate. Add a screenshot of the live site to your portfolio README.