Technology Encyclopedia Home >How to Deploy a Django App with Gunicorn and Nginx on a Cloud Server

How to Deploy a Django App with Gunicorn and Nginx on a Cloud Server

Django is one of those frameworks where local development feels very different from production. Locally: python manage.py runserver, and you're done. In production, that same command will get you in trouble — it's a development server, not designed to handle real traffic or run unattended.

I learned this the hard way on my first Django project. The app ran fine locally but the production setup took me a whole day to figure out. Gunicorn as the application server, Nginx as the reverse proxy, PostgreSQL instead of SQLite, and static files that actually get served — none of this is obvious from Django's documentation alone.

This guide pulls all of it together in one place: the complete production deployment of a Django app on Ubuntu 22.04.

I run this on Tencent Cloud Lighthouse. The 2 vCPU / 4 GB RAM plan handles Django comfortably for most projects. A few reasons I use Lighthouse for Django specifically: the snapshot feature lets me take a point-in-time backup of the entire server — app code, database, configuration — before a tricky migration, so I have a rollback option if things go wrong. The console-level firewall also makes it easy to lock down PostgreSQL to localhost only (not expose it to the internet) while still being able to access the Django admin via HTTPS. You can also upgrade specs from the control panel as traffic grows, without migrating to a new server.


Table of Contents

  1. The Django Production Stack
  2. Prerequisites
  3. Part 1 — Set Up the Server
  4. Part 2 — Install Python and PostgreSQL
  5. Part 3 — Deploy Your Django Project
  6. Part 4 — Configure Django for Production
  7. Part 5 — Set Up Gunicorn
  8. Part 6 — Configure Nginx
  9. Part 7 — Handle Static and Media Files
  10. Part 8 — Enable HTTPS
  11. The Gotcha: ALLOWED_HOSTS and 400 Bad Request
  12. Common Commands

Key Takeaways

  • Set DEBUG = False and configure ALLOWED_HOSTS before going live
  • Run collectstatic and configure Nginx to serve static files directly
  • Use python-dotenv or environment variables for SECRET_KEY and database credentials
  • Gunicorn runs Django workers; Nginx handles the public-facing HTTPS
  • Take a database backup before running manage.py migrate on production

The Django Production Stack {#stack}

Internet → Nginx → Gunicorn → Django app
                ↓
         Static/Media files (served directly by Nginx)
                ↓
         PostgreSQL database
Component Role
Nginx Serves static/media files, proxies app requests, handles SSL
Gunicorn WSGI server running Django workers
Django Your application
PostgreSQL Database (better suited to Django than SQLite for production)
systemd Process management

Prerequisites {#prerequisites}

Requirement Notes
Cloud server Tencent Cloud Lighthouse Ubuntu 22.04
A Django project We'll use an example project; adapt for your own
Domain name Optional for IP-only testing

Part 1 — Set Up the Server {#part-1}

ssh ubuntu@YOUR_SERVER_IP
sudo apt update && sudo apt upgrade -y

# Install dependencies
sudo apt install -y python3 python3-pip python3-venv \
  libpq-dev nginx git curl

# Firewall
sudo ufw allow ssh
sudo ufw allow 'Nginx Full'
sudo ufw enable

Part 2 — Install Python and PostgreSQL {#part-2}

PostgreSQL

sudo apt install -y postgresql postgresql-contrib

sudo systemctl start postgresql
sudo systemctl enable postgresql

Create a database and user for Django:

sudo -u postgres psql
CREATE DATABASE mydjangodb;
CREATE USER mydjango_user WITH PASSWORD 'choose_a_strong_password';

-- Django-recommended settings
ALTER ROLE mydjango_user SET client_encoding TO 'utf8';
ALTER ROLE mydjango_user SET default_transaction_isolation TO 'read committed';
ALTER ROLE mydjango_user SET timezone TO 'UTC';

GRANT ALL PRIVILEGES ON DATABASE mydjangodb TO mydjango_user;

\q

Part 3 — Deploy Your Django Project {#part-3}

mkdir -p ~/apps && cd ~/apps

# Clone your project or create one
git clone https://github.com/your-username/your-django-app.git myproject
cd myproject

# Create virtual environment
python3 -m venv venv
source venv/bin/activate

# Install dependencies
pip install django gunicorn psycopg2-binary whitenoise python-dotenv
# If you have a requirements.txt:
# pip install -r requirements.txt

Part 4 — Configure Django for Production {#part-4}

Create a .env file for sensitive settings:

nano .env
DEBUG=False
SECRET_KEY=generate-a-long-random-string-here
DATABASE_URL=postgresql://mydjango_user:your_password@localhost/mydjangodb
ALLOWED_HOSTS=yourdomain.com,www.yourdomain.com,YOUR_SERVER_IP

Update settings.py to read from environment variables:

import os
from dotenv import load_dotenv

load_dotenv()

SECRET_KEY = os.environ.get('SECRET_KEY')
DEBUG = os.environ.get('DEBUG', 'False') == 'True'
ALLOWED_HOSTS = os.environ.get('ALLOWED_HOSTS', '').split(',')

DATABASES = {
    'default': {
        'ENGINE': 'django.db.backends.postgresql',
        'NAME': 'mydjangodb',
        'USER': 'mydjango_user',
        'PASSWORD': os.environ.get('DB_PASSWORD'),
        'HOST': 'localhost',
        'PORT': '5432',
    }
}

# Static files
STATIC_ROOT = os.path.join(BASE_DIR, 'staticfiles')
STATIC_URL = '/static/'

# Whitenoise for serving static files efficiently
MIDDLEWARE = [
    'whitenoise.middleware.WhiteNoiseMiddleware',
    # ... your other middleware
]

Run database migrations and collect static files:

source venv/bin/activate
python manage.py migrate
python manage.py collectstatic --noinput
python manage.py createsuperuser  # Create admin user

Test it runs:

python manage.py runserver 0.0.0.0:8000
# Visit http://YOUR_SERVER_IP:8000 to verify, then Ctrl+C

Part 5 — Set Up Gunicorn {#part-5}

Test Gunicorn manually:

cd ~/apps/myproject
source venv/bin/activate

gunicorn --bind 0.0.0.0:8000 myproject.wsgi:application
# Replace "myproject" with your Django project name (the directory containing settings.py)

Visit http://YOUR_SERVER_IP:8000 to confirm it works, then stop with Ctrl+C.

Create a systemd service:

sudo nano /etc/systemd/system/gunicorn_myproject.service
[Unit]
Description=Gunicorn for Django myproject
After=network.target

[Service]
User=ubuntu
Group=www-data
WorkingDirectory=/home/ubuntu/apps/myproject
EnvironmentFile=/home/ubuntu/apps/myproject/.env
ExecStart=/home/ubuntu/apps/myproject/venv/bin/gunicorn \
          --workers 3 \
          --bind unix:/run/gunicorn_myproject.sock \
          --access-logfile /var/log/gunicorn_myproject_access.log \
          --error-logfile /var/log/gunicorn_myproject_error.log \
          myproject.wsgi:application

Restart=on-failure
RestartSec=5s

[Install]
WantedBy=multi-user.target
sudo systemctl daemon-reload
sudo systemctl start gunicorn_myproject
sudo systemctl enable gunicorn_myproject
sudo systemctl status gunicorn_myproject

# Verify socket created
ls /run/gunicorn_myproject.sock

Part 6 — Configure Nginx {#part-6}

sudo nano /etc/nginx/sites-available/myproject
server {
    listen 80;
    server_name yourdomain.com www.yourdomain.com;

    # Static files served directly by Nginx
    location /static/ {
        root /home/ubuntu/apps/myproject;
        expires 30d;
        add_header Cache-Control "public, no-transform";
    }

    # Media files
    location /media/ {
        root /home/ubuntu/apps/myproject;
    }

    # Django via Gunicorn
    location / {
        include proxy_params;
        proxy_pass http://unix:/run/gunicorn_myproject.sock;
    }

    access_log /var/log/nginx/myproject_access.log;
    error_log  /var/log/nginx/myproject_error.log;
}
sudo ln -s /etc/nginx/sites-available/myproject /etc/nginx/sites-enabled/
sudo nginx -t
sudo systemctl reload nginx

Part 7 — Handle Static and Media Files {#part-7}

In production, Django doesn't serve static files itself — Nginx does. Make sure:

  1. STATIC_ROOT is set in settings.py
  2. python manage.py collectstatic has been run (it copies all static files to STATIC_ROOT)
  3. The Nginx location /static/ block points to the right directory

The staticfiles/ directory (from collectstatic) is what Nginx serves.

For media files (user uploads):

# settings.py
MEDIA_ROOT = os.path.join(BASE_DIR, 'media')
MEDIA_URL = '/media/'

Set permissions:

sudo chown -R ubuntu:www-data ~/apps/myproject/media
sudo chmod -R 755 ~/apps/myproject/media

Part 8 — Enable HTTPS {#part-8}

sudo apt install -y certbot python3-certbot-nginx
sudo certbot --nginx -d yourdomain.com -d www.yourdomain.com

Also update Django's ALLOWED_HOSTS and CSRF_TRUSTED_ORIGINS:

CSRF_TRUSTED_ORIGINS = ['https://yourdomain.com', 'https://www.yourdomain.com']

Restart Gunicorn after any settings change:

sudo systemctl restart gunicorn_myproject

The Gotcha: ALLOWED_HOSTS and 400 Bad Request {#gotcha}

After going live, a common surprise: you visit your domain and get a 400 Bad Request from Django.

The cause is almost always ALLOWED_HOSTS. In production with DEBUG=False, Django rejects any request where the Host header doesn't match a value in ALLOWED_HOSTS.

# Wrong — too restrictive
ALLOWED_HOSTS = ['localhost']

# Correct — include your actual domain
ALLOWED_HOSTS = ['yourdomain.com', 'www.yourdomain.com']

# If you're testing with IP only
ALLOWED_HOSTS = ['yourdomain.com', 'www.yourdomain.com', 'YOUR_SERVER_IP']

After changing settings.py, restart Gunicorn:

sudo systemctl restart gunicorn_myproject

Common Commands {#commands}

# Restart app after code changes
sudo systemctl restart gunicorn_myproject

# Collect static files after frontend changes
source ~/apps/myproject/venv/bin/activate
cd ~/apps/myproject
python manage.py collectstatic --noinput

# Run database migrations
python manage.py migrate

# View app logs
sudo journalctl -u gunicorn_myproject -f
tail -f /var/log/gunicorn_myproject_error.log

# View Nginx logs
tail -f /var/log/nginx/myproject_error.log

Troubleshooting {#troubleshooting}

Issue Likely Cause Fix
Connection refused Service not running or wrong port Check systemctl status SERVICE and verify firewall rules
Permission denied Wrong file ownership or permissions Check file ownership with ls -la and use chown/chmod to fix
502 Bad Gateway Backend service not running Restart the backend service; check logs with journalctl -u SERVICE
SSL certificate error Certificate expired or domain mismatch Run sudo certbot renew and verify domain DNS points to server IP
Service not starting Config error or missing dependency Check logs with journalctl -u SERVICE -n 50 for specific error
Out of disk space Logs or data accumulation Run df -h to identify usage; clean logs or attach CBS storage
High memory usage Too many processes or memory leak Check with htop; consider upgrading instance plan if consistently high
Firewall blocking traffic Port not open in UFW or Lighthouse console Open port in Lighthouse console firewall AND sudo ufw allow PORT

Frequently Asked Questions {#faq}

What is the difference between Django development and production servers?
manage.py runserver is single-threaded, unoptimized for traffic, and not designed for public internet exposure. Production requires Gunicorn (WSGI server) + Nginx (reverse proxy) + a proper database like PostgreSQL.

Why use PostgreSQL instead of SQLite for production Django?
SQLite doesn't handle concurrent writes well and isn't designed for multi-user production environments. PostgreSQL is production-grade, handles concurrent access properly, and is what Django is optimized for.

How do I run Django database migrations on the server?
SSH into the server and run python manage.py migrate from your project directory with the virtual environment activated. Always take a database backup before running migrations.

What is ALLOWED_HOSTS in Django and how do I configure it?
ALLOWED_HOSTS is a security setting that specifies which hostnames Django will serve. Set it to your domain and server IP: ALLOWED_HOSTS = ['yourdomain.com', 'www.yourdomain.com', 'YOUR_SERVER_IP']. Django returns 400 for requests to unlisted hosts.

How do I serve Django static files in production?
Run python manage.py collectstatic to gather all static files into STATIC_ROOT. Configure Nginx to serve that directory directly (Nginx is much faster at serving static files than Python).

Deploy your Django app today:
👉 Tencent Cloud Lighthouse — Python/Django-ready Ubuntu VPS
👉 View current pricing and promotions
👉 Explore all active deals and offers