Django is a high-level Python web framework that handles much of the complexity of web development – URL routing, database ORM, templating, authentication, and admin interface – out of the box. It powers some of the largest websites on the internet and remains the go-to framework for Python web applications.
This guide walks through deploying a Django application on Rocky Linux 10 / AlmaLinux 10 using Gunicorn as the WSGI application server and Nginx as the reverse proxy. This is the recommended production setup, replacing the older Apache + mod_wsgi approach. Python 3.12 ships with RHEL 10 and its derivatives, and we will use Django 5.2 LTS for long-term support.
Prerequisites
- A server running Rocky Linux 10 or AlmaLinux 10 with root or sudo access
- A domain name pointed to your server IP (for Nginx virtual host configuration)
- Ports 80 (HTTP) and 443 (HTTPS) open in the firewall
- At least 1GB RAM and 1 vCPU
All commands below assume you are running as root. If you are a sudo user, switch to root first:
sudo -i
Step 1: Install Python 3 and Required Packages
Rocky Linux 10 and AlmaLinux 10 ship with Python 3.12 in the base repositories. Install Python along with the development headers and pip package manager. If you need a newer Python version, see our guide on installing Python on RHEL 10 / Rocky Linux 10.
dnf install -y python3 python3-pip python3-devel gcc
Verify the installed Python version:
python3 --version
Expected output:
$ python3 --version
Python 3.12.8
Step 2: Create a Python Virtual Environment
Create a dedicated system user for the Django application. Running the app under its own user is a security best practice – it limits the damage if the application is compromised.
useradd -r -m -d /opt/django -s /bin/bash django
Switch to the django user and create a virtual environment:
su - django
python3 -m venv /opt/django/venv
source /opt/django/venv/bin/activate
Upgrade pip inside the virtual environment:
pip install --upgrade pip
Step 3: Install Django 5.2 LTS and Gunicorn
Install Django and Gunicorn inside the virtual environment. Django 5.2 is a Long Term Support release with security updates guaranteed for at least three years.
pip install django gunicorn
Verify the installed versions:
$ python -m django --version
5.2.12
$ gunicorn --version
gunicorn (version 23.0.0)
Step 4: Create a Django Project
Create a new Django project. If you are deploying an existing project from a Git repository, clone it into /opt/django/ instead and install its dependencies from requirements.txt.
cd /opt/django
django-admin startproject myproject
Edit the Django settings to configure allowed hosts and static files. Open the settings file:
vi /opt/django/myproject/myproject/settings.py
Update the following settings. Replace example.com and the server IP with your actual values:
# Allow your domain and server IP
ALLOWED_HOSTS = ['example.com', 'www.example.com', '192.168.1.100']
# Set DEBUG to False for production
DEBUG = False
# Static files configuration
STATIC_URL = '/static/'
STATIC_ROOT = '/opt/django/myproject/static/'
Run migrations and collect static files:
cd /opt/django/myproject
python manage.py migrate
python manage.py collectstatic --noinput
Create a Django superuser for the admin panel:
python manage.py createsuperuser
Test that Gunicorn can serve the project. Run this from the project directory:
cd /opt/django/myproject
gunicorn --bind 0.0.0.0:8000 myproject.wsgi:application
Open http://your-server-ip:8000 in a browser to confirm it works. Press Ctrl+C to stop Gunicorn, then exit the django user session:
deactivate
exit
Step 5: Configure Gunicorn as a Systemd Service
Create a systemd service file so Gunicorn starts automatically on boot and can be managed with systemctl. This is similar to how other Python WSGI apps like Flask with Gunicorn are deployed.
vi /etc/systemd/system/gunicorn.service
Add the following configuration:
[Unit]
Description=Gunicorn daemon for Django application
After=network.target
[Service]
User=django
Group=django
WorkingDirectory=/opt/django/myproject
ExecStart=/opt/django/venv/bin/gunicorn \
--workers 3 \
--bind unix:/opt/django/myproject/gunicorn.sock \
--access-logfile /opt/django/logs/gunicorn-access.log \
--error-logfile /opt/django/logs/gunicorn-error.log \
myproject.wsgi:application
Restart=on-failure
RestartSec=5
[Install]
WantedBy=multi-user.target
Create the logs directory and set ownership:
mkdir -p /opt/django/logs
chown django:django /opt/django/logs
Reload systemd, enable, and start the Gunicorn service:
systemctl daemon-reload
systemctl enable --now gunicorn
Verify the service is running:
systemctl status gunicorn
Expected output should show active (running). Also confirm the socket file was created:
ls -la /opt/django/myproject/gunicorn.sock
Step 6: Install and Configure Nginx as Reverse Proxy
Nginx sits in front of Gunicorn, handling client connections, serving static files directly, and proxying dynamic requests to the Gunicorn socket. Install Nginx from the default repositories:
dnf install -y nginx
Create an Nginx server block for the Django application:
vi /etc/nginx/conf.d/django.conf
Add the following configuration. Replace example.com with your actual domain name:
server {
listen 80;
server_name example.com www.example.com;
# Serve static files directly
location /static/ {
alias /opt/django/myproject/static/;
expires 30d;
add_header Cache-Control "public, immutable";
}
# Serve media files directly
location /media/ {
alias /opt/django/myproject/media/;
expires 30d;
}
# Proxy all other requests to Gunicorn
location / {
proxy_pass http://unix:/opt/django/myproject/gunicorn.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 300;
proxy_connect_timeout 300;
}
# Max upload size
client_max_body_size 10M;
}
Test the Nginx configuration for syntax errors:
nginx -t
Expected output:
$ nginx -t
nginx: the configuration file /etc/nginx/nginx.conf syntax is ok
nginx: configuration file /etc/nginx/nginx.conf test is successful
Enable and start Nginx:
systemctl enable --now nginx
Step 7: Configure SELinux for Django
Rocky Linux 10 has SELinux enabled in enforcing mode by default. Nginx needs permission to connect to the Gunicorn socket and read static files. Without these SELinux adjustments, you will get 502 Bad Gateway or 403 Forbidden errors.
Allow Nginx to connect to the upstream Gunicorn socket:
setsebool -P httpd_can_network_connect 1
Set the correct SELinux context on the static and media files so Nginx can read them:
semanage fcontext -a -t httpd_sys_content_t "/opt/django/myproject/static(/.*)?"
semanage fcontext -a -t httpd_sys_content_t "/opt/django/myproject/media(/.*)?"
restorecon -Rv /opt/django/myproject/static/
restorecon -Rv /opt/django/myproject/media/
Set the context on the Gunicorn socket so Nginx can connect to it:
semanage fcontext -a -t httpd_var_run_t "/opt/django/myproject/gunicorn.sock"
restorecon -v /opt/django/myproject/gunicorn.sock
If semanage is not installed, install the policycoreutils-python-utils package:
dnf install -y policycoreutils-python-utils
Verify SELinux is not blocking anything by checking the audit log:
$ ausearch -m avc -ts recent
----
no matches
No matches means SELinux is not denying any access. For more details on managing SELinux policies, see our guide on troubleshooting SELinux issues with seTroubleshoot.
Step 8: Configure Firewall Rules
Open ports 80 (HTTP) and 443 (HTTPS) in firewalld. Port 443 is needed if you plan to add SSL later. For a deeper look at firewalld configuration on RHEL and Rocky Linux, check our dedicated guide.
firewall-cmd --permanent --add-service=http
firewall-cmd --permanent --add-service=https
firewall-cmd --reload
Verify the rules are active:
firewall-cmd --list-services
Expected output should include http and https in the list.
Step 9: Verify the Django Deployment
Open your browser and navigate to your server’s domain name or IP address. You should see the Django default page or your application if you deployed an existing project.
Run a quick check from the command line:
$ curl -I http://example.com
HTTP/1.1 200 OK
Server: nginx
Content-Type: text/html; charset=utf-8
X-Frame-Options: DENY
Check that static files are served correctly:
$ curl -I http://example.com/static/admin/css/base.css
HTTP/1.1 200 OK
Content-Type: text/css
Cache-Control: public, immutable
Access the Django admin panel at http://example.com/admin/ and log in with the superuser credentials created earlier.
Deploying an Existing Django Project from Git
If you are deploying an existing project instead of creating a new one, replace Step 4 with the following. Switch to the django user and clone your repository:
su - django
source /opt/django/venv/bin/activate
cd /opt/django
git clone https://github.com/youruser/yourproject.git myproject
Install project dependencies:
cd /opt/django/myproject
pip install -r requirements.txt
Then continue with the database migrations, static file collection, and Gunicorn configuration as described in the steps above. Make sure to update the WorkingDirectory and ExecStart paths in the systemd service file if your project structure differs.
Conclusion
The Django application is now running in production on Rocky Linux 10 / AlmaLinux 10 with Gunicorn handling WSGI requests and Nginx as the reverse proxy serving static content. This stack is reliable and scales well – Gunicorn workers can be increased to match available CPU cores.
For a production-ready deployment, add SSL with Let’s Encrypt using Certbot, configure a production database like PostgreSQL or MariaDB, set up log rotation for Gunicorn and Nginx logs, and enable automated backups of your database and media files.