Deploying Django on a VPS
In this guide you'll deploy a Django project on your VPS in production mode: a virtual environment, Gunicorn as a systemd service, and Nginx as a reverse proxy serving static files — plus the correct DEBUG and ALLOWED_HOSTS settings.
What you'll need
- A VPS running Ubuntu 22.04/24.04, Debian 11/12, or AlmaLinux/Rocky Linux 8/9, with SSH access as root or a sudo user.
- A domain pointed at the server's IP (a bare IP address works too, if you don't have a domain yet).
- Your own Django project — developed locally or already in a git repository, with a
requirements.txtdependency file. - Basic comfort working in a terminal over SSH.
Step 1. Install system packages
Connect to the server over SSH and install Python with the virtual environment tooling, plus Nginx:
# Ubuntu / Debian
sudo apt update
sudo apt install -y python3-venv python3-pip nginx# AlmaLinux / Rocky
sudo dnf install -y python3 python3-pip nginxStep 2. Create a user, copy the project, and set up a virtual environment
Running a web app as root isn't a good idea. Create a dedicated system user for the project:
# Ubuntu / Debian
sudo adduser django# AlmaLinux / Rocky
sudo useradd -m -s /bin/bash django
sudo passwd djangoSwitch to that user and deploy the project into its home directory — so that manage.py ends up directly in /home/django/myproject:
sudo -iu django
git clone https://github.com/yourname/myproject.git myproject
cd myprojectCreate a virtual environment and activate it:
python3 -m venv venv
source venv/bin/activateInstall the project's dependencies and Gunicorn:
pip install --upgrade pip
pip install -r requirements.txt
pip install gunicornStep 3. Configure DEBUG and ALLOWED_HOSTS
Django is configured for development by default, not production. Before opening the site to the outside world, edit myproject/settings.py (replace myproject with your own project's settings package name):
# myproject/settings.py
DEBUG = False
ALLOWED_HOSTS = ["example.com", "www.example.com", "203.0.113.10"]With DEBUG = False, Django doesn't respond to requests at all if ALLOWED_HOSTS is empty — this list protects the site from requests with a spoofed Host header. Add to it:
- the domain(s) the site will be served on — for example,
example.comandwww.example.com; - the server's IP address, if you need it;
- subdomains can be allowed with a single wildcard by putting a leading dot on the value:
.example.commatches bothexample.comand any of its subdomains; - a value of
*(allow anyHost) is technically valid, but then you have to implement your own validation of theHostheader — for a normal site, use a specific list of domains/IPs instead.
Step 4. Test-run the project with Gunicorn
While you're still in the virtual environment, run Gunicorn by hand — it's the fastest way to confirm the project comes up at all before wrapping it in systemd:
gunicorn --bind 127.0.0.1:8000 myproject.wsgi:applicationIn a second SSH session, check the response (replace example.com with the value you put in ALLOWED_HOSTS):
curl -I -H "Host: example.com" http://127.0.0.1:8000/The expected result is HTTP/1.1 200 OK (or a redirect, if your project is set up that way). Stop Gunicorn with Ctrl+C.
Once you're done testing, run deactivate and exit — the remaining steps (systemd, Nginx) are done as root or a sudo user.
Step 5. Set up Gunicorn as a systemd service
As root/sudo, create the unit file /etc/systemd/system/myproject.service:
[Unit]
Description=Gunicorn daemon for myproject
After=network.target
[Service]
User=django
Group=django
WorkingDirectory=/home/django/myproject
ExecStart=/home/django/myproject/venv/bin/gunicorn \
--workers 3 \
--bind 127.0.0.1:8000 \
myproject.wsgi:application
Restart=on-failure
[Install]
WantedBy=multi-user.targetUser/Group— the project's user from step 2, not root.--workers— the number of Gunicorn worker processes; a good starting point is "(2 × number of cores) + 1" — check the core count withnproc.- Gunicorn is launched via its full path inside
venv, not the system-wide one — that guarantees the environment with this project's own dependencies is used.
Apply and enable the service:
sudo systemctl daemon-reload
sudo systemctl enable --now myproject
sudo systemctl status myprojectStep 6. Configure Nginx: reverse proxy and static files
Collect the project's static files. Switch back to the project's user and activate the virtual environment:
sudo -iu django
cd myproject
source venv/bin/activateAdd to myproject/settings.py:
# myproject/settings.py
STATIC_URL = "static/"
STATIC_ROOT = BASE_DIR / "staticfiles"and run:
python manage.py collectstaticDjango copies all static files — its own and those of third-party apps, including the admin — into /home/django/myproject/staticfiles/. Nginx will serve this folder directly, without touching Gunicorn at all.
Switch back to root/sudo (deactivate, then exit) and create the site's configuration file — for example, sudo nano /etc/nginx/sites-available/myproject on Ubuntu/Debian, or sudo nano /etc/nginx/conf.d/myproject.conf on AlmaLinux/Rocky — with the following content:
server {
listen 80;
server_name example.com www.example.com;
location /static/ {
alias /home/django/myproject/staticfiles/;
}
location / {
proxy_pass http://127.0.0.1:8000;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
}
}Replace server_name with your own domain (or the server's IP) — the same value must be in ALLOWED_HOSTS from step 3.
On Ubuntu/Debian, enable the site with a symlink and remove the default one:
sudo ln -s /etc/nginx/sites-available/myproject /etc/nginx/sites-enabled/
sudo rm -f /etc/nginx/sites-enabled/defaultOn AlmaLinux/Rocky there's nothing extra to enable — everything under /etc/nginx/conf.d/ is read automatically.
Test the configuration and reload Nginx:
sudo nginx -t
sudo systemctl reload nginx
sudo systemctl enable --now nginxStep 7. Open the port in the firewall
If the server has a firewall enabled, the site won't be reachable from outside while port 80 is closed.
# ufw (Ubuntu/Debian)
sudo ufw allow 80/tcp# firewalld (AlmaLinux/Rocky)
sudo firewall-cmd --permanent --add-service=http
sudo firewall-cmd --reloadStep 8. Check the result
Open the domain (or the server's IP) in a browser, or check from the terminal:
curl -I http://example.com/The expected response is HTTP/1.1 200 OK. If the page has styles or images served from /static/ (for example, the Django admin's styles at /static/admin/), make sure those load too — that confirms Nginx is serving STATIC_ROOT correctly.
What's next
- Add HTTPS (for example, via Let's Encrypt/certbot) — the current Nginx config is already set up to have a
listen 443 ssl;block added on top of it. - If the server will host several projects, give each one its own directory, virtual environment, systemd unit, and Nginx server block with its own
server_name. - For other ready-made VPS scenarios, see the "Ready-made VMmanager 6 recipes" article.