Skip to main contentSkip to navigation
[email protected]
Client AreaSupport
Hosting Mammoth
HostingMammothYour Data, Our Responsibility
Home
Solutions
Hosting Services
Store
Pricing
About
Blog
API
Contact

Stay Ahead of the Curve

Get the latest insights on cybersecurity, AI innovations, and enterprise data solutions delivered to your inbox.

Hosting Mammoth
HostingMammothEnterprise Solutions

Enterprise-grade data solutions. Hosting, recovery, cybersecurity, and AI-powered services for businesses worldwide.

[email protected]
Sun - Fri, 9:00am - 5:00pm

Services

  • Cloud Hosting
  • Data Recovery
  • Cybersecurity
  • Legal Support
  • MSP Services
  • Web Development
  • AI Services
  • Free Server Migration

Hosting

  • VPS Hosting (NVMe SSD)
  • VDS Hosting (NVMe)
  • Storage VPS (High SSD)
  • GPU Servers
  • Managed Services
  • Cloud Firewall
  • Load Balancer
  • One-Click Apps
  • n8n Hosting
  • Object Storage
  • FAQ

Company

  • Store
  • Pricing
  • About Us
  • Locations
  • Blog
  • Testimonials
  • Contact
  • Affiliate Program
  • White-Label
  • Terms of Service
  • Privacy Policy
  • Browser Cookies
  • SLA

Support

  • Client Area
  • Submit Ticket
  • Knowledge Base
  • Server Status
  • API Documentation

© 2026 Hosting Mammoth. All rights reserved.

Knowledge Base
Getting StartedAccount ManagementVPS HostingGPU ServersStorage VPSCloud FirewallLoad BalancerServer ManagementBilling & PaymentsSupport & TicketsAffiliate ProgramReseller ProgramMarketplace & Appsn8n HostingManaged ServicesServer MigrationAPI & DevelopersSecurityTroubleshootingGlossaryInstall Guides
  1. Home
  2. /
  3. Support
  4. /
  5. Install Guides
  6. /
  7. How To Install Django Ubuntu
GUIDEInstall Guides

How to Deploy Django with Gunicorn + Nginx on Ubuntu 24.04 VPS

21 min read

How to Deploy Django with Gunicorn + Nginx + PostgreSQL on Ubuntu 24.04 VPS

Django is one of the most productive web frameworks available, but getting it into production requires stitching together a handful of moving parts: a WSGI server that can actually handle traffic, a reverse proxy that serves static files and terminates TLS, a real database instead of the default SQLite file, and a process manager that keeps everything running after reboots. This guide walks you through the full production deployment on a fresh Ubuntu 24.04 VPS — from creating a dedicated deploy user to serving your site over HTTPS with automated Let's Encrypt renewal.

Prefer a faster path? Spin up a CloudCore Starter VPS in under a minute and follow this guide from a clean baseline. Plans start at EUR 7.99/month with unmetered bandwidth.

Table of Contents

  • What You Are Building
  • Prerequisites
  • Step 1: Update the System and Create a Deploy User
  • Step 2: Install PostgreSQL 16 and Create the Database
  • Step 3: Install Python 3.12 and Create a Virtual Environment
  • Step 4: Install Django, psycopg, and Gunicorn
  • Step 5: Create or Clone Your Django Project
  • Step 6: Configure settings.py for Production
  • Step 7: Collect Static Files and Run Migrations
  • Step 8: Gunicorn systemd Socket and Service
  • Step 9: Configure Nginx as a Reverse Proxy
  • Step 10: Obtain a TLS Certificate with Certbot
  • Step 11: Manage Secrets with an EnvironmentFile
  • Optional: Add Celery + Redis for Background Tasks
  • Deploy Flow for Future Updates
  • Troubleshooting
  • FAQ
  • Next Steps
  • What You Are Building

    By the end of this tutorial you will have a production-grade Django stack running on a single Ubuntu 24.04 VPS with the following layout:

    • Nginx listening on ports 80 and 443, terminating TLS and serving /static/ and /media/ files directly from disk
    • Gunicorn running as a systemd service, bound to a Unix socket at /run/gunicorn.sock, socket-activated so it starts on first request after a reboot
    • PostgreSQL 16 listening only on 127.0.0.1, with a dedicated role and database for your project
    • Django installed inside a Python 3.12 virtual environment under a non-privileged django user, reading its secrets from a systemd EnvironmentFile
    • Certbot with auto-renewal via systemd timer
    This layout is the same one used by most commercial Django hosts and is the recommended deployment pattern in the official Django deployment documentation. It separates concerns cleanly: Nginx handles TLS and static files, Gunicorn handles Python execution, PostgreSQL handles data, and systemd supervises the lot.

    Prerequisites

    Before you begin, make sure you have:

    • A VPS running Ubuntu 24.04 LTS with root or sudo access
    • SSH access to your server
    • A domain name pointing an A record at your server's public IP (required for TLS in Step 10)
    • At least 1 GB of RAM (2 GB+ recommended once PostgreSQL and Gunicorn workers are running)
    • At least 20 GB of disk space
    Recommended Plan: CloudCore Starter
    >
    For a single Django site with moderate traffic, we recommend the CloudCore Starter plan:
    >
    - 2 vCPU cores
    - 4 GB RAM
    - 50 GB NVMe SSD
    - Unmetered bandwidth
    - EUR 7.99/month
    >
    This is enough headroom for Django, PostgreSQL, Redis, and 3-4 Gunicorn workers on the same box.

    Connect to your server via SSH as root:

    bash
    ssh root@your-server-ip

    Step 1: Update the System and Create a Deploy User

    Never run Django (or any public-facing service) as root. Create a dedicated unprivileged user that will own the project directory and run Gunicorn.

    Start with a full package refresh:

    bash
    apt update && apt upgrade -y

    If the kernel was updated, reboot and reconnect:

    bash
    reboot

    Create the deploy user and add it to the sudo group (for administrative tasks during setup — you can remove it later):

    bash
    adduser --disabled-password --gecos "" django
    usermod -aG sudo django

    Copy your SSH keys so you can log in as the new user:

    bash
    rsync --archive --chown=django:django ~/.ssh /home/django

    Open a new SSH session as the django user to confirm it works:

    bash
    ssh django@your-server-ip

    From here on, all commands assume you are logged in as django and will use sudo where privilege escalation is required.

    Install the baseline build dependencies Python will need to compile packages with C extensions (psycopg, Pillow, etc.):

    bash
    sudo apt install -y build-essential libpq-dev curl git pkg-config

    Step 2: Install PostgreSQL 16 and Create the Database

    Django's default SQLite backend is fine for development but breaks down under any real concurrency. For production, use PostgreSQL — the most mature and feature-rich option among the databases Django officially supports.

    For a full walkthrough with tuning guidance, see our dedicated How to Install PostgreSQL 16 on Ubuntu 24.04 guide. The short version is below.

    Install PostgreSQL 16 from the Ubuntu repositories:

    bash
    sudo apt install -y postgresql postgresql-contrib

    Confirm the service is running:

    bash
    sudo systemctl status postgresql

    Switch to the postgres OS user and open a psql shell:

    bash
    sudo -u postgres psql

    Inside psql, create a role and a database for your project. Replace strongpassword with a long random string — you will reference it later from the environment file.

    sql
    CREATE DATABASE mysite_prod;
    CREATE USER mysite_user WITH PASSWORD 'strongpassword';
    ALTER ROLE mysite_user SET client_encoding TO 'utf8';
    ALTER ROLE mysite_user SET default_transaction_isolation TO 'read committed';
    ALTER ROLE mysite_user SET timezone TO 'UTC';
    GRANT ALL PRIVILEGES ON DATABASE mysite_prod TO mysite_user;
    \c mysite_prod
    GRANT ALL ON SCHEMA public TO mysite_user;
    \q

    The ALTER ROLE calls set sensible per-connection defaults that Django expects. The final GRANT ALL ON SCHEMA public is required on PostgreSQL 15+ because the default privileges on the public schema were tightened in that release — without it, migrate will fail with a permission error.

    By default PostgreSQL listens only on 127.0.0.1, which is exactly what we want. Do not open port 5432 to the internet.

    Step 3: Install Python 3.12 and Create a Virtual Environment

    Ubuntu 24.04 ships with Python 3.12 as its system Python, which is supported by Django 5.0 and 5.1. If you need a different version, see our How to Install Python on Ubuntu 24.04 guide for deadsnakes PPA instructions.

    Install the venv and pip packages:

    bash
    sudo apt install -y python3-pip python3-venv python3-dev

    Create a directory for your project and a virtual environment inside it:

    bash
    mkdir -p ~/apps/mysite
    cd ~/apps/mysite
    python3 -m venv venv

    Activate the environment:

    bash
    source venv/bin/activate

    Your shell prompt should now be prefixed with (venv). Upgrade pip inside the venv:

    bash
    pip install --upgrade pip setuptools wheel

    Everything installed from here while the venv is active stays isolated inside ~/apps/mysite/venv/ and will not conflict with system Python packages.

    Step 4: Install Django, psycopg, and Gunicorn

    With the venv active, install the three packages that form the core of the stack:

    bash
    pip install "Django>=5.0,<6.0" "psycopg[binary]>=3.1" gunicorn python-decouple

    A note on each:

    • Django — the framework itself. We pin to the 5.x line; pick the version that matches your project.
    • psycopg[binary] — the PostgreSQL driver. The [binary] extra pulls a prebuilt wheel with libpq embedded, which avoids the need for the libpq-dev headers at runtime. psycopg (v3) is the modern replacement for psycopg2; Django 4.2+ supports both and the official recommendation is to use v3 for new projects.
    • gunicorn — the WSGI server. See gunicorn.org for configuration reference.
    • python-decouple — optional, for reading .env files. You can substitute django-environ or plain os.environ if you prefer.
    Freeze the exact versions into a requirements.txt so the server can be rebuilt deterministically:

    bash
    pip freeze > requirements.txt

    Step 5: Create or Clone Your Django Project

    If you are starting fresh, scaffold a new project inside the current directory:

    bash
    django-admin startproject mysite .

    The trailing dot puts manage.py at ~/apps/mysite/manage.py and the settings module at ~/apps/mysite/mysite/settings.py.

    If you already have a project in a Git repository, clone it instead:

    bash
    cd ~/apps
    git clone https://github.com/yourorg/mysite.git
    cd mysite
    python3 -m venv venv
    source venv/bin/activate
    pip install -r requirements.txt

    Verify the project boots with the development server before moving on:

    bash
    python manage.py runserver 0.0.0.0:8000

    Open http://your-server-ip:8000 (you may need to temporarily allow port 8000 through UFW). You should see the Django welcome page or your own project's homepage. Stop the dev server with Ctrl+C — you will never use it again in production.

    Step 6: Configure settings.py for Production

    Open mysite/settings.py and make the following changes. This is the single most important step for a secure deployment.

    python
    import os
    from pathlib import Path
    from decouple import config, Csv

    BASE_DIR = Path(__file__).resolve().parent.parent

    SECURITY --------------------------------------------------------------

    SECRET_KEY = config("DJANGO_SECRET_KEY") DEBUG = config("DJANGO_DEBUG", default=False, cast=bool) ALLOWED_HOSTS = config("DJANGO_ALLOWED_HOSTS", cast=Csv())

    Database --------------------------------------------------------------

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

    Static and media ------------------------------------------------------

    STATIC_URL = "/static/" STATIC_ROOT = BASE_DIR / "staticfiles" STATICFILES_DIRS = [BASE_DIR / "static"]

    MEDIA_URL = "/media/" MEDIA_ROOT = BASE_DIR / "media"

    TLS and proxy headers -------------------------------------------------

    SECURE_PROXY_SSL_HEADER = ("HTTP_X_FORWARDED_PROTO", "https") SECURE_SSL_REDIRECT = not DEBUG SESSION_COOKIE_SECURE = True CSRF_COOKIE_SECURE = True SECURE_HSTS_SECONDS = 31536000 # 1 year SECURE_HSTS_INCLUDE_SUBDOMAINS = True SECURE_HSTS_PRELOAD = True SECURE_CONTENT_TYPE_NOSNIFF = True SECURE_REFERRER_POLICY = "same-origin" X_FRAME_OPTIONS = "DENY"

    CSRF_TRUSTED_ORIGINS = config( "DJANGO_CSRF_TRUSTED_ORIGINS", default="", cast=Csv(), )

    Things worth noting:

    • SECRET_KEY and database credentials come from the environment — never commit them. Step 11 wires these up via a systemd EnvironmentFile.
    • ALLOWED_HOSTS must contain every hostname that will reach Django, including your bare domain and the www. variant if you use it. Leaving this empty or wildcarded is a common cause of DisallowedHost 400 errors in production.
    • SECURE_PROXY_SSL_HEADER tells Django to trust the X-Forwarded-Proto header that Nginx sets, so request.is_secure() returns True and SECURE_SSL_REDIRECT does not create a redirect loop.
    • STATIC_ROOT is the directory collectstatic writes to. Nginx will serve from here directly — Django itself never handles static files in production.
    • HSTS settings are aggressive: once a browser sees HSTS with a long max-age, it will refuse to connect to your domain over HTTP even if you later disable TLS. Only enable these after you have confirmed HTTPS works end-to-end in Step 10.
    Validate the configuration:

    bash
    python manage.py check --deploy

    Django will list any remaining deployment warnings — fix them before going live.

    Step 7: Collect Static Files and Run Migrations

    With settings in place, create the database schema and gather every static asset into STATIC_ROOT:

    bash
    python manage.py migrate
    python manage.py collectstatic --noinput

    Expected output from collectstatic:

    text
    176 static files copied to '/home/django/apps/mysite/staticfiles'.

    Create a superuser so you can log into the admin:

    bash
    python manage.py createsuperuser

    Finally, test Gunicorn by hand before wrapping it in systemd:

    bash
    gunicorn --bind 0.0.0.0:8000 mysite.wsgi:application

    Visit http://your-server-ip:8000 in a browser. The page should render, but /static/ assets will 404 because Django is not serving them and Nginx is not yet in the picture. That is expected — stop Gunicorn with Ctrl+C and move on.

    Step 8: Gunicorn systemd Socket and Service

    systemd socket activation lets the kernel hold the listening socket open and spawn Gunicorn on demand. The first request after boot triggers the service start; there is no race between Nginx and Gunicorn coming up.

    Create the socket unit:

    bash
    sudo tee /etc/systemd/system/gunicorn.socket > /dev/null <<'EOF'
    [Unit]
    Description=gunicorn socket

    [Socket] ListenStream=/run/gunicorn.sock SocketUser=django SocketGroup=www-data SocketMode=0660

    [Install] WantedBy=sockets.target EOF

    Create the service unit:

    bash
    sudo tee /etc/systemd/system/gunicorn.service > /dev/null <<'EOF'
    [Unit]
    Description=gunicorn daemon for mysite
    Requires=gunicorn.socket
    After=network.target postgresql.service

    [Service] Type=notify User=django Group=www-data RuntimeDirectory=gunicorn WorkingDirectory=/home/django/apps/mysite EnvironmentFile=/etc/mysite/mysite.env ExecStart=/home/django/apps/mysite/venv/bin/gunicorn \ --access-logfile - \ --error-logfile - \ --workers 3 \ --worker-class sync \ --timeout 60 \ --bind unix:/run/gunicorn.sock \ mysite.wsgi:application ExecReload=/bin/kill -s HUP $MAINPID KillMode=mixed TimeoutStopSec=5 PrivateTmp=true Restart=on-failure

    [Install] WantedBy=multi-user.target EOF

    Key details:

    • User=django, Group=www-data — Gunicorn runs as the unprivileged deploy user, but the socket is group-owned by www-data (the group Nginx runs under), so Nginx can write to it without Django needing to be in Nginx's group.
    • SocketMode=0660 — owner and group can read/write, world has no access.
    • --workers 3 — a common starting point is (2 * cores) + 1. On a 2 vCPU VPS, 3-5 workers is reasonable. Each worker is a separate process holding its own copy of Django in memory (roughly 80-150 MB).
    • Type=notify — Gunicorn 20+ emits sd_notify messages so systemd knows exactly when it has finished loading.
    Enable and start both units:

    bash
    sudo systemctl daemon-reload
    sudo systemctl enable --now gunicorn.socket
    sudo systemctl status gunicorn.socket

    Expected output:

    text
    ● gunicorn.socket - gunicorn socket
         Loaded: loaded (/etc/systemd/system/gunicorn.socket; enabled; preset: enabled)
         Active: active (listening) since Wed 2026-04-16 10:00:00 UTC; 3s ago
       Listen: /run/gunicorn.sock (Stream)

    The service itself will not start until something actually connects to the socket. Trigger it manually with curl over the Unix socket:

    bash
    curl --unix-socket /run/gunicorn.sock http://localhost/

    You should see Django's HTML response. Confirm the service is now active:

    bash
    sudo systemctl status gunicorn.service

    Step 9: Configure Nginx as a Reverse Proxy

    Install Nginx (see our full How to Install Nginx on Ubuntu 24.04 guide for tuning):

    bash
    sudo apt install -y nginx

    Create a site config:

    bash
    sudo tee /etc/nginx/sites-available/mysite > /dev/null <<'EOF'
    upstream django_app {
        server unix:/run/gunicorn.sock;
    }

    server { listen 80; server_name example.com www.example.com;

    client_max_body_size 25m; access_log /var/log/nginx/mysite.access.log; error_log /var/log/nginx/mysite.error.log;

    location = /favicon.ico { access_log off; log_not_found off; }

    location /static/ { alias /home/django/apps/mysite/staticfiles/; expires 30d; add_header Cache-Control "public, immutable"; }

    location /media/ { alias /home/django/apps/mysite/media/; expires 7d; }

    location / { proxy_pass http://django_app; 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_redirect off; proxy_read_timeout 60s; } } EOF

    Nginx needs to read files inside /home/django/, so grant the www-data group execute permission on the path:

    bash
    sudo chmod o+x /home/django /home/django/apps /home/django/apps/mysite

    Enable the site, test the config, and reload:

    bash
    sudo ln -s /etc/nginx/sites-available/mysite /etc/nginx/sites-enabled/
    sudo rm -f /etc/nginx/sites-enabled/default
    sudo nginx -t
    sudo systemctl reload nginx

    Open http://example.com (substituting your domain). You should see your site with static files loading correctly.

    Open the firewall for HTTP and HTTPS:

    bash
    sudo ufw allow "Nginx Full"
    sudo ufw allow OpenSSH
    sudo ufw enable

    Step 10: Obtain a TLS Certificate with Certbot

    Install Certbot and its Nginx plugin:

    bash
    sudo apt install -y certbot python3-certbot-nginx

    Request a certificate — Certbot reads your Nginx config, provisions from Let's Encrypt, and rewrites the server block to listen on 443 with HTTP to HTTPS redirect:

    bash
    sudo certbot --nginx -d example.com -d www.example.com

    Answer the prompts (email address, agree to TOS, choose redirect). When it finishes, confirm automatic renewal is scheduled:

    bash
    sudo systemctl list-timers | grep certbot

    Certbot installs a systemd timer that runs twice daily and renews any certificate within 30 days of expiry. You do not need to do anything else.

    Test the full TLS setup with the SSL Labs server test or a quick curl:

    bash
    curl -I https://example.com

    Look for HTTP/2 200 and an Strict-Transport-Security header confirming HSTS is active.

    Step 11: Manage Secrets with an EnvironmentFile

    Your Gunicorn service references /etc/mysite/mysite.env — create it now.

    bash
    sudo mkdir -p /etc/mysite
    sudo tee /etc/mysite/mysite.env > /dev/null <<'EOF'
    DJANGO_SECRET_KEY=replace-with-a-50-character-random-string
    DJANGO_DEBUG=False
    DJANGO_ALLOWED_HOSTS=example.com,www.example.com
    DJANGO_CSRF_TRUSTED_ORIGINS=https://example.com,https://www.example.com
    DB_NAME=mysite_prod
    DB_USER=mysite_user
    DB_PASSWORD=strongpassword
    DB_HOST=127.0.0.1
    DB_PORT=5432
    EOF

    sudo chown root:django /etc/mysite/mysite.env sudo chmod 640 /etc/mysite/mysite.env

    Only root can write the file; the django group (and therefore the Gunicorn process) can read it but no one else can. Generate a fresh secret key with:

    bash
    python -c "import secrets; print(secrets.token_urlsafe(64))"

    Restart Gunicorn to pick up the variables:

    bash
    sudo systemctl restart gunicorn.service

    If you prefer python-decouple to read a .env file directly, drop the file at ~/apps/mysite/.env with the same contents and remove the EnvironmentFile= line from the systemd unit. The EnvironmentFile approach is preferred because it keeps secrets out of the project directory entirely.

    Optional: Add Celery + Redis for Background Tasks

    Any non-trivial Django site eventually needs asynchronous work: sending transactional email, generating PDFs, calling slow third-party APIs. The standard Django pattern is Celery with Redis as the broker.

    See our How to Install Redis on Ubuntu 24.04 guide for the full Redis setup. The short version:

    bash
    sudo apt install -y redis-server
    sudo systemctl enable --now redis-server

    Install Celery and the Redis client in your venv:

    bash
    source ~/apps/mysite/venv/bin/activate
    pip install celery[redis]
    pip freeze > requirements.txt

    Add a mysite/celery.py:

    python
    import os
    from celery import Celery

    os.environ.setdefault("DJANGO_SETTINGS_MODULE", "mysite.settings") app = Celery("mysite") app.config_from_object("django.conf:settings", namespace="CELERY") app.autodiscover_tasks()

    And import it from mysite/__init__.py:

    python
    from .celery import app as celery_app
    __all__ = ("celery_app",)

    Append to settings.py:

    python
    CELERY_BROKER_URL = "redis://127.0.0.1:6379/0"
    CELERY_RESULT_BACKEND = "redis://127.0.0.1:6379/1"
    CELERY_TASK_SERIALIZER = "json"
    CELERY_ACCEPT_CONTENT = ["json"]
    CELERY_TIMEZONE = "UTC"

    Create a systemd service for the Celery worker at /etc/systemd/system/celery.service, modeled on the Gunicorn unit but with ExecStart=/home/django/apps/mysite/venv/bin/celery -A mysite worker --loglevel=info. For periodic tasks, add a matching celerybeat.service that runs celery -A mysite beat.

    Deploy Flow for Future Updates

    With everything in place, the standard deploy loop for code changes becomes:

    bash
    cd ~/apps/mysite
    source venv/bin/activate
    git pull
    pip install -r requirements.txt
    python manage.py migrate
    python manage.py collectstatic --noinput
    sudo systemctl restart gunicorn.service
    sudo systemctl restart celery.service   # if using Celery

    Script this as ~/apps/mysite/deploy.sh so future deploys are a one-liner. For zero-downtime deploys, look at Gunicorn's --reload flag during development, or graceful restarts via sudo systemctl reload gunicorn.service which sends HUP to the master and cycles workers one at a time.

    Troubleshooting

    ProblemCauseSolution
    502 Bad Gateway from NginxGunicorn not running or socket permissions wrongsudo systemctl status gunicorn.service, check /var/log/nginx/mysite.error.log for permission denied, verify /run/gunicorn.sock is owned by django:www-data with mode 0660
    500 Internal Server Error with no detailDEBUG=False hides the tracebacksudo journalctl -u gunicorn -n 100 --no-pager to see the Python traceback from Gunicorn's stderr
    Static files return 404collectstatic not run, wrong STATIC_ROOT, or Nginx cannot read the pathRun python manage.py collectstatic, confirm alias in Nginx matches STATIC_ROOT exactly (trailing slash matters), verify chmod o+x on parent directories
    DisallowedHost at /Hostname not in ALLOWED_HOSTSAdd every hostname (bare domain, www., server IP if used directly) to DJANGO_ALLOWED_HOSTS in the env file, restart Gunicorn
    FATAL: password authentication failed for user "mysite_user"Password mismatch between PG and env file, or pg_hba.conf set to peerConfirm password with psql -U mysite_user -h 127.0.0.1 -d mysite_prod, check /etc/postgresql/16/main/pg_hba.conf has host all all 127.0.0.1/32 scram-sha-256
    permission denied for schema public during migratePostgreSQL 15+ tightened public schema privilegesReconnect as postgres, run GRANT ALL ON SCHEMA public TO mysite_user; inside the target database
    CSRF verification failed on POSTMissing CSRF_TRUSTED_ORIGINS or mismatched schemeAdd https://example.com to DJANGO_CSRF_TRUSTED_ORIGINS, confirm SECURE_PROXY_SSL_HEADER is set so Django knows the request is HTTPS
    Gunicorn socket file missing after rebootSocket unit not enabledsudo systemctl enable gunicorn.socket so it starts at boot
    Infinite redirect loop on HTTPSSECURE_SSL_REDIRECT=True without SECURE_PROXY_SSL_HEADERAdd the SECURE_PROXY_SSL_HEADER setting so Django recognizes Nginx's X-Forwarded-Proto: https
    High memory usage / OOM killsToo many Gunicorn workers for available RAMReduce --workers in the systemd unit; each worker is ~100 MB. On 2 GB RAM, 2-3 workers is the ceiling with PostgreSQL on the same box

    Viewing Logs

    Three log streams matter:

    bash
    # Gunicorn stdout/stderr (includes Django tracebacks when DEBUG=False)
    sudo journalctl -u gunicorn -f

    Nginx access and error logs

    sudo tail -f /var/log/nginx/mysite.access.log /var/log/nginx/mysite.error.log

    PostgreSQL

    sudo tail -f /var/log/postgresql/postgresql-16-main.log

    FAQ

    Why Gunicorn instead of uWSGI or Uvicorn?

    Gunicorn is the most widely documented WSGI server in the Django ecosystem — the official Django tutorial and most hosting providers use it as the default. It handles synchronous Django views efficiently and is trivial to configure. uWSGI is more featureful but notoriously harder to tune and its upstream is effectively unmaintained as of 2024. Uvicorn is an ASGI server and is the right choice if you are running Django Channels, async views exclusively, or serving WebSockets — for traditional sync Django, Gunicorn is simpler and faster.

    Should I run PostgreSQL on the same VPS?

    For sites under a few thousand daily active users, yes — co-locating Django and PostgreSQL on one box eliminates network latency on every query and keeps the architecture simple. The CloudCore Starter plan at 4 GB RAM comfortably runs both. Move PostgreSQL to a separate server (or managed service) once you need read replicas, when your database exceeds 20-30 GB, or when your application server can no longer fit the working set in RAM alongside Postgres.

    Do I need Nginx if Gunicorn can serve HTTP directly?

    Yes. Gunicorn is a WSGI server, not a production HTTP server. It does not serve static files efficiently, does not terminate TLS, has weak handling of slow clients (a classic slowloris vulnerability), and has no built-in caching or compression. Nginx exists in front of every serious Django deployment for exactly these reasons.

    How many workers should I run?

    The Gunicorn docs recommend (2 * CPU cores) + 1 as a starting point for CPU-bound sync workloads. On a 2 vCPU VPS that gives you 5 workers. In practice, the ceiling on small VPS plans is usually memory, not CPU — each worker consumes 80-150 MB of RAM depending on your dependencies. Measure with ps aux | grep gunicorn after the site has been running under load, and reduce --workers if you approach swap.

    Can I deploy multiple Django sites on the same server?

    Yes, and it is a common pattern. Create a separate deploy user, virtualenv, systemd socket+service pair, and Nginx server block for each site. Name the socket files uniquely (/run/gunicorn-site1.sock, /run/gunicorn-site2.sock). PostgreSQL happily hosts dozens of small databases in a single cluster — just create a dedicated role and database for each site.

    What about Docker / Kubernetes?

    Containerizing Django is absolutely viable and this guide translates directly — the Dockerfile installs Python and pip-installs your requirements.txt, and docker compose replaces the systemd units. For a single-server deployment, however, bare systemd is usually simpler to operate, uses less RAM, and avoids the complexity of container networking for static file serving. Reach for Docker when you need repeatable environments across dev/staging/prod or when you are about to move to multiple hosts.

    Next Steps

    You now have a production Django stack on Ubuntu 24.04. Good follow-ups:

    • Tune PostgreSQL — the default postgresql.conf is extremely conservative. Run pgtune against your VPS specs and apply the recommended shared_buffers, effective_cache_size, and work_mem values.
    • Set up automated backups — use pg_dump on a nightly cron, or pgBackRest / barman for point-in-time recovery. Pipe the dumps to object storage off-server.
    • Add application monitoring — install Sentry's self-hosted stack or hook into the managed SaaS for exception tracking with the sentry-sdk Django integration.
    • Rate-limit at Nginx — use limit_req_zone to cap abusive clients before they reach Gunicorn.
    • Add a CDN in front — Cloudflare's free tier pairs well with a small Django VPS, offloading static asset delivery and absorbing DDoS traffic.

    Ready to deploy?
    >
    Launch a CloudCore Starter VPS — 2 vCPU, 4 GB RAM, 50 GB NVMe, unmetered bandwidth, Ubuntu 24.04 preinstalled — and follow this guide start to finish in about 40 minutes.
    >
    - EUR 7.99/month, billed monthly with no contract
    - Instant provisioning, root access within 60 seconds
    - 24/7 support if you get stuck on any step above
    >
    Deploy Your Django VPS Now

    Was this article helpful?

    ← Back to Install GuidesBrowse all categories →

    Still have questions?

    Contact Support →Submit a Ticket