Step-by-Step Guide to Deploying a Django Project on a Server

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.

A laptop connected by a glowing line to a server rack in the cloud

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 = False in 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_HOSTS to 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_KEY out 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 missed collectstatic.
  • 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 gunicorn by 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.

  • Tags:
  • No tags

Comments (0)

Leave a Reply

Log in to post a comment.