TutorialsAugust 4, 20264 min read

How to Deploy a Django App to a VPS

Deploy Django on Ubuntu 24.04 for production: Gunicorn, a systemd service, static files with collectstatic, PostgreSQL, and Nginx.

NBy Nxeon

Django ships with runserver, but that is for development only. A production Django deployment runs behind Gunicorn, serves static files properly, connects to a real database, and is fronted by Nginx with TLS. This guide takes a Django project from repo to live site on Ubuntu 24.04.

Prepare the server

Install Python tooling and create a virtualenv for the project:

sudo apt update
sudo apt install python3-venv python3-pip -y
cd ~/mydjangoapp
python3 -m venv venv
source venv/bin/activate
pip install -r requirements.txt gunicorn

If your project uses PostgreSQL (recommended), install it first following how to install PostgreSQL on a VPS, then pip install psycopg2-binary.

Configure Django for production

In settings.py, set the essentials:

DEBUG = False
ALLOWED_HOSTS = ["django.example.com", "127.0.0.1"]
STATIC_ROOT = BASE_DIR / "staticfiles"

Never run production with DEBUG = True — it leaks secrets and tracebacks. Keep your SECRET_KEY and database credentials in environment variables, not in the file.

Run migrations and collect static files:

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

collectstatic gathers CSS/JS into STATIC_ROOT so Nginx can serve them directly — Django should not serve static files in production.

The Nxeon game-server control panel — live console, player slots, and TPS
The Nxeon game-server control panel — live console, player slots, and TPS

Test with Gunicorn

Django exposes a WSGI application; point Gunicorn at it (replace myproject with your project package name):

gunicorn --workers 3 --bind 127.0.0.1:8000 myproject.wsgi:application

Check curl http://127.0.0.1:8000 responds, then stop it.

Create a systemd service

Create /etc/systemd/system/mydjangoapp.service:

[Unit]
Description=Gunicorn for mydjangoapp
After=network.target

[Service]
User=youruser
Group=www-data
WorkingDirectory=/home/youruser/mydjangoapp
EnvironmentFile=/home/youruser/mydjangoapp/.env
ExecStart=/home/youruser/mydjangoapp/venv/bin/gunicorn \
  --workers 3 --bind unix:/home/youruser/mydjangoapp/app.sock \
  myproject.wsgi:application

[Install]
WantedBy=multi-user.target

The EnvironmentFile loads your secrets from a .env. Enable and start:

sudo systemctl daemon-reload
sudo systemctl enable --now mydjangoapp
sudo systemctl status mydjangoapp

For a deeper look at each field, see creating a systemd service for your app.

Configure Nginx

Create /etc/nginx/sites-available/mydjangoapp, serving static files directly and proxying everything else to Gunicorn:

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

    location /static/ {
        alias /home/youruser/mydjangoapp/staticfiles/;
    }

    location / {
        include proxy_params;
        proxy_pass http://unix:/home/youruser/mydjangoapp/app.sock;
    }
}

Enable, test, reload:

sudo ln -s /etc/nginx/sites-available/mydjangoapp /etc/nginx/sites-enabled/
sudo nginx -t && sudo systemctl reload nginx

The pattern mirrors our Nginx reverse proxy guide with an extra static-file location.

Add HTTPS

sudo apt install certbot python3-certbot-nginx -y
sudo certbot --nginx -d django.example.com

Remember to add SECURE_PROXY_SSL_HEADER = ('HTTP_X_FORWARDED_PROTO', 'https') in settings so Django knows it is behind TLS. See the Certbot guide for renewal.

Lock down Django for production

Beyond DEBUG = False, a few settings meaningfully harden a live Django site. Add these once you are served over HTTPS:

SECURE_SSL_REDIRECT = True
SESSION_COOKIE_SECURE = True
CSRF_COOKIE_SECURE = True
SECURE_HSTS_SECONDS = 31536000
X_FRAME_OPTIONS = "DENY"

Django ships a checklist for exactly this — run it to catch anything you missed:

python manage.py check --deploy

It flags insecure settings with clear explanations, and it is worth a clean pass before launch.

Handle migrations on every deploy

Schema changes ship as migrations, so your deploy step must apply them. After pulling new code:

python manage.py migrate --noinput
python manage.py collectstatic --noinput
sudo systemctl restart mydjangoapp

Running migrate before restarting keeps the database in step with the code. For zero-downtime deploys, make migrations backwards-compatible so old and new code can both run against the schema briefly during the switch.

FAQ

Why doesn't my CSS load after deploying Django?

Django is not serving static files in production, and you likely skipped collectstatic or misconfigured the Nginx /static/ alias. Run collectstatic, set STATIC_ROOT, and point Nginx's alias at that folder.

Should I use PostgreSQL or SQLite for a Django site?

SQLite is fine for tiny, single-process sites, but PostgreSQL is the standard production choice for concurrency and reliability. See our PostgreSQL guide.

How do I run Celery or a background worker too?

Create a second systemd service for the Celery worker, pointing ExecStart at celery -A myproject worker, and a broker like Redis. Manage both units with systemctl.

My app returns 502 Bad Gateway — what now?

Nginx cannot reach Gunicorn. Check sudo systemctl status mydjangoapp and journalctl -u mydjangoapp -e; the app probably failed to start (a missing env var or import error) or the socket path in Nginx does not match the service.

Nxeon VPS hosting for developers gives you full root and fast NVMe to run Django and PostgreSQL side by side — with free migration help to move an existing project across. Compare deploying Flask or FastAPI.

#django#python#gunicorn#nginx#deployment#seobatch

Deploy your first server in under a minute

Creating an account is free and takes no card details. You pay when you deploy — choose a billing term and pay from your wallet or by card at checkout.