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