Ravindra BagaleCourses & study guides मराठी Track your progress

Chapter 11: Application Hosting — WordPress, Flask and Express

11.2 Flask application with Gunicorn + Nginx

Students in my class run python app.py on the server and think the deployment is done. Look – that is only the development server. Let's do it the production way.

Why Gunicorn?

Friends, app.run() is only for development on your laptop. In production, Gunicorn runs several workers and Nginx stands in front.

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 code in Git from day one, even for small college projects. Deployment then becomes git pull + restart, and if something breaks you can go 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

Ravindra Bagale's Tip

Many students mix up app:app and wsgi:app in Gunicorn and get ModuleNotFoundError in journalctl. wsgi:app means the file wsgi.py and the app object inside it. If the service fails, first look at journalctl -u flaskdemo -n 50 --no-pager.

Lab

Chala, Flask demo deploy kara aani http://<PUBLIC_IP>/ var styled page disto ka te bagha. Mag templates/index.html madhe <h1> madhe "Mauli Dairy" liha, sudo service flaskdemo restart kara aani badal disto ka te check kara.

Got it? Gunicorn runs the code, Nginx stands in front, and systemd never lets it stop. Now let's move on to Express.