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.

Long Nguyen Avatar

Long Nguyen

Fullstack Developer · AI Engineer · Researcher

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.

FAQ

Frequently asked questions

Why use a Unix socket between Nginx and Gunicorn instead of a port?

A Unix socket keeps traffic between Nginx and Gunicorn local to the machine, which is slightly faster and avoids exposing Gunicorn on a network port. Nginx handles the public connection; Gunicorn only ever talks to Nginx over the socket.

Why run Gunicorn through systemd instead of starting it manually?

systemd starts Gunicorn on boot, restarts it if it crashes, and lets you control it like any other service. Running it by hand means it dies when your terminal closes and never comes back after a reboot.

How many Gunicorn workers should I use?

A common starting point is roughly two times the number of CPU cores, plus one. This app uses three. Tune from there based on real traffic and how much memory each worker uses.

Does Certbot renew the SSL certificate automatically?

Yes. Certbot installs a timer that renews the Let's Encrypt certificate before it expires, so once it's set up you generally don't have to think about it again.

Why does the Gunicorn timeout need to be so high?

This app makes slow AI calls, and the default timeout would kill those long-running requests. Raising it to 180 seconds gives the AI pipeline room to finish. Apps without slow external calls can usually keep a much lower timeout.

Stay updated with Netalith

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