AI Tools

Deploying a Django App to Production With Gunicorn & Nginx

Deploy a Django app to production the real way: Gunicorn via systemd socket, Nginx reverse proxy, SSL with Certbot, log rotation, and a simple deploy script.

Drake Nguyen

Founder · System Architect

3 min read
Django production architecture: Nginx with SSL reverse-proxies over a Unix socket to Gunicorn running the Django app

From Working to Shipped

Across the last nine parts we built a complete AI interview app — parsing, an agent pipeline, background processing, voice, and scoring. In part 9 it ran end to end on a laptop. This final part is about the gap almost every tutorial skips: getting it onto the internet, reliably, so real users can reach it. That means deploying a Django app to production with Gunicorn and Nginx, plus SSL, logging, and a repeatable deploy step.

One thing this guide assumes is already in place: a production database. SQLite won't do for a real deployment, so before deploying you'll want a proper database server set up — either PostgreSQL or MySQL, each covered in its own guide. With that ready, the rest of this walkthrough gets the app itself online.

The architecture is standard and battle-tested: Nginx sits at the front handling HTTPS and static files, and passes dynamic requests over a local socket to Gunicorn, which actually runs Django. Nothing exotic — just each tool doing the one job it's good at.

Running Gunicorn as a systemd Service

Gunicorn is the application server that runs Django in production. Rather than launch it by hand, we let systemd manage it — so it starts on boot, restarts on crash, and is controllable like any other service. The setup uses two units: a socket and a service.

The socket unit creates the Unix socket Nginx will talk to:

# /etc/systemd/system/aiinterviewer.socket
[Unit]
Description=aiinterviewer gunicorn socket

[Socket]
ListenStream=/run/aiinterviewer.sock

[Install]
WantedBy=sockets.target

The service unit runs Gunicorn itself, bound to that socket:

# /etc/systemd/system/aiinterviewer.service
[Unit]
Description=aiinterviewer gunicorn daemon
Requires=aiinterviewer.socket
After=network.target

[Service]
Group=www-data
WorkingDirectory=/var/www/ai_interviewer
ExecStart=/var/www/ai_interviewer/venv/bin/gunicorn \
    --workers 3 \
    --timeout 180 \
    --log-level info \
    --capture-output \
    --bind unix:/run/aiinterviewer.sock \
    --access-logfile /var/www/ai_interviewer/media/log/gunicorn/access.log \
    --error-logfile /var/www/ai_interviewer/media/log/gunicorn/error.log \
    ai_interviewer.wsgi:application

[Install]
WantedBy=multi-user.target

A couple of the Gunicorn flags are worth understanding. --workers 3 runs three worker processes so requests are handled in parallel; a common starting point is roughly two-times-CPU-cores plus one. --timeout 180 is generous here on purpose — this app makes slow AI calls, and a stricter default would kill long-running requests. See the Gunicorn settings docs for the full list.

With the units written, reload systemd and start the socket — Gunicorn launches automatically the first time a request hits it:

sudo systemctl daemon-reload
sudo systemctl start aiinterviewer.socket
sudo systemctl enable aiinterviewer.socket

# verify the socket exists and responds
sudo systemctl status aiinterviewer.socket
curl --unix-socket /run/aiinterviewer.sock localhost

Nginx as a Reverse Proxy

Nginx is the public-facing front door. It serves static files directly (fast, no Python involved) and forwards everything else to Gunicorn over the socket:

# /etc/nginx/sites-available/aiinterviewer
server {
    listen 80;
    server_name ai-interviewer.tech www.ai-interviewer.tech;

    location = /favicon.ico { access_log off; log_not_found off; }

    location /static/ {
        root /var/www/ai_interviewer;
    }

    location / {
        include proxy_params;
        proxy_pass http://unix:/run/aiinterviewer.sock;
    }
}

Enable the site, test the config before reloading (a syntax error would otherwise take Nginx down), and restart:

sudo ln -s /etc/nginx/sites-available/aiinterviewer /etc/nginx/sites-enabled
sudo nginx -t          # always test before reloading
sudo systemctl restart nginx

Serving static files from Nginx instead of Django is the key move here: Nginx is built for it, and it keeps your Python workers free to handle the requests that actually need them.

HTTPS With Certbot

A production app has no business running on plain HTTP — especially one handling résumés. Certbot gets a free Let's Encrypt certificate and rewires Nginx for HTTPS in one command:

sudo apt install certbot python3-certbot-nginx -y
sudo certbot --nginx -d ai-interviewer.tech -d www.ai-interviewer.tech

Certbot edits the Nginx config to serve HTTPS, sets up the HTTP-to-HTTPS redirect, and installs a timer to auto-renew the certificate before it expires. It's one of the few "just works" experiences in server administration.

Firewall, Log Rotation, and Repeatable Deploys

Three finishing touches turn a working server into one you can actually operate.

A firewall, so only the ports you intend are reachable — SSH and web traffic, nothing else:

sudo ufw allow OpenSSH
sudo ufw allow 'Nginx Full'
sudo ufw enable

Log rotation, so Gunicorn's access and error logs don't quietly fill the disk. This keeps 90 days, compressed:

# /etc/logrotate.d/aiinterviewer
/var/www/ai_interviewer/media/log/gunicorn/*.log {
    daily
    rotate 90
    compress
    delaycompress
    missingok
    notifempty
    copytruncate
}

The copytruncate option matters for Gunicorn specifically: it lets logs rotate without needing to signal or restart the process. Left unmanaged, logs are one of the most common ways a small server runs itself out of disk months later.

A deploy script, so shipping an update is one command instead of a remembered sequence:

# build.sh
#!/bin/bash
git pull origin master
source venv/bin/activate
python manage.py migrate --noinput
python manage.py collectstatic --noinput
sudo systemctl restart aiinterviewer
echo "Build completed!"

Every deploy now runs the same steps in the same order — pull, migrate, collect static, restart. Scripting it isn't just convenience; it removes the chance of forgetting a step at 2am, which is exactly when deploys tend to happen.

The App Is Live — and the Series Is Done

That's the whole journey: from an empty Django project to a live, HTTPS-secured AI interview app that parses résumés, runs an agent-driven interview by voice, scores the answers, and reports back — reachable by anyone, at a real domain. Every part was the real thing, built and shipped, not a toy.

You can try the finished product, free and with no sign-up, at ai-interviewer.tech. And if you'd rather start from the complete, production-ready source instead of rebuilding all ten parts yourself — the tuned prompts, the hardened pieces, and the full deployment included — it's packaged as the AI Mock Interview SaaS Starter Kit. Whether you build it or start from the source, you now know exactly what goes into a real AI product — end to end.

Stay updated with Netalith

Get coding resources, product updates, and special offers directly in your inbox.