Django is one of the most popular Python web frameworks, and deploying it in production requires more than the built-in development server. The recommended production stack on RHEL 8 pairs Django with Gunicorn as the WSGI application server and Nginx as a reverse proxy that handles static files, TLS termination, and connection management. This tutorial walks through the complete deployment: setting up a virtual environment, configuring Django for production, running Gunicorn, setting up Nginx as a proxy, and managing the application with a systemd service so it starts automatically on boot.

Prerequisites

  • RHEL 8 with Python 3.9 or 3.11 installed via AppStream
  • Nginx installed (dnf install -y nginx)
  • A non-root deployment user (e.g. django) with a home directory
  • A registered domain name or server IP for ALLOWED_HOSTS
  • Firewall port 80 (and 443 for TLS) open
  • Root or sudo access for systemd and Nginx configuration

Step 1 — Create the Virtual Environment and Install Django with Gunicorn

Switch to your deployment user, create the project directory, and install Django and Gunicorn inside a virtual environment so the system Python remains untouched.

useradd -m -s /bin/bash django
su - django
mkdir -p /opt/myproject && cd /opt/myproject
python3 -m venv venv --upgrade-deps
source venv/bin/activate
pip install django gunicorn
django-admin startproject myproject .
python manage.py migrate
python manage.py createsuperuser

Step 2 — Configure Django for Production

Edit myproject/settings.py to set the production-required values. Never run with DEBUG = True in production — it leaks source code and configuration details to anyone who triggers an error.

DEBUG = False
ALLOWED_HOSTS = ['yourdomain.com', '192.0.2.10']
STATIC_URL = '/static/'
STATIC_ROOT = '/opt/myproject/staticfiles'
MEDIA_URL = '/media/'
MEDIA_ROOT = '/opt/myproject/media'

Collect all static files into STATIC_ROOT so Nginx can serve them directly:

python manage.py collectstatic --noinput
ls /opt/myproject/staticfiles/

Step 3 — Test Gunicorn and Bind to a Unix Socket

Verify Gunicorn can serve the application before wiring up Nginx. Using a Unix socket rather than a TCP port is faster and avoids exposing the application server to the network directly.

source /opt/myproject/venv/bin/activate
gunicorn --workers 3 --bind unix:/run/gunicorn.sock myproject.wsgi:application
# Test in a second terminal:
curl --unix-socket /run/gunicorn.sock http://localhost/
# Stop gunicorn with Ctrl-C when confirmed working

Step 4 — Create a systemd Service for Gunicorn

A systemd service unit ensures Gunicorn starts at boot, restarts on failure, and runs as the unprivileged django user.

cat > /etc/systemd/system/gunicorn.service << 'EOF'
[Unit]
Description=Gunicorn Django application server
After=network.target

[Service]
Type=notify
User=django
Group=django
WorkingDirectory=/opt/myproject
ExecStart=/opt/myproject/venv/bin/gunicorn 
  --access-logfile /var/log/gunicorn/access.log 
  --error-logfile /var/log/gunicorn/error.log 
  --workers 3 
  --bind unix:/run/gunicorn/gunicorn.sock 
  myproject.wsgi:application
RuntimeDirectory=gunicorn
LogsDirectory=gunicorn
Restart=on-failure

[Install]
WantedBy=multi-user.target
EOF
systemctl daemon-reload
systemctl enable --now gunicorn
systemctl status gunicorn

Step 5 — Configure Nginx as a Reverse Proxy

Create an Nginx server block that proxies dynamic requests to Gunicorn and serves static and media files directly from disk, which is far more efficient than routing them through Django.

cat > /etc/nginx/conf.d/myproject.conf << 'EOF'
server {
    listen 80;
    server_name yourdomain.com;

    location /static/ {
        alias /opt/myproject/staticfiles/;
    }

    location /media/ {
        alias /opt/myproject/media/;
    }

    location / {
        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_pass http://unix:/run/gunicorn/gunicorn.sock;
    }
}
EOF
nginx -t
systemctl enable --now nginx
systemctl reload nginx

Step 6 — Open the Firewall and Test End-to-End

Allow HTTP traffic through the RHEL 8 firewall and verify the full stack is responding correctly.

firewall-cmd --permanent --add-service=http
firewall-cmd --permanent --add-service=https
firewall-cmd --reload
curl -I http://yourdomain.com/
curl -I http://yourdomain.com/static/admin/css/base.css
systemctl status gunicorn nginx

If you see a 502 Bad Gateway, check journalctl -u gunicorn for errors and verify the socket path in both the systemd unit and Nginx config match exactly.

Conclusion

You have deployed a Django application on RHEL 8 using a virtual environment, configured it for production with DEBUG=False and ALLOWED_HOSTS, collected static files, run Gunicorn on a Unix socket managed by systemd, and placed Nginx in front as a reverse proxy serving static assets directly. This stack is production-ready and forms the foundation for adding TLS with Let’s Encrypt, database connection pooling, and horizontal scaling. The systemd service ensures the application survives reboots and recovers automatically from crashes.

Next steps: How to Secure Nginx with Let’s Encrypt on RHEL 8, How to Use Python Virtual Environments on RHEL 8, and How to Configure PostgreSQL for a Django Application on RHEL 8.