Step-by-Step Guide to Deploying a Django Project on a Server
Getting a Django project running on your laptop with python manage.py runserver takes five minutes. Getting that same project running safely and reliably on a real server, for real visitors, is a different job entirely — and the gap between the two trips up almost everyone the first time. Here's the full path from a finished project to a live, production-grade deployment behind Nginx and Gunicorn.

Illustration: from your laptop to a production server.
1. Prepare your Django project for production
The development server (runserver) is explicitly documented by Django as unsuitable for production — it's single-threaded, unoptimized, and not hardened against real traffic. Before touching the server, get the project itself production-ready:
- Collect static files into one directory so your web server (not Django) can serve them directly, which is dramatically faster:
python manage.py collectstatic - Set
DEBUG = Falsein settings.py. This isn't optional — with DEBUG on, any unhandled error dumps a full traceback, including settings values and environment details, to whoever triggered it. Leaving DEBUG on in production is one of the most common real-world Django security mistakes. - Set
ALLOWED_HOSTSto your actual domain(s) — with DEBUG off, Django refuses to serve requests for any host not explicitly listed here, which prevents a class of HTTP Host header attacks:ALLOWED_HOSTS = ['yourdomain.com', 'server_ip'] - Move
SECRET_KEYout of source control and into an environment variable. This key signs sessions and security tokens — if it's sitting in a public repo, anyone can forge session data.
2. Set up the server
Update the system and install the core dependencies: Python itself, pip, and a way to isolate this project's packages from the system.
sudo apt update && sudo apt upgrade -y sudo apt install python3 python3-pip python3-venv -y
Then install the two pieces that actually serve the app in production: Gunicorn, a WSGI application server that runs your Django code as multiple worker processes (instead of runserver's single thread), and Nginx, a web server that sits in front of Gunicorn.
sudo apt install nginx pip install gunicorn
Why both? Gunicorn alone can technically serve HTTP directly, but it's not built to efficiently handle things like serving static files, managing thousands of slow/idle client connections, TLS termination, or buffering slow uploads. Nginx handles all of that in front, and passes only the actual application requests back to Gunicorn — a standard, battle-tested split that's used across the vast majority of production Django deployments.
3. Configure the Django application
python3 -m venv myenv source myenv/bin/activate pip install -r requirements.txt
The virtual environment keeps this project's exact dependency versions isolated from the system Python and from any other project on the same server — critical once you're running more than one app on a box. Before wiring up Nginx, sanity-check that Gunicorn can actually serve the app on its own:
gunicorn --bind 0.0.0.0:8000 myproject.wsgi
If that works and you can load the site on port 8000, the application itself is fine and any remaining issues are in the Nginx/systemd wiring, not the Django code — a useful debugging checkpoint.
4. Configure Nginx as the reverse proxy
sudo nano /etc/nginx/sites-available/myproject
server {
listen 80;
server_name yourdomain.com;
location = /favicon.ico { access_log off; log_not_found off; }
location /static/ {
root /path/to/your/project;
}
location / {
include proxy_params;
proxy_pass http://unix:/path/to/your/project/myproject.sock;
}
}
The /static/ block is the performance-critical part — it tells Nginx to serve those files itself, straight off disk, instead of passing every CSS and JS request through to Gunicorn and Django. Everything else falls through to location /, which proxies to Gunicorn over a Unix socket (faster and simpler to secure than a TCP port for same-machine communication).
sudo ln -s /etc/nginx/sites-available/myproject /etc/nginx/sites-enabled sudo nginx -t sudo systemctl restart nginx
nginx -t validates the config syntax before you reload — always run it first, since restarting Nginx with a broken config file takes your whole site down, not just the new one.
5. Run Gunicorn as a managed service
Running Gunicorn by hand in a terminal means it dies the moment you close that terminal or the server reboots. A systemd service fixes that — the OS itself supervises the process and restarts it automatically if it crashes.
sudo nano /etc/systemd/system/gunicorn.service
[Unit] Description=gunicorn daemon for Django project After=network.target [Service] User=username Group=groupname WorkingDirectory=/path/to/your/project ExecStart=/path/to/your/virtualenv/bin/gunicorn --workers 3 --bind unix:/path/to/your/project/myproject.sock myproject.wsgi:application [Install] WantedBy=multi-user.target
The worker count is worth tuning deliberately rather than leaving at a guess — a common starting formula is (2 × CPU cores) + 1 workers, adjusted based on actual memory and traffic once the app is live.
sudo systemctl start gunicorn sudo systemctl enable gunicorn
enable is what makes Gunicorn come back up automatically after a server reboot — easy to forget, and the reason a server that "was working fine" sometimes doesn't come back after a routine restart.
6. Secure the server with HTTPS
sudo apt install certbot python3-certbot-nginx sudo certbot --nginx -d yourdomain.com
Certbot (Let's Encrypt) automatically obtains a free TLS certificate and reconfigures the Nginx site to serve over HTTPS. Certificates expire every 90 days, so automate renewal rather than relying on remembering:
0 3 * * * certbot renew --quiet
7. Verify the deployment
- Load the domain in a browser and confirm the site renders correctly, including static assets (CSS/JS/images) — a common first-deploy bug is a working page with no styling, which almost always traces back to the
/static/Nginx block or a missedcollectstatic. - If something's wrong, the logs tell you which half of the stack failed: Nginx errors (bad proxy config, permissions, SSL issues) land in
/var/log/nginx/error.log; application errors (Django exceptions, import errors, database issues) show up in Gunicorn's logs, wherever your systemd service is configured to send them (journalctl -u gunicornby default).
Frequently Asked Questions
Why not just use runserver in production if traffic is low? Even at low traffic, runserver lacks production security hardening, doesn't handle concurrent requests well, and isn't designed to stay reliably up — a single unhandled worker crash takes the whole site down with no automatic recovery, which is exactly what systemd + Gunicorn solves.
Do I need both Nginx and Gunicorn, or can I pick one? Gunicorn can serve HTTP directly, but production deployments almost universally keep Nginx in front of it for static file performance, TLS termination, and protection against slow/malicious connections reaching your application workers directly.
What's the most common deployment mistake? Forgetting DEBUG = False or a correct ALLOWED_HOSTS before going live — both are silent until something goes wrong, at which point DEBUG=True turns an ordinary error into a security incident by exposing internal details to whoever triggered it.
Comments (0)
Leave a Reply
Log in to post a comment.