Flask is a lightweight Python web microframework that provides only the essentials: URL routing, request/response handling, and Jinja2 templating. Unlike Django, Flask does not include an ORM, authentication, or admin interface by default — these are added through extensions like SQLAlchemy, Flask-Login, and Flask-Admin. This minimalism makes Flask the ideal choice for REST APIs, microservices, and applications where you want full control over the component stack. For production deployment, Flask applications run via a WSGI server (Gunicorn) behind Nginx. This guide covers creating a production-ready Flask 3 REST API, deploying it with Gunicorn as a systemd service, and configuring Nginx as a reverse proxy on RHEL 9.

Prerequisites

  • Python 3.12 and Nginx installed on RHEL 9

Step 1 — Create the Flask Application

mkdir /var/www/myflask && cd /var/www/myflask
python3.12 -m venv .venv
source .venv/bin/activate

pip install flask gunicorn python-dotenv flask-sqlalchemy psycopg2-binary
# app.py — minimal Flask REST API
from flask import Flask, jsonify, request
import os

app = Flask(__name__)
app.config['SECRET_KEY'] = os.environ.get('SECRET_KEY', 'change-in-production')

@app.route('/health')
def health():
    return jsonify({'status': 'ok', 'version': '1.0'})

@app.route('/api/items', methods=['GET'])
def get_items():
    # Placeholder — replace with database query
    items = [{'id': 1, 'name': 'Item One'}, {'id': 2, 'name': 'Item Two'}]
    return jsonify({'items': items, 'count': len(items)})

@app.route('/api/items', methods=['POST'])
def create_item():
    data = request.get_json()
    if not data or 'name' not in data:
        return jsonify({'error': 'name is required'}), 400
    # Placeholder — save to database
    return jsonify({'id': 3, 'name': data['name']}), 201

if __name__ == '__main__':
    app.run()

Step 2 — Create a Gunicorn Configuration File

# gunicorn.conf.py
workers = 4                          # 2×CPU + 1
worker_class = 'sync'                # Use 'gevent' for async I/O
bind = 'unix:/run/myflask.sock'
timeout = 120
keepalive = 5
accesslog = '/var/log/myflask/access.log'
errorlog = '/var/log/myflask/error.log'
loglevel = 'info'

Step 3 — Create systemd Service

# /etc/systemd/system/myflask.service
[Unit]
Description=Flask/Gunicorn API
After=network.target

[Service]
User=nginx
Group=nginx
WorkingDirectory=/var/www/myflask
EnvironmentFile=/var/www/myflask/.env
ExecStart=/var/www/myflask/.venv/bin/gunicorn -c gunicorn.conf.py app:app
ExecReload=/bin/kill -s HUP $MAINPID
Restart=always
RuntimeDirectory=myflask

[Install]
WantedBy=multi-user.target
mkdir -p /var/log/myflask && chown nginx:nginx /var/log/myflask
systemctl daemon-reload && systemctl enable --now myflask

Step 4 — Configure Nginx

# /etc/nginx/conf.d/myflask.conf
server {
    listen 80;
    server_name api.example.com;

    location / {
        proxy_pass http://unix:/run/myflask.sock;
        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;
        proxy_read_timeout 120;
    }
}
nginx -t && systemctl reload nginx

# Test the API
curl http://api.example.com/health
curl http://api.example.com/api/items

Conclusion

Flask with Gunicorn and Nginx on RHEL 9 provides a lightweight, high-performance Python API stack. For high-concurrency async APIs (many simultaneous long-polling connections), replace the default sync worker with gevent or uvicorn workers: pip install gevent then set worker_class = 'gevent' in the Gunicorn config. For REST APIs with no long-lived connections, the default sync workers are sufficient and simpler to reason about under load.

Next steps: How to Deploy a Django Application on RHEL 9, How to Use Python Virtual Environments on RHEL 9, and How to Install and Configure Nginx on RHEL 9.