Python Development

Django Production Deployment Checklist (Gunicorn + nginx + PostgreSQL)

By Domain India Team · DomainIndia SupportPublished 8 min read
Knowledge base article
Contents (12 sections)

Running Django in production means a long-running Python process behind a web server, not the runserver command you use while developing. The usual stack on your own server is Gunicorn for the app, nginx in front, PostgreSQL for data and systemd to keep it all running. This checklist walks through that setup on a Linux VPS, the settings to harden before launch, and the mistakes that most often take Django sites down.

Key takeaways

On a VPS: create a non-root user, a PostgreSQL database and a virtual environment; set DEBUG = False, ALLOWED_HOSTS and a secret key from the environment; run python manage.py check --deploy; serve the app with Gunicorn under systemd, bound to 127.0.0.1; put nginx in front for static files and HTTPS with Let's Encrypt. Use Django 5.2 LTS for new projects. A small Django site can also run on Domain India cPanel or DirectAdmin hosting through the Python app tool, and the App Platform runs Django from a Dockerfile.

1. Choose where Django runs

Django doesn't need a VPS in every case. Pick by what the project needs:

WhereGood forLimits
Shared hosting (cPanel or DirectAdmin Python app tool)Small Django sites next to an existing websiteWSGI only, shared CPU and memory, no background workers, no root
App PlatformProduction apps without server adminYou provide a Dockerfile; no SSH or root
VPSFull control: Gunicorn, Celery, Channels, any system packageYou run and secure the server yourself

For the shared hosting route, follow how to deploy a Python app on shared hosting. The rest of this guide is for a VPS or server where you have root.

2. Prepare the server

Use a current long-term-support release such as Ubuntu 24.04 LTS, Debian 12 or AlmaLinux 9, with Python 3.12 or later. On Ubuntu or Debian:

bash
sudo apt update
sudo apt install python3 python3-venv python3-pip \
                 postgresql nginx git \
                 certbot python3-certbot-nginx

On AlmaLinux use dnf with the equivalent packages. Then create an unprivileged user for the app. Never run Django as root:

bash
sudo adduser --system --group --home /home/django --shell /bin/bash django

3. Create the database

bash
sudo -u postgres psql <<'EOF'
CREATE USER myapp WITH PASSWORD 'use-a-long-random-password';
CREATE DATABASE myapp_production OWNER myapp;
EOF

Making the app user the database owner gives it the rights it needs on PostgreSQL 15 and later, where ordinary users can no longer create tables in the public schema by default. Keep PostgreSQL listening on localhost only.

4. Code, virtual environment and requirements

As the django user, clone your project and create a virtual environment:

bash
sudo -iu django
git clone https://github.com/you/myapp.git
cd myapp
python3 -m venv venv
source venv/bin/activate
pip install -r requirements.txt

A minimal requirements.txt for production:

text
Django>=5.2,<5.3
gunicorn
psycopg[binary]
whitenoise
python-decouple

Django 5.2 is the current long-term-support release, with security fixes until April 2028. Django 4.2 LTS reached end of support in April 2026, so upgrade any project still on it.

5. Harden settings.py

The default settings.py is for development. Read secrets from the environment (here with python-decouple, which reads a .env file) and never commit them.

python
from decouple import config, Csv

DEBUG = config("DEBUG", default=False, cast=bool)
SECRET_KEY = config("SECRET_KEY")
ALLOWED_HOSTS = config("ALLOWED_HOSTS", cast=Csv())
CSRF_TRUSTED_ORIGINS = ["https://yourdomain.com", "https://www.yourdomain.com"]

DATABASES = {
    "default": {
        "ENGINE": "django.db.backends.postgresql",
        "NAME": config("DB_NAME"),
        "USER": config("DB_USER"),
        "PASSWORD": config("DB_PASSWORD"),
        "HOST": config("DB_HOST", default="localhost"),
        "PORT": config("DB_PORT", default="5432"),
        "CONN_MAX_AGE": 60,
        "CONN_HEALTH_CHECKS": True,
    }
}

SECURE_PROXY_SSL_HEADER = ("HTTP_X_FORWARDED_PROTO", "https")
SECURE_SSL_REDIRECT = True
SESSION_COOKIE_SECURE = True
CSRF_COOKIE_SECURE = True
SECURE_HSTS_SECONDS = 3600  # raise to 31536000 once HTTPS is proven
SECURE_CONTENT_TYPE_NOSNIFF = True
X_FRAME_OPTIONS = "DENY"

STATIC_URL = "/static/"
STATIC_ROOT = BASE_DIR / "staticfiles"
STORAGES = {
    "default": {"BACKEND": "django.core.files.storage.FileSystemStorage"},
    "staticfiles": {"BACKEND": "whitenoise.storage.CompressedManifestStaticFilesStorage"},
}

Generate a fresh secret key for production and put it, with the other values, in /home/django/myapp/.env, readable only by the django user:

bash
python -c "from django.core.management.utils import get_random_secret_key; print(get_random_secret_key())"

If you use WhiteNoise, add whitenoise.middleware.WhiteNoiseMiddleware directly after SecurityMiddleware in MIDDLEWARE. The old STATICFILES_STORAGE setting was removed in Django 5.1; use STORAGES as above.

Turn on HSTS carefully

HSTS tells browsers to refuse plain HTTP for your domain for the whole period you set. Start with a short value, confirm every page and subdomain works over HTTPS, then raise it to a year. Add SECURE_HSTS_INCLUDE_SUBDOMAINS and SECURE_HSTS_PRELOAD only when every subdomain has a valid certificate, because preloading is hard to undo. See security headers explained.

6. Migrate, collect static files and run the deploy check

With the virtual environment active:

bash
python manage.py check --deploy
python manage.py migrate
python manage.py collectstatic --noinput
python manage.py createsuperuser

check --deploy is Django's own pre-launch audit. Fix every warning it prints before you continue.

7. Run Gunicorn under systemd

Test Gunicorn by hand first: gunicorn myapp.wsgi --bind 127.0.0.1:8000. Then create /home/django/myapp/gunicorn.conf.py:

python
import multiprocessing

bind = "127.0.0.1:8000"
workers = multiprocessing.cpu_count() * 2 + 1
timeout = 60
max_requests = 1000
max_requests_jitter = 50
accesslog = "-"
errorlog = "-"

Size workers to your memory as well as your CPU: each worker holds a full copy of your app. Then create /etc/systemd/system/myapp.service as root:

ini
[Unit]
Description=Gunicorn for myapp
After=network.target postgresql.service

[Service]
User=django
Group=django
WorkingDirectory=/home/django/myapp
EnvironmentFile=/home/django/myapp/.env
ExecStart=/home/django/myapp/venv/bin/gunicorn --config gunicorn.conf.py myapp.wsgi:application
ExecReload=/bin/kill -s HUP $MAINPID
Restart=on-failure
RestartSec=5

[Install]
WantedBy=multi-user.target
bash
sudo systemctl daemon-reload
sudo systemctl enable --now myapp
sudo systemctl status myapp
journalctl -u myapp -f

8. nginx in front, then HTTPS

Create an nginx site for your domain (/etc/nginx/sites-available/myapp.conf on Ubuntu and Debian, /etc/nginx/conf.d/myapp.conf on AlmaLinux):

nginx
server {
    listen 80;
    server_name yourdomain.com www.yourdomain.com;
    client_max_body_size 20m;

    location /static/ {
        alias /home/django/myapp/staticfiles/;
        expires 30d;
    }

    location /media/ {
        alias /home/django/myapp/media/;
    }

    location / {
        proxy_pass http://127.0.0.1:8000;
        proxy_set_header Host $host;
        proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
        proxy_set_header X-Forwarded-Proto $scheme;
        proxy_read_timeout 60s;
    }
}

nginx must be able to read the static folder: give it execute permission on /home/django or move static files under /var/www. Test and reload with sudo nginx -t && sudo systemctl reload nginx, then get a certificate:

bash
sudo certbot --nginx -d yourdomain.com -d www.yourdomain.com
sudo certbot renew --dry-run

Point the domain's A record at the VPS before running Certbot. See how to change your domain's DNS settings.

9. Deploy updates

A simple deploy, run as the django user:

bash
cd ~/myapp
git pull
source venv/bin/activate
pip install -r requirements.txt
python manage.py migrate
python manage.py collectstatic --noinput
sudo systemctl reload myapp

The last line needs a sudo rule that lets the django user reload only this service. To automate the same steps from GitHub, adapt deploying with GitHub Actions to SSH into the VPS and reload systemd.

10. Pre-launch checklist

  1. Deploy check is clean.
    python manage.py check --deploy prints no warnings.
  2. Secrets are out of the code.
    DEBUG is off, and SECRET_KEY and database passwords live only in .env.
  3. Hosts are explicit.
    ALLOWED_HOSTS and CSRF_TRUSTED_ORIGINS list your real domains, never *.
  4. Gunicorn is private.
    It listens on 127.0.0.1 only, and the firewall allows just SSH, 80 and 443.
  5. HTTPS works.
    HTTP redirects to HTTPS and curl -I https://yourdomain.com shows the security headers.
  6. Static and media files load.
    No 404s for CSS, JavaScript or uploads.
  7. Backups run.
    Database dumps and the media folder are copied off the server; see automated backups with cron and rclone.
  8. Errors reach you.
    Logs are rotated and an error tracker such as Sentry, or at least email to ADMINS, is set up.

11. Common mistakes

DEBUG left on
Error pages expose settings, paths and stack traces to anyone.
ALLOWED_HOSTS set to star
Accepts any Host header and opens the door to host-header attacks.
Gunicorn on 0.0.0.0
The app is reachable directly, bypassing nginx and HTTPS.
Django serving static files
Slow and not meant for production; use nginx or WhiteNoise.
Skipping migrate or collectstatic
New columns are missing (500 errors) or CSS and JavaScript return 404.
Running as root, or with gunicorn &
A compromise gets root, and the app doesn't survive a crash or reboot.

12. Running Django on Domain India

VPS. Our VPS plans are self-managed with full root access, so everything in this guide applies. You install and patch the software and look after backups yourself.

VPS Starter
₹552.65/mo + GST
  • 1 vCPU
  • 2 GB DDR4 RAM
  • 64 GB NVMe SSD Storage
  • 2 TB Monthly Bandwidth
See plan details

App Platform. If you would rather not run a server, the App Platform runs Django from your own Dockerfile (only Node.js is detected automatically), deploys from GitHub or with a deploy token, and includes a PostgreSQL database. It does not support WebSockets, so Django Channels needs a VPS. See getting started with the App Platform.

App Starter
₹100/mo + GST
  • 512 MB RAM per app
  • 1 vCPU
  • 5 GB NVMe SSD
  • PostgreSQL Database
See plan details

Shared hosting. Our cPanel and DirectAdmin servers can run a small Django site as a WSGI app through the panel's Python app tool, with a MySQL-compatible database. Jailed SSH access is available on every shared hosting plan (cPanel, DirectAdmin, Webuzo). It is off by default; ask support to enable it for your account.

Can I use SQLite for Django in production?

For a small, mostly read-only site it can work. SQLite allows one writer at a time, so for anything with regular writes or several users at once, use PostgreSQL or MySQL.

Which Django version should I use in 2026?

Django 5.2, the current long-term-support release, which receives security fixes until April 2028. Django 4.2 reached end of support in April 2026.

Gunicorn or uWSGI?

Gunicorn is simpler and fits most Django apps. uWSGI offers more options, but most projects don't need them, and its development has slowed.

Do I need Docker to deploy Django?

No. systemd with a virtual environment is enough for one app on one server. Docker helps when you run several apps, need identical environments, or deploy to the App Platform, which builds from a Dockerfile.

How do I run Django with WebSockets?

Django Channels needs an ASGI server such as Daphne or Uvicorn behind nginx instead of plain Gunicorn. Run it on a VPS; the Domain India App Platform does not support WebSockets.

Can I run Django on Domain India shared hosting?

Yes, for small sites. Our cPanel and DirectAdmin servers have a Python app tool that serves Django as a WSGI app through a passenger_wsgi.py file. Background workers and WebSockets need a VPS.

How do I run scheduled Django commands?

On a VPS, use a cron job or a systemd timer that activates the virtual environment and runs python manage.py yourcommand. See the cron expression guide for the schedule syntax.

Ready to deploy? Compare VPS plans, try the App Platform, or read the Django, FastAPI and Flask comparison if you are still choosing a framework. Schedule jobs with the cron expression guide.

Need a server for your Django app?

Self-managed VPS plans with full root access, ready for Gunicorn, nginx and PostgreSQL.

See VPS plans

Was this article helpful?

Your answer helps us decide what to improve next.

Still need help? Open a support ticket and our team will reply.

Prefer an app? Add this site to your home screen.Get the app