Ravindra BagaleCourses & study guides

Chapter 11: Application Hosting — WordPress, Flask and Express

11.2 Flask application with Gunicorn + Nginx

In my sessions, students often run python app.py on the server and think the deployment is done. Bagha — that is only the development server. Let's do it the production way.

Why Gunicorn?

flask run / app.run() starts a development server: single-threaded, not secure, not meant for production. Gunicorn ("Green Unicorn") is a production WSGI server that runs several worker processes of your app. Nginx sits in front to handle clients, static files and TLS.

 Client ─► Nginx :80 ─┬─ /static/*  → served directly from /opt/flaskdemo/static
                      └─ everything else → proxy_pass http://127.0.0.1:8000
                                              │
                                     Gunicorn master (systemd service)
                                     ├── worker 1 (Flask app)
                                     ├── worker 2
                                     └── worker 3

Step 1 — Project structure

/opt/flaskdemo
├── app.py            # Flask application
├── wsgi.py           # entry point for Gunicorn
├── requirements.txt
├── templates/
│   └── index.html
├── static/
│   └── style.css
└── venv/             # virtual environment (not committed to Git)

Step 2 — Install and create the app

# AL2023 / CentOS: sudo yum install -y python3 python3-pip git nginx
# Ubuntu:          sudo apt install -y python3 python3-venv python3-pip git nginx
sudo mkdir -p /opt/flaskdemo/{templates,static} && sudo chown -R $USER:$USER /opt/flaskdemo
cd /opt/flaskdemo
python3 -m venv venv
./venv/bin/pip install flask gunicorn
./venv/bin/pip freeze > requirements.txt

cat > app.py <<'EOF'
import socket
from datetime import datetime
from flask import Flask, render_template, jsonify

app = Flask(__name__)

@app.route("/")
def home():
    return render_template("index.html", host=socket.gethostname(), now=datetime.now())

@app.route("/api/info")
def info():
    return jsonify(app="flaskdemo", host=socket.gethostname())
EOF

cat > wsgi.py <<'EOF'
from app import app

if __name__ == "__main__":
    app.run()
EOF

cat > templates/index.html <<'EOF'
<!DOCTYPE html>
<html><head><meta charset="utf-8"><title>Flask on EC2</title>
<link rel="stylesheet" href="/static/style.css"></head>
<body><div class="card"><h1>Flask + Gunicorn + Nginx</h1>
<p>Served by host <b>{{ host }}</b> at {{ now.strftime("%d-%m %H:%M:%S") }}</p></div></body></html>
EOF

cat > static/style.css <<'EOF'
body { font-family: Arial, sans-serif; background: #eef3f9; }
.card { max-width: 600px; margin: 60px auto; background: #fff; padding: 30px; border-radius: 10px; }
EOF

If your code is on GitHub, replace the file-creation steps with git clone https://github.com/<you>/<repo>.git /opt/flaskdemo and ./venv/bin/pip install -r requirements.txt.

Step 3 — Test Gunicorn manually

cd /opt/flaskdemo
./venv/bin/gunicorn --workers 3 --bind 127.0.0.1:8000 wsgi:app &
sleep 2 && curl -s http://127.0.0.1:8000/api/info
kill %1

wsgi:app means "module wsgi.py, object app". A common rule for workers is (2 × number of CPUs) + 1.

Step 4 — systemd service

sudo tee /etc/systemd/system/flaskdemo.service > /dev/null <<EOF
[Unit]
Description=Gunicorn instance serving flaskdemo
After=network.target

[Service]
User=$USER
Group=$USER
WorkingDirectory=/opt/flaskdemo
Environment="PATH=/opt/flaskdemo/venv/bin"
ExecStart=/opt/flaskdemo/venv/bin/gunicorn --workers 3 --bind 127.0.0.1:8000 --access-logfile - wsgi:app
Restart=always

[Install]
WantedBy=multi-user.target
EOF
sudo systemctl daemon-reload
sudo service flaskdemo start
sudo systemctl enable flaskdemo
sudo service flaskdemo status

Step 5 — Nginx configuration

# AL2023 / CentOS path shown; on Ubuntu use /etc/nginx/sites-available/flaskdemo + symlink + remove default
sudo tee /etc/nginx/conf.d/flaskdemo.conf > /dev/null <<'EOF'
server {
    listen 80;
    server_name YOUR_SERVER_NAME;

    location /static/ {
        alias /opt/flaskdemo/static/;
        expires 7d;
    }
    location / {
        proxy_pass http://127.0.0.1:8000;
        proxy_set_header Host $host;
        proxy_set_header X-Real-IP $remote_addr;
        proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
        proxy_set_header X-Forwarded-Proto $scheme;
    }
}
EOF
sudo sed -i "s/YOUR_SERVER_NAME/$(curl -s https://checkip.amazonaws.com)/" /etc/nginx/conf.d/flaskdemo.conf
sudo service nginx start
sudo systemctl enable nginx
sudo nginx -t && sudo service nginx reload

CentOS Stream 9 SELinux extras:

sudo setsebool -P httpd_can_network_connect 1
sudo semanage fcontext -a -t httpd_sys_content_t "/opt/flaskdemo/static(/.*)?"
sudo restorecon -Rv /opt/flaskdemo/static

Open http://<PUBLIC_IP>/ — you should see the styled page.

Ravindra Bagale's Tip

Keep your application code in Git from day one, even for small college projects. Deployment then becomes git pull + restart, and if something breaks you can roll back instantly with git checkout <previous-commit>. Interviewers also like seeing a clean GitHub history.

Step 6 — Updating the application

cd /opt/flaskdemo
git pull                                     # if deployed from Git
./venv/bin/pip install -r requirements.txt
sudo service flaskdemo restart
journalctl -u flaskdemo -n 50 --no-pager     # check logs