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

Django’s built-in development server is single-threaded, outputs detailed error tracebacks to the browser, and is explicitly documented as unsuitable for production. The standard production deployment for Django on Linux is a two-layer stack: Gunicorn (a Python WSGI HTTP server) handles the Python application layer, and Nginx sits in front as a reverse proxy that handles static files, TLS termination, connection buffering, and forwarding dynamic requests to Gunicorn. On RHEL 7, this stack integrates with systemd for process management and requires a few SELinux adjustments to run cleanly. This guide walks through every step from a fresh Django project to a fully operational Nginx + Gunicorn deployment.

Prerequisites

  • RHEL 7 server with root or sudo access
  • Python 3 installed (via SCL or IUS — see previous tutorials)
  • Nginx installed (sudo yum install -y nginx)
  • A registered domain or IP address pointing to the server
  • Basic familiarity with Django project structure
  • Firewall ports 80 and 443 open

Step 1: Create a Dedicated System User

Running your application as root is a critical security mistake. Create a dedicated, non-login system user that owns the application files and runs the Gunicorn process.

sudo useradd -r -s /sbin/nologin -d /var/www/myapp -m djangoapp

This creates a system account (-r) with no interactive shell (-s /sbin/nologin) and a home directory at /var/www/myapp.

Step 2: Set Up the Project Directory and Virtual Environment

# Create the application directory structure
sudo mkdir -p /var/www/myapp
sudo chown djangoapp:djangoapp /var/www/myapp

# Switch to the application user
sudo -u djangoapp bash

# Move into the app directory
cd /var/www/myapp

# Create the virtual environment (using SCL Python 3.6 as example)
source /opt/rh/rh-python36/enable
python3 -m venv venv

# Activate the environment
source venv/bin/activate

# Confirm Python path
which python
# /var/www/myapp/venv/bin/python

Step 3: Install Django and Gunicorn

# With the venv active and as the djangoapp user:
pip install --upgrade pip
pip install django gunicorn

# Also install any database adapter you need, e.g. for PostgreSQL:
# pip install psycopg2-binary

# Verify installations
django-admin --version
gunicorn --version

Step 4: Create and Configure a Django Project

If you are deploying an existing Django project, clone it into /var/www/myapp/ instead of creating a new one.

# Create a new Django project (replace 'myproject' with your project name)
django-admin startproject myproject /var/www/myapp/myproject

# Your directory structure is now:
# /var/www/myapp/
# ├── venv/
# └── myproject/
#     ├── manage.py
#     └── myproject/
#         ├── __init__.py
#         ├── settings.py
#         ├── urls.py
#         └── wsgi.py

Edit settings.py for production. Key settings to update:

# /var/www/myapp/myproject/myproject/settings.py

# Set your domain or IP (never use '*' in production)
ALLOWED_HOSTS = ['yourdomain.com', 'www.yourdomain.com', '192.168.1.100']

# Disable debug mode in production
DEBUG = False

# Static files directory (Nginx will serve these directly)
STATIC_URL = '/static/'
STATIC_ROOT = '/var/www/myapp/staticfiles'

# Media files (user uploads)
MEDIA_URL = '/media/'
MEDIA_ROOT = '/var/www/myapp/media'

# Secret key — use an environment variable, never hard-code
import os
SECRET_KEY = os.environ.get('DJANGO_SECRET_KEY', 'change-this-in-production')

Collect static files so Nginx can serve them:

cd /var/www/myapp/myproject
python manage.py collectstatic --noinput

Run database migrations:

python manage.py migrate

Step 5: Test Gunicorn Manually

Before creating a systemd service, verify that Gunicorn can start your application correctly. The argument to Gunicorn is the Python dotted path to your WSGI application object.

cd /var/www/myapp/myproject

# Run Gunicorn binding to a local port for testing
gunicorn --workers 3 --bind 0.0.0.0:8000 myproject.wsgi:application

# You should see output like:
# [INFO] Listening at: http://0.0.0.0:8000
# [INFO] Worker booted (pid: 12345)

In a second terminal, test the response:

curl -I http://localhost:8000/
# HTTP/1.1 200 OK
# Server: gunicorn

Once verified, stop Gunicorn with Ctrl+C. For production, Gunicorn should bind to a Unix socket rather than a TCP port — Unix sockets are faster for local communication and cannot be accidentally exposed to the network.

Step 6: Create a systemd Service for Gunicorn

A systemd service ensures Gunicorn starts automatically at boot, restarts on failure, and logs to the journal.

Exit the djangoapp user shell back to root/sudo user, then create the service file:

sudo vi /etc/systemd/system/gunicorn-myapp.service
[Unit]
Description=Gunicorn daemon for myapp Django application
After=network.target

[Service]
Type=notify
User=djangoapp
Group=djangoapp
RuntimeDirectory=gunicorn
WorkingDirectory=/var/www/myapp/myproject

# Set the secret key as an environment variable
Environment="DJANGO_SECRET_KEY=your-very-long-random-secret-key-here"
Environment="DJANGO_SETTINGS_MODULE=myproject.settings"

# Full path to gunicorn inside the virtual environment
ExecStart=/var/www/myapp/venv/bin/gunicorn 
    --workers 3 
    --bind unix:/run/gunicorn/myapp.sock 
    --access-logfile /var/log/gunicorn/myapp-access.log 
    --error-logfile /var/log/gunicorn/myapp-error.log 
    myproject.wsgi:application

ExecReload=/bin/kill -s HUP $MAINPID
KillMode=mixed
TimeoutStopSec=5
PrivateTmp=true
Restart=on-failure
RestartSec=5

[Install]
WantedBy=multi-user.target

Create the log directory and socket directory:

sudo mkdir -p /var/log/gunicorn
sudo chown djangoapp:djangoapp /var/log/gunicorn

sudo mkdir -p /run/gunicorn
sudo chown djangoapp:nginx /run/gunicorn
sudo chmod 750 /run/gunicorn

Enable and start the service:

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

# Verify the socket file was created
ls -la /run/gunicorn/myapp.sock

Step 7: Configure Nginx as a Reverse Proxy

Nginx listens on ports 80/443, serves static files directly from the filesystem, and forwards all other requests to Gunicorn via the Unix socket.

sudo vi /etc/nginx/conf.d/myapp.conf
server {
    listen 80;
    server_name yourdomain.com www.yourdomain.com;

    # Security headers
    add_header X-Content-Type-Options nosniff;
    add_header X-Frame-Options DENY;
    add_header X-XSS-Protection "1; mode=block";

    # Serve static files directly — Nginx is much faster than Django/Gunicorn for this
    location /static/ {
        alias /var/www/myapp/staticfiles/;
        expires 30d;
        add_header Cache-Control "public, immutable";
    }

    location /media/ {
        alias /var/www/myapp/media/;
        expires 7d;
    }

    # Forward all other requests to Gunicorn
    location / {
        proxy_pass http://unix:/run/gunicorn/myapp.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;

        # Timeouts
        proxy_connect_timeout 60s;
        proxy_send_timeout 60s;
        proxy_read_timeout 60s;

        # Buffer settings
        proxy_buffering on;
        proxy_buffer_size 8k;
        proxy_buffers 8 8k;
    }
}

Test the Nginx configuration and reload:

sudo nginx -t
# nginx: configuration file /etc/nginx/nginx.conf test is successful

sudo systemctl enable nginx
sudo systemctl start nginx
# or if already running:
sudo systemctl reload nginx

Step 8: Handle SELinux

RHEL 7 runs SELinux in enforcing mode by default. The Nginx process needs permission to connect to the Gunicorn Unix socket, and Django needs to read its application files. SELinux may block these operations unless the correct policies are applied.

# Allow Nginx to connect to upstream sockets/proxies
sudo setsebool -P httpd_can_network_connect 1

# If Nginx needs to read files from /var/www/myapp:
sudo chcon -R -t httpd_sys_content_t /var/www/myapp/staticfiles
sudo chcon -R -t httpd_sys_content_t /var/www/myapp/media

# Set the socket file context so Nginx can connect to it
sudo semanage fcontext -a -t httpd_var_run_t "/run/gunicorn(/.*)?"
sudo restorecon -Rv /run/gunicorn

# Allow the application to write to its directories (logs, media uploads, etc.)
sudo chcon -R -t httpd_sys_rw_content_t /var/log/gunicorn
sudo chcon -R -t httpd_sys_rw_content_t /var/www/myapp/media

If you encounter SELinux denials, check the audit log for context:

sudo tail -f /var/log/audit/audit.log | grep denied
sudo ausearch -m AVC -ts recent | audit2allow -a

Step 9: Configure the Firewall

# Open HTTP and HTTPS ports
sudo firewall-cmd --permanent --add-service=http
sudo firewall-cmd --permanent --add-service=https
sudo firewall-cmd --reload

# Verify
sudo firewall-cmd --list-services

Step 10: Verify the Deployment

## Check all services are running
sudo systemctl status gunicorn-myapp nginx

# Check Gunicorn logs
sudo tail -f /var/log/gunicorn/myapp-error.log

# Test from the command line
curl -I http://yourdomain.com/
# HTTP/1.1 200 OK
# Server: nginx/1.16.1

# Test static file serving (should not pass through Gunicorn)
curl -I http://yourdomain.com/static/admin/css/base.css
# HTTP/1.1 200 OK
# Cache-Control: public, immutable

Deploying Django with Gunicorn and Nginx on RHEL 7 produces a robust, production-grade stack that handles concurrent connections efficiently, serves static assets at full Nginx speed, and keeps your Python application process isolated under a dedicated non-root user. The systemd integration means Gunicorn survives server reboots and recovers automatically from unexpected crashes. With SELinux and firewall rules correctly applied, the deployment meets RHEL 7’s security expectations without compromising functionality. As your traffic grows, you can scale by increasing Gunicorn’s worker count, adding load balancers, or introducing caching layers — but the foundation described here remains the same.