Deploy a Flask App to Production
Learn how to deploy a Flask app to production step by step. This tutorial covers core concepts, hands-on exercises, troubleshooting, and what to study next.
Focus: deploy a flask app to production
You’ve built a Flask app that works perfectly on your laptop — but the moment you share the URL, it falls over, or worse, exposes dangerous debug errors to the world. Running flask run is for development only; production requires a fundamentally different setup. This lesson shows you exactly how to deploy a Flask app to production — from choosing a server and WSGI middleware to managing environment variables and surviving restarts — so your app stays fast, secure, and available 24/7.
The problem this lesson solves
When you run flask run in development, Flask’s built-in server is single-threaded, unauthenticated, and unforgiving. It crashes on the first unexpected error. It serves static files inefficiently. It even shows your source code in tracebacks. That’s fine for local testing, but in production you need:
- Concurrency — handle multiple users at once without blocking.
- Security — hide debug info, force HTTPS, validate requests.
- Stability — survive restarts, memory leaks, and traffic spikes.
- Performance — serve static files quickly and cache responses.
If you ignore these, your app will be slow, insecure, and prone to downtime. This tutorial solves that problem by walking you through a production-grade deployment using Gunicorn (the WSGI server) behind Nginx (the reverse proxy), with environment variables managed safely.
Core concept / mental model
Think of a production web stack as a concert venue:
- Flask is the performer — it has the talent (logic) but can’t handle the crowd alone.
- WSGI server (Gunicorn) is the stage crew — it runs your Flask app, manages multiple workers, and accepts incoming requests.
- Reverse proxy (Nginx) is the security and traffic director at the entrance — it handles static files, load balancing, and blocks malicious requests before they reach the stage.
- Environment variables are the backstage passes — they give the app secrets (API keys, database URLs) without exposing them to the public.
In this model, the WSGI server is mandatory — Flask’s built-in server is not designed for production. Gunicorn is the industry-standard choice for Python web apps. Nginx sits in front to serve static files and handle brute-force protection.
Definitions
- WSGI (Web Server Gateway Interface): A Python standard that connects web servers to web applications. Gunicorn implements WSGI to run Flask.
- Reverse proxy: A server that forwards client requests to backend servers. It offloads tasks like SSL termination and caching.
- Environment variable: A key-value pair stored outside your code, used for configuration.
How it works step by step
Deploying a Flask app to production follows a logical sequence. Here’s the high-level flow:
- Prepare your app — separate configuration from code, use environment variables, and ensure your app has a
create_app()factory or a module-levelappinstance. - Install Gunicorn — add it to your
requirements.txtor install it in the production environment. - Run Gunicorn — start it with multiple workers to handle concurrency.
- Set up Nginx — configure it as a reverse proxy to forward requests to Gunicorn and serve static files.
- Manage environment variables — use a
.envfile or your hosting provider’s dashboard to store secrets. - Test and monitor — restart Gunicorn on code changes and watch logs.
Each step is cause → effect: if you skip Nginx, static files become a bottleneck; if you skip environment variables, secrets leak into your repository.
Hands-on walkthrough
Let’s deploy a simple Flask app step by step. First, create a minimal app:
# app.py
from flask import Flask
import os
app = Flask(__name__)
@app.route("/")
def home():
return "Hello, production world!"
if __name__ == "__main__":
port = int(os.environ.get("PORT", 5000))
app.run(host="0.0.0.0", port=port)
Now install Gunicorn and run it:
pip install gunicorn
gunicorn --workers 3 --bind 0.0.0.0:8000 app:app
This starts three worker processes, each processing requests concurrently. The --bind tells Gunicorn to listen on port 8000.
Next, configure Nginx to act as a reverse proxy. Create /etc/nginx/sites-available/flask_app:
server {
listen 80;
server_name yourdomain.com;
location /static {
alias /path/to/your/static/files;
}
location / {
proxy_pass http://127.0.0.1:8000;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
}
}
Enable the site and restart Nginx:
sudo ln -s /etc/nginx/sites-available/flask_app /etc/nginx/sites-enabled/
sudo systemctl restart nginx
Now your app is live — requests hit Nginx first, which serves static files directly and forwards dynamic requests to Gunicorn.
Managing environment variables
Never hard-code secrets. Use a .env file (with python-dotenv) or your hosting provider’s dashboard:
# .env
FLASK_ENV=production
SECRET_KEY=your-strong-secret
DATABASE_URL=postgresql://user:pass@localhost/db
In your code, load them:
import os
from dotenv import load_dotenv
load_dotenv()
app = Flask(__name__)
app.config["SECRET_KEY"] = os.environ.get("SECRET_KEY")
Run Gunicorn with dotenv loaded:
gunicorn --workers 3 --bind 0.0.0.0:8000 app:app
The app now reads secrets from the environment, not from source code.
Compare options / when to choose what
You have several production deployment options. Here’s a comparison:
| Option | Pros | Cons | Best for |
|---|---|---|---|
| Gunicorn + Nginx | Simple, fast, widely used | Manual setup, no auto-scaling | Small to medium apps on a VPS |
| Waitress | Pure Python, cross-platform | Less performant than Gunicorn | Windows servers |
| uWSGI | Highly configurable, high performance | Steep learning curve | High-traffic apps needing fine-tuning |
| Docker + orchestration (K8s) | Scalable, reproducible | Complex infrastructure | Large apps with microservices |
| PaaS (Heroku, Railway, Render) | Zero config, auto-deploy | Cost, vendor lock-in | Startups and prototypes |
Pro tip: For most beginners, Gunicorn + Nginx gives the best balance of control and simplicity. Start there, then move to Docker when you need reproducibility.
Troubleshooting & edge cases
Even with a solid setup, things will go wrong. Here are common issues and fixes:
- 502 Bad Gateway from Nginx — Gunicorn isn’t running or is bound to a different port. Check with
ps aux | grep gunicornand confirm--bindmatchesproxy_pass. - Static files 404 — Ensure your
location /staticblock points to the correct absolute path. Useflask runto verify the path works. - Environment variable not loading — If you use
python-dotenv, make sure.envis in the same directory and that you callload_dotenv()beforeapp.config. - Gunicorn worker timeout — Long requests hit the 30-second default. Increase
--timeout 120for slow APIs. - Permission errors binding to port 80 — Nginx runs as root, but Gunicorn should bind to a higher port (e.g., 8000) and let Nginx handle 80.
Pro tip: Always run Gunicorn with
--access-logfile -to see requests in real time. It’s your first debugging tool.
What you learned & what's next
You now understand the full production stack: Flask as the web framework, Gunicorn as the WSGI server, Nginx as the reverse proxy, and environment variables for secure configuration. You can deploy a Flask app to production that is fast, secure, and stable.
Next in this track, you’ll learn about containerizing Flask apps with Docker — the natural evolution for reproducible deployments across any environment. With Docker, you’ll package your app, its dependencies, and even Nginx configuration into a single image that runs anywhere.
Practice recap
Now try it yourself: take any existing Flask app, add Gunicorn to your requirements.txt, and run it locally with gunicorn --workers 3 app:app. Then configure a simple Nginx reverse proxy on a test domain. Verify that static files load from Nginx and dynamic routes work through Gunicorn. This practice will cement the deployment flow before you tackle Docker.
Common mistakes
- Running
flask runin production — the built-in server is not designed for concurrent traffic and exposes debug info. - Hard-coding secrets like
SECRET_KEYin source code — they end up in version control and leak. - Skipping the reverse proxy — Flask serves static files inefficiently, and Nginx handles that much better.
- Setting
debug=Truein production — it enables the debugger and leaks stack traces to users. - Binding Gunicorn to port 80 directly — it requires root permissions and bypasses Nginx’s security features.
Variations
- Use Waitress if you're on Windows or prefer a pure Python WSGI server with no C extensions.
- Use Docker + Docker Compose to wrap your app, Gunicorn, and Nginx into containers for reproducible deployments.
- Use a PaaS like Render or Railway — they handle Gunicorn and Nginx behind the scenes, so you only push code.
Real-world use cases
- Deploying a REST API for a mobile app backend — using Gunicorn with multiple workers to handle high request volume.
- Hosting a personal portfolio site with a contact form — Nginx serves static files fast while Gunicorn handles the form submission.
- Running an internal dashboard for a startup — environment variables keep database credentials safe, and Nginx provides SSL termination.
Key takeaways
- Flask's built-in server is for development only — always use a WSGI server like Gunicorn in production.
- Nginx acts as a reverse proxy: it handles static files, SSL, and security, forwarding dynamic requests to Gunicorn.
- Protect secrets with environment variables — never hard-code API keys or database URLs.
- Set
debug=FalseandFLASK_ENV=productionto avoid leaking internal errors. - Choose your deployment stack based on scale: Gunicorn+Nginx for most apps, Docker for reproducibility, PaaS for zero-config.
- Troubleshoot with logs: use Gunicorn’s access log and check Nginx’s error log for 502s.
Keep learning
Related tutorials, quizzes, and articles for this topic.
Discussion
Questions, corrections, and tips help everyone reading this page.
0 comments
Add a comment
No comments yet — start the thread.