How to Deploy a Flask Application with Gunicorn and Nginx on RHEL 7

Flask is a lightweight Python web framework ideal for building APIs and web applications. While Flask’s built-in development server is useful for testing, it is not suitable for production workloads. The standard production stack on RHEL 7 combines Gunicorn as a WSGI application server with Nginx acting as a reverse proxy to handle static files, SSL termination, and load balancing. This guide walks you through every step required to deploy a Flask application in this configuration on Red Hat Enterprise Linux 7, including creating a virtual environment, writing a systemd service unit, configuring Nginx, and managing environment variables securely.

Prerequisites

  • A running RHEL 7 server with root or sudo access
  • Python 3.6 or later installed (yum install python3)
  • Nginx installed (yum install nginx)
  • A non-root system user to run the application (e.g., flaskuser)
  • Firewall access on ports 80 and 443
  • A domain name or server IP address for Nginx configuration

Step 1: Create a Dedicated System User

Running web applications as root is a security risk. Create a dedicated user that owns the application files and runs the Gunicorn process.

sudo useradd -r -s /sbin/nologin flaskuser
sudo mkdir -p /var/www/myflaskapp
sudo chown flaskuser:flaskuser /var/www/myflaskapp

The -r flag creates a system account and -s /sbin/nologin prevents interactive login, reducing the attack surface.

Step 2: Create a Python Virtual Environment

Virtual environments isolate your application’s Python dependencies from the system Python installation, preventing version conflicts.

sudo -u flaskuser python3 -m venv /var/www/myflaskapp/venv

Activate the virtual environment to install packages into it:

sudo -u flaskuser bash -c "source /var/www/myflaskapp/venv/bin/activate && pip install --upgrade pip"

Step 3: Install Flask and Gunicorn

With the virtual environment active, install Flask and Gunicorn using pip:

sudo -u flaskuser bash -c "
  source /var/www/myflaskapp/venv/bin/activate
  pip install flask gunicorn
"

Verify the installations:

sudo -u flaskuser /var/www/myflaskapp/venv/bin/pip show flask gunicorn

Step 4: Write the Flask Application

Create the application entry point at /var/www/myflaskapp/app.py. A minimal but realistic Flask application looks like this:

sudo -u flaskuser tee /var/www/myflaskapp/app.py <<'EOF'
from flask import Flask, jsonify
import os

app = Flask(__name__)

@app.route('/')
def index():
    return jsonify({"status": "ok", "env": os.environ.get("APP_ENV", "production")})

@app.route('/health')
def health():
    return jsonify({"healthy": True}), 200

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

Test that Gunicorn can load the application directly before configuring systemd:

sudo -u flaskuser /var/www/myflaskapp/venv/bin/gunicorn 
    --workers 3 
    --bind 0.0.0.0:8000 
    app:app 
    --chdir /var/www/myflaskapp

If the application starts without errors, press Ctrl+C to stop it and proceed.

Step 5: Create an Environment File

Store sensitive configuration values—database URLs, secret keys, API tokens—in a dedicated environment file rather than hardcoding them in your application or service unit.

sudo tee /var/www/myflaskapp/.env <<'EOF'
APP_ENV=production
SECRET_KEY=replace-with-a-strong-random-value
DATABASE_URL=postgresql://user:password@localhost/mydb
EOF

sudo chown flaskuser:flaskuser /var/www/myflaskapp/.env
sudo chmod 640 /var/www/myflaskapp/.env

The 640 permission ensures only the flaskuser owner and its group can read the file; world access is denied.

Step 6: Create the Gunicorn systemd Service

Create a systemd unit file so that Gunicorn starts automatically on boot and restarts if it crashes. The EnvironmentFile directive loads your .env values into the process environment.

sudo tee /etc/systemd/system/gunicorn.service <<'EOF'
[Unit]
Description=Gunicorn WSGI server for Flask application
After=network.target

[Service]
User=flaskuser
Group=flaskuser
WorkingDirectory=/var/www/myflaskapp
EnvironmentFile=/var/www/myflaskapp/.env
ExecStart=/var/www/myflaskapp/venv/bin/gunicorn 
    --workers 3 
    --bind unix:/run/gunicorn/gunicorn.sock 
    --access-logfile /var/log/gunicorn/access.log 
    --error-logfile /var/log/gunicorn/error.log 
    app:app
ExecReload=/bin/kill -s HUP $MAINPID
RuntimeDirectory=gunicorn
LogsDirectory=gunicorn
Restart=on-failure
RestartSec=5s
KillMode=mixed
TimeoutStopSec=5

[Install]
WantedBy=multi-user.target
EOF

The RuntimeDirectory=gunicorn directive tells systemd to create /run/gunicorn/ automatically with correct permissions. Enable and start the service:

sudo systemctl daemon-reload
sudo systemctl enable gunicorn
sudo systemctl start gunicorn
sudo systemctl status gunicorn

Confirm the Unix socket exists:

ls -la /run/gunicorn/gunicorn.sock

Step 7: Configure Nginx as a Reverse Proxy

Nginx will accept all incoming HTTP requests and forward them to Gunicorn via the Unix socket. Create a server block configuration file:

sudo tee /etc/nginx/conf.d/myflaskapp.conf <<'EOF'
upstream gunicorn_backend {
    server unix:/run/gunicorn/gunicorn.sock fail_timeout=0;
}

server {
    listen 80;
    server_name yourdomain.com www.yourdomain.com;

    access_log  /var/log/nginx/myflaskapp_access.log;
    error_log   /var/log/nginx/myflaskapp_error.log;

    location / {
        proxy_pass          http://gunicorn_backend;
        proxy_set_header    Host              $http_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_redirect      off;
        proxy_connect_timeout 60s;
        proxy_read_timeout    60s;
        proxy_send_timeout    60s;
        client_max_body_size  16m;
    }

    location /static/ {
        alias /var/www/myflaskapp/static/;
        expires 30d;
        access_log off;
    }
}
EOF

Test the Nginx configuration syntax and reload:

sudo nginx -t
sudo systemctl enable nginx
sudo systemctl start nginx
sudo systemctl reload nginx

Step 8: Configure the Firewall

Open ports 80 (HTTP) and 443 (HTTPS) in firewalld so external traffic can reach Nginx:

sudo firewall-cmd --permanent --add-service=http
sudo firewall-cmd --permanent --add-service=https
sudo firewall-cmd --reload
sudo firewall-cmd --list-all

If SELinux is enforcing, you must allow Nginx to connect to the Gunicorn socket:

sudo setsebool -P httpd_can_network_connect 1

Step 9: Verify the Deployment

Test the full stack end-to-end using curl:

curl -s http://yourdomain.com/
curl -s http://yourdomain.com/health

Check logs if there are any issues:

sudo journalctl -u gunicorn -n 50 --no-pager
sudo tail -n 50 /var/log/gunicorn/error.log
sudo tail -n 50 /var/log/nginx/myflaskapp_error.log

Step 10: Reloading the Application After Code Changes

After updating your Flask application code, reload Gunicorn gracefully without dropping active connections:

sudo systemctl reload gunicorn

For a full restart (e.g., after adding or removing workers in the service file):

sudo systemctl daemon-reload
sudo systemctl restart gunicorn

You now have a production-ready Flask deployment on RHEL 7 with Gunicorn and Nginx. Gunicorn serves the Python application using multiple worker processes, while Nginx handles connection management, static file serving, and acts as the public-facing entry point. The systemd service ensures the application starts automatically on boot and recovers from failures, and the EnvironmentFile approach keeps sensitive credentials out of your codebase. As a next step, consider adding SSL certificates via Certbot and configuring log rotation with logrotate to manage disk usage over time.