Why Self-Host Django on a VPS?
Managed platforms for Django bill per usage and hide the server layer, which complicates optimizing migrations, Celery tasks, and large media files. On a VPS, you control the complete stack: Gunicorn as the WSGI server, Nginx at the front, PostgreSQL for data, and Redis for caching and task queues. You decide on the Python version, the number of workers, the static-file collection strategy, and the database tuning. For a production Django application that goes beyond the prototype, this transparency is decisive: you can profile a slow query, adjust PostgreSQL's settings, and deploy a new version with no hidden extra cost.
Concrete Benefits of a Self-Hosted Django
- Full control over PostgreSQL: indexes, VACUUM, persistent connections, and scheduled pg_dump backups.
- Asynchronous tasks with Celery and Redis without billing per worker execution.
- Fixed, predictable cost, regardless of traffic or the volume of media files served.
- Data and logs under your jurisdiction, useful for compliance and auditing.
- Freedom of Python version and system dependencies (GDAL, WeasyPrint, Pillow) without buildpack constraints.
- Reproducible deployments via Docker and the ability to clone the environment identically.
Hardware and Software Prerequisites
For a Django application with PostgreSQL and Redis, plan for at least 2 vCPU and 2 GB of RAM; aim for 4 GB if you add Celery and a busy Redis cache. The recommended OS is Ubuntu 22.04 or 24.04 LTS. Install Docker and Docker Compose to isolate the services, or Python 3.11+ and systemd for a native installation. A domain name pointed via an A record to the VPS IP is required for the TLS certificate. Budget 20 GB of disk for the application, the database, and the media files of a medium-sized project.
Deploy Django Step by Step
Prepare the VPS and the Project
Connect via SSH, create a dedicated non-root user, and install Docker. Clone your repository and create a .env file containing SECRET_KEY, DATABASE_URL, ALLOWED_HOSTS=yourdomain.com, and DEBUG=False.
Containerize with Gunicorn
Write a Dockerfile based on python:3.11-slim, install the dependencies from requirements.txt, then launch the application with gunicorn projet.wsgi:application --bind 0.0.0.0:8000 --workers 3. Define a web service, a db service (PostgreSQL), and a redis service in docker-compose.yml.
Apply Migrations and Static Files
Run docker compose run --rm web python manage.py migrate, then python manage.py collectstatic --noinput. Mount a volume for /app/staticfiles and /app/media so that Nginx can serve them directly.
Configure Nginx as a Reverse Proxy
Create a server block that forwards traffic to http://web:8000 via proxy_pass, and that serves /static/ and /media/ from the volumes. Add the X-Forwarded-Proto and Host headers so that Django correctly detects HTTPS.
Enable HTTPS with Let's Encrypt
Use Certbot or an nginx-proxy container with acme-companion to automatically obtain and renew the certificate for yourdomain.com. Force the HTTP-to-HTTPS redirect and enable HSTS in Django via SECURE_HSTS_SECONDS.
Start and Supervise
Run docker compose up -d, check the logs with docker compose logs -f web, and create the superuser with python manage.py createsuperuser. Schedule a cron job for pg_dump backups and the clean restart of Celery workers.
Enable the PgBouncer connection pooler between Django and PostgreSQL: with several Gunicorn workers and Celery tasks, you quickly exhaust Postgres's max_connections. PgBouncer in transaction mode multiplies the number of queries served without touching the database tuning, and stabilizes latency under load.