This guide walks you through a production-style Django deployment on Rocky Linux 9 or AlmaLinux 9: PostgreSQL for the database, Gunicorn to run the WSGI application, and Nginx as the public-facing reverse proxy. You will use a Python virtual environment, systemd to supervise Gunicorn, and Unix sockets (rather than exposing Gunicorn on a TCP port) so only Nginx talks to your app process.
What you will set up
- System packages, EPEL, and PostgreSQL 14 from the PostgreSQL Global Development Group (PGDG) YUM repository
- A dedicated project layout under
/opt/djangoappwith avenvand a Django project - Database configuration via environment variables (no secrets in Git)
- Static file collection for Nginx to serve directly
- A systemd unit for Gunicorn and an Nginx
serverblock that proxies to a Unix socket
Related guides
- How to run Django and Postgres in docker-compose
- Running PostgreSQL 14 with Docker and Docker-Compose
- Getting started with Django – A simple CRUD application
- How to Install and Configure Postgres 14 on Rocky Linux 9
Prerequisites
- A Rocky Linux 9 or AlmaLinux 9 server (the commands are the same on both for this tutorial)
- SSH access with
sudo - A domain name (optional for testing with an IP, recommended for TLS and
ALLOWED_HOSTS) - Basic familiarity with the shell,
vimor another editor, and Django’smanage.pycommands
You will install Django inside a virtual environment so project dependencies stay isolated and upgrades are predictable.
1. Update the system and enable EPEL
Refresh installed packages, then install EPEL (Extra Packages for Enterprise Linux), which provides additional dependencies some Python stacks expect:
| |
2. SELinux (know what you are changing)
This article focuses on the application stack; a full SELinux policy for Django, PostgreSQL, and Nginx is its own topic. If you are learning on a throwaway VM, some guides disable SELinux for simplicity—on real servers, prefer leaving SELinux enforcing and fixing contexts or booleans instead of disabling security wholesale.
If you still choose to disable it persistently, edit /etc/selinux/config and set the mode (not “mod”) to disabled:
| |
To switch to permissive mode until the next reboot (diagnostics only):
| |
3. Install build tools, Python, and PostgreSQL
Install Python and headers so pip can build packages when needed:
| |
Add the PGDG repository and install PostgreSQL 14 server, client tools, and development headers (useful if you compile psycopg2):
| |
For a deeper walkthrough of PostgreSQL on this OS, see How to Install and Configure Postgres 14 on Rocky Linux 9.
Initialize the data directory (once per machine):
| |
4. Create database and role for Django
Switch to the postgres OS user and open psql:
| |
In the SQL prompt, replace the example names and password. Prefer a strong password and store it in a password manager until you copy it into an env file on the server (see systemd section).
| |
On PostgreSQL 15+, default public schema permissions are stricter; the GRANT … ON SCHEMA public pattern becomes even more important. PostgreSQL 14 still benefits from explicit grants for clarity.
5. Create a virtual environment and install Django
Pick a deployment user (examples below use appuser). Create that user if you do not want to run the app as your personal login:
| |
As that user, create the environment with the standard library (no sudo pip needed):
| |
If pip cannot find pg_config when building psycopg2, expose the PostgreSQL 14 binaries first:
| |
Install Django, Gunicorn, and one PostgreSQL adapter approach: either psycopg2 (links against system libpq—good when postgresql14-devel is present) or psycopg2-binary (prebuilt wheels—convenient for development). For this server setup with postgresql14-devel installed:
| |
6. Create the Django project and configure the database
Still in /opt/djangoapp with the venv active:
| |
This creates manage.py in /opt/djangoapp/ and the Python package myapp/ beside it (/opt/djangoapp/myapp/settings.py, wsgi.py, and so on). Stay in /opt/djangoapp when you run manage.py commands.
Edit myapp/settings.py (path: /opt/djangoapp/myapp/settings.py):
- Ensure
import osis present at the top (Django’sstartprojecttemplate includes it). - Set
ALLOWED_HOSTSto your real domain or IP (never leave*in production). - Replace the default SQLite
DATABASESblock with PostgreSQL, reading from the environment.
Use the modern engine name django.db.backends.postgresql (preferred in current Django; postgresql_psycopg2 is an older alias):
| |
Migrate, create admin, collect static
Export variables (or rely on systemd in the next section), then run migrations:
| |
Quick checks: dev server and Gunicorn
| |
Visit http://YOUR_SERVER_IP:8000 and /admin. Stop with Ctrl+C.
Test Gunicorn on a TCP port (temporary):
| |
Admin CSS will still look bare until Nginx serves STATIC_ROOT; that is expected. Stop Gunicorn with Ctrl+C, then deactivate if you are done with the venv shell.
7. systemd unit for Gunicorn (Unix socket)
Do not embed real SECRET_KEY or database passwords in the unit file checked into documentation—on the server, use root-only permissions on an EnvironmentFile.
Create /etc/django/myapp.env (mode 600, owned by root):
| |
Example contents (generate a new secret for production):
| |
Create /etc/systemd/system/myapp.service:
| |
| |
Notes:
Type=simpleis widely compatible with Gunicorn; newer Gunicorn builds can useType=notifyif you enable systemd integration in your Gunicorn configuration.- Omit
--reloadin production; it adds overhead and is intended for development. Group=nginxhelps when you set socket permissions so Nginx can read the Unix socket.
Reload systemd and start the app:
| |
If the socket is not yet group-readable, after the first successful start:
| |
(or ensure your Gunicorn umask/group settings create it correctly from the first request).
8. Nginx reverse proxy
Install Nginx if needed:
| |
Create /etc/nginx/conf.d/myapp.conf (adjust server_name and paths):
| |
The sample favicon.ico alias assumes you add a file there or remove the block. Django’s collectstatic layout may put admin assets under static/admin/... only.
Validate and reload Nginx:
| |
9. Permissions so Nginx can reach the app
Nginx (user nginx) must traverse /opt/djangoapp and read static files and the Unix socket. A common pattern:
| |
Verify ownership: project files owned by appuser, socket appuser:nginx with 660.
10. Firewall (firewalld)
If firewalld is enabled, allow HTTP and HTTPS:
| |
11. HTTPS (recommended)
For a public site, terminate TLS at Nginx. On many servers, Let’s Encrypt via certbot is the simplest path:
| |
Then set SECURE_PROXY_SSL_HEADER and USE_TLS/CSRF_TRUSTED_ORIGINS in Django as appropriate when everything sits behind HTTPS—see the Django deployment checklist.
Troubleshooting
| Symptom | Things to check |
|---|---|
| 502 Bad Gateway from Nginx | Is myapp running? (systemctl status myapp). Socket path match? Permissions on /opt/djangoapp/app.sock? |
| Database connection errors | postgresql-14 active? Credentials in /etc/django/myapp.env? pg_hba.conf allows local md5/scram for 127.0.0.1? |
| Static files 404 | Run collectstatic. STATIC_ROOT matches Nginx alias. Permissions for nginx to read static/. |
| Works on IP, breaks on domain | ALLOWED_HOSTS must include the domain. DNS must point to this server. |
| Migrations not applied | Run python manage.py migrate with the same env vars as production. |
Logs:
| |
Security and maintenance checklist
- Rotate
DJANGO_SECRET_KEYonly with a deliberate plan (it invalidates sessions); store it in/etc/django/myapp.env, mode600. - Set
DEBUG=0and realALLOWED_HOSTSbefore going live. - Keep
gunicorn, Django, and OS packages updated (dnf update). - Use TLS, limit admin exposure (VPN, IP allowlist, or separate host), and configure backups for PostgreSQL.
- Prefer fail2ban or similar if you expose SSH and admin endpoints to the internet.
Conclusion
You now have a typical Rocky Linux 9 / AlmaLinux 9 layout for Django: PostgreSQL for data, venv for Python dependencies, Gunicorn supervised by systemd on a Unix socket, and Nginx serving static assets and proxying dynamic requests. Extend this foundation with HTTPS, monitoring, centralized logging, and hardened SELinux policies when you move from a lab setup to production traffic.
If you want a containerized variant of the same stack, start from the Docker Compose + Django guide linked above.