58.9 Deploying Flask: Gunicorn and Nginx
Alright, let’s get your beautiful Flask app out of your development playground and onto a real server where people can actually use it. We’re going to move from Flask’s built-in development server (the one you run with flask run), which is about as production-ready as a paper mache helmet, to a proper, robust setup. The industry-standard duo for this job is Gunicorn as the application server and Nginx as the reverse proxy. Think of it like this: Gunicorn is your specialist factory floor, efficiently churning out your app’s responses, and Nginx is the front office, handling traffic, serving static files, and providing a security buffer.
Why This Two-Tiered Approach?
You might be wondering why we need two pieces of software. Can’t Gunicorn do it all? Technically, yes, but you really, really don’t want it to. Gunicorn is brilliant at managing Python worker processes to run your application code. However, it’s not optimized for serving static files (like your CSS, JavaScript, and images) at high speed, and having it directly face the internet is asking for trouble. Nginx, on the other hand, is a beast at these exact tasks. It handles thousands of simultaneous connections with minimal resource usage, serves static files directly without bothering your Python workers, and acts as a crucial defensive layer, mitigating bad requests and attacks before they ever reach Gunicorn. This separation of concerns is what makes this setup so robust and scalable.
Getting Your App Ready for Production
First, let’s tidy up your project. You need a clear way to tell Gunicorn, “Here, run this.” The standard way is to ensure your application is defined in a way that’s importable. The simplest structure often involves a setup.py or at the very least, a wsgi.py file.
Let’s say your main application object is created in myapp/__init__.py. You might have a structure like this:
myproject/
├── myapp/
│ ├── __init__.py
│ ├── models.py
│ └── routes.py
├── venv/
├── setup.py
└── wsgi.py
The key file here is wsgi.py. It’s dead simple. Its only job is to provide the “application” object that Gunicorn will use.
# wsgi.py
from myapp import create_app
application = create_app()
if __name__ == "__main__":
application.run()
Note the variable name application instead of app. It’s the conventional name for the WSGI callable, though Gunicorn is smart enough to find app too. Let’s just be explicit.
Installing and Taming Gunicorn
Install Gunicorn in your virtual environment. Do not install it globally. Always, always use your project’s virtual environment.
pip install gunicorn
Now, you can test it directly from your project directory:
gunicorn -w 4 -b 0.0.0.0:8000 wsgi:application
Let’s break that down:
-w 4: Spawn 4 worker processes. A good starting rule of thumb is(2 * num_cores) + 1. Don’t just set this to 100; you’ll drown your server in context-switching. Start with 4 and adjust based on load.-b 0.0.0.0:8000: Bind to all network interfaces on port 8000. This makes it accessible to the outside world (or at least to Nginx on the same machine).wsgi:application: This is the formatmodule_name:variable_name. It tells Gunicorn to import theapplicationobject from yourwsgi.pyfile.
Running this command should make your app available on port 8000. Hit it with your browser or curl to confirm it’s working. This is a great smoke test.
Making Gunicorn a Proper Service: Systemd
Running Gunicorn from the command line is for amateurs. We use systemd. It will manage the process, ensure it starts on boot, restart it if it crashes, and collect logs. Create a service file:
sudo nano /etc/systemd/system/myflaskapp.service
Now, paste in the following configuration. This is where you need to pay attention to paths and usernames. Get this wrong, and it will fail silently, which is always a joy.
# /etc/systemd/system/myflaskapp.service
[Unit]
Description=Gunicorn instance to serve my Flask app
After=network.target
[Service]
User=www-data
Group=www-data
WorkingDirectory=/home/username/myproject
Environment="PATH=/home/username/myproject/venv/bin"
ExecStart=/home/username/myproject/venv/bin/gunicorn -w 4 --bind unix:myproject.sock -m 007 wsgi:application
[Install]
WantedBy=multi-user.target
Crucial details:
- User/Group: We’re using
www-data, the standard web user, for security. Your app should not run as root. - WorkingDirectory: This is the full path to your project root.
- Environment=“PATH”: This points to your virtual environment’s
bindirectory, so systemd can find thegunicornexecutable. - ExecStart: Notice we changed the bind to a Unix socket (
myproject.sock) instead of a network port. This is more efficient and secure for communication with Nginx on the same machine. The-m 007sets the socket file permissions.
Now, enable and start the service:
sudo systemctl daemon-reload
sudo systemctl start myflaskapp
sudo systemctl enable myflaskapp
Check its status with sudo systemctl status myflaskapp to see if it’s running happily. If it’s failed, use journalctl -u myflaskapp to see the logs and figure out what you typo’d.
Configuring Nginx as the Reverse Proxy
Now, we tell Nginx to take incoming web traffic and proxy it to the Gunicorn socket we just created. We do this with a server block configuration.
sudo nano /etc/nginx/sites-available/myproject
Paste in this configuration. Again, mind your paths and domain names.
# /etc/nginx/sites-available/myproject
server {
listen 80;
server_name your_domain.com www.your_domain.com;
location / {
include proxy_params;
proxy_pass http://unix:/home/username/myproject/myproject.sock;
}
location /static {
alias /home/username/myproject/myapp/static;
expires 30d;
}
}
The magic here is in the two location blocks:
location /: For all requests, Nginx will forward them to the Gunicorn socket using the standardproxy_params(which set important headers likeHostandX-Real-IP).location /static: This is the performance win. Nginx will bypass Gunicorn entirely for any file in the/staticURL path and serve them directly from the filesystem. Theexpiresdirective tells browsers to cache these files for 30 days.
Enable this configuration by linking it to the sites-enabled directory and then test your Nginx config (Nginx is famously pedantic about syntax errors).
sudo ln -s /etc/nginx/sites-available/myproject /etc/nginx/sites-enabled/
sudo nginx -t
If the test is successful, restart Nginx:
sudo systemctl restart nginx
You should now be able to hit your server’s IP address or domain name and see your Flask app, served securely and efficiently through the Gunicorn and Nginx tag team. The first time it works, it’s pure magic. And when it doesn’t, your best friends are sudo journalctl -u myflaskapp and sudo tail -f /var/log/nginx/error.log. Welcome to the trenches.