Django Production Deployment Standards (VPS + Nginx)
A production-grade guide for deploying Django applications on Ubuntu-based VPS/VPCs using Gunicorn, systemd sockets, PostgreSQL, and Nginx—with HTTPS, gzip, and proper static/media handling.
🚀 Deploying a Django Application on a VPS/VPC Using Nginx (Ubuntu)
This step-by-step guide explains how to deploy a production-ready Django application on a VPS/VPC running Ubuntu using Nginx.
📌 Prerequisites
-
A VPS/VPC running Ubuntu
-
A Fast API application
-
Valid SSH authentication to the server (valid user)
-
Nginx installed (or install during steps)
-
Optional: A domain name
Step-01: SSH to the Remote Server
Use the below SSH command to gain access to the remote server:
ssh -p {exposed_port_number} {user}@{server_ip / domain_name}
Step-02: Update & Upgrade Packages
Update & upgrade currently installed packages in the remote server using the following command:
sudo apt update && sudo apt upgrade -y
Step-03: Create Project Directory
Create proper directory for the project using below command:
mkdir /home/{user}/{project_name}/backend
Step-04: Upload or Clone Your Project
Option A: Clone from Git
Clone the project codebase from your Git repository (e.g. Gitea, Github etc.) to the directory using the following command:
git clone {Repository HTTPS URL}
Option B: Upload Using FileZilla (recommended for non-Git setups)
Using FileZilla, upload all the contents from local machine’s project directory.
/home/{user}/{project_name}/backend
Step-05: Install System Dependencies
Install required dependencies using the below command:
sudo apt install postgresql postgresql-contrib nginx curl make -y
Step-06: Configure PostgreSQL
To create the PostgreSQL database and user, you have to switch to psql interactive session by using the following command:
sudo -u postgres psql
Create Database:
You will be given a PostgreSQL prompt starting with postgres=#. Now to create the database, use the following command:
NB: Don't forget to put ';' (semicolon) after every statement!
CREATE DATABASE {database_name};
Create User:
Now create new user who to access the database using the following command:
CREATE USER {username} WITH PASSWORD '{password}';
Set Defaults:
Using the following commands, set default transaction isolation, character encoding and default timezone for the newly created user which are mandatory:
ALTER ROLE {username} SET client_encoding TO 'utf8';
ALTER ROLE {username} SET default_transaction_isolation TO 'read committed';
ALTER ROLE {username} SET timezone TO 'UTC';
Grant Privileges:
Now give the new user administrative access to the database so that the user can access the database using the command mentioned below:
GRANT ALL PRIVILEGES ON DATABASE {database_name} TO {username};
Exit:
Now you are done with the database part. Use the following command to exit the interactive terminal:
\q
Check the Project’s Environment File
Check the project environment file (.env) to ensure that all the database related variables are in order or not by using the following command:
sudo nano /{project_name}/backend/.env
Now check and update the following values:
DB_USER={username}
DB_PASSWORD={password}
DB_NAME={database_name}
Step-07: Virtual Environment Setup Using UV
Install UV:
To create the virtual environment (venv), we are going to use uv package. To install the package use the following command:
curl -LsSf https://astral.sh/uv/install.sh | sh
Verify Installation:
Now check if uv is installed properly or not using the following command:
uv --version
Go to Project Directory:
If everything is good till now, go to the project directory to create the virtual environment using the following command:
cd /{project_name}/backend
Create Virtual Environment:
By using the command mentioned below, create the virtual environment for the project:
uv venv
Activate Venv:
Activate the virtual environment using the following command:
source .venv/bin/activate
Install Dependencies:
Install all the project dependencies using the following command:
uv pip install -r requirements.txt
Verify Installed Dependencies:
Use the following command to check whether all the dependencies are installed correctly or not:
uv pip list
Step-08: Run Migrations
If Using Makefile:
Now you need to run the migrations to create all the related tables into the database. There should be a makefile inside the project root directory. If there is, then run the following commands to complete database migrations:
make migrate
If Using Django Directly:
And if there is no makefile in the root directory, then run the following commands:
python manage.py makemigrations
python manage.py migrate
If there is no error, then your migration is successfully done!
Step-09: Create Socket & Service File
Create Project Socket
To create project socket file, run the following command:
sudo nano /etc/systemd/system/{project_name}.socket # replace spaces with `_` in the project name
Then add the following instructions inside the file to create the socket:
[Unit]
Description={short_description} # e.g. socket for {project_name} backend
[Socket]
ListenStream=/run/{project_name}.sock
[Install]
WantedBy=sockets.target
Save & exit:
Ctrl + S, then Ctrl + X
Create Project Service
To create the project service, use the following command:
sudo nano /etc/systemd/system/{project_name}.service # replace spaces with `_` in the project name
Then add the following instructions inside the file to create the service:
[Unit]
Description={short_description} # e.g. {project_name} backend service
Requires={project_name}.socket # use the name of the socket file
After=network.target
[Service]
User={username}
Group={user_group}
WorkingDirectory=/home/{username}/{project_name}/backend
EnvironmentFile=/home/{username}/{project_name}/backend/.env # add this line if you have a .env file
ExecStart=/home/{username}/{project_name}/backend/.venv/bin/gunicorn \
--access-logfile - \
--error-logfile - \
--workers {number_of_workers} \ # rule of thumb: number_of_workers = (CPU_cores * 2) + 1
--worker-class gevent \
--worker-connections 1000 \
--max-requests 1000 \
--max-requests-jitter 100 \
--timeout 30 \
--keep-alive 2 \
--bind unix:/run/{project_name}.sock \
{django_project_name}.wsgi:application # put django project name here (the one you created with `django-admin startproject`)
[Install]
WantedBy=multi-user.target
Save & exit:
Ctrl + S, then Ctrl + X
Enable Socket
Now you have to enable the socket. Use the following commands to do so:
sudo systemctl start {project_name}.socket
sudo systemctl enable {project_name}.socket
Check Socket Status
Use the following command to check socket’s status:
sudo systemctl status {project_name}.socket
Enable Service
To enable project’s service, use the following commands:
sudo systemctl daemon-reload
sudo systemctl start {project_name}
Step-10: Configure Nginx
After setting up the socket and service, you have to configure an nginx file to set up the proxy pass. Use the following command to do it:
sudo nano /etc/nginx/sites-available/{project_name} # replace spaces with `_` in the project name
Now write the following instructions in the file to configure it
server {
listen 80;
listen [::]:80;
server_name {server_domain_or_IP};
client_max_body_size {size};
# Security headers
add_header X-Frame-Options "SAMEORIGIN" always;
add_header X-Content-Type-Options "nosniff" always;
add_header X-XSS-Protection "1; mode=block" always;
add_header Referrer-Policy "no-referrer-when-downgrade" always;
# Gzip compression
gzip on;
gzip_vary on;
gzip_min_length 1024;
gzip_proxied any;
gzip_comp_level 6;
gzip_types
text/plain
text/css
text/xml
text/javascript
application/json
application/javascript
application/xml+rss
application/atom+xml
image/svg+xml;
location = /favicon.ico {
access_log off;
log_not_found off;
expires 1y;
add_header Cache-Control "public, immutable";
}
# static file location
location /static/ {
alias /home/{username}/{project_name}/backend/staticfiles/;
expires 1y;
add_header Cache-Control "public, immutable";
access_log off;
}
# media file location
location /api/media/ {
alias /home/{username}/{project_name}/backend/media/;
expires 1M;
add_header Cache-Control "public";
}
location / {
include proxy_params;
proxy_pass http://unix:/run/{project_name}.sock; # exact same name inside `{project_name}.socket`
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;
proxy_connect_timeout 30s;
proxy_send_timeout 30s;
proxy_read_timeout 30s;
}
}
Save & exit:
Ctrl + S, then Ctrl + X
Step-11: Create Symbolic Link
Now create symbolic link using the following command:
sudo ln -s /etc/nginx/sites-available/{project_name} /etc/nginx/sites-enabled
Then test whether the nginx service is running properly or not using the following command:
sudo nginx -t
If there are no errors, restart nginx using the following command:
sudo systemctl restart nginx
Step-12: Obtain SSL Certificate
This section upgrades your HTTP setup to secure HTTPS using free certificates from Let’s Encrypt. Firstly, install necessary packages via following command:
sudo apt install certbot python3-certbot-nginx -y
Then, obtain SSL Certificates using the following command:
sudo certbot --nginx -d {domain_name} -d www.{domain_name} # `-d www.{domain_name}` this part is optional
Choose:
- “Redirect
HTTPtoHTTPS”
Now Certbot will:
-
Generate SSL certificates
-
Modify your Nginx config
-
Reload Nginx
-
Add auto-renew
After running the command successfully, Certbot replaces your config with:
server {
listen 80;
listen [::]:80;
server_name {domain_name} www.{domain_name};
return 301 https://$host$request_uri;
}
server {
listen 443 ssl;
listen [::]:443 ssl;
server_name {domain_name} www.{domain_name};
ssl_certificate /etc/letsencrypt/live/{domain_name}/fullchain.pem;
ssl_certificate_key /etc/letsencrypt/live/{domain_name}/privkey.pem;
include /etc/letsencrypt/options-ssl-nginx.conf;
ssl_dhparam /etc/letsencrypt/ssl-dhparams.pem;
client_max_body_size {size};
# Security headers
add_header X-Frame-Options "SAMEORIGIN" always;
add_header X-Content-Type-Options "nosniff" always;
add_header X-XSS-Protection "1; mode=block" always;
add_header Referrer-Policy "no-referrer-when-downgrade" always;
# Gzip compression
gzip on;
gzip_vary on;
gzip_min_length 1024;
gzip_proxied any;
gzip_comp_level 6;
gzip_types
text/plain
text/css
text/xml
text/javascript
application/json
application/javascript
application/xml+rss
application/atom+xml
image/svg+xml;
location = /favicon.ico {
access_log off;
log_not_found off;
expires 1y;
add_header Cache-Control "public, immutable";
}
# static file location
location /static/ {
alias /home/{username}/{project_name}/backend/staticfiles/;
expires 1y;
add_header Cache-Control "public, immutable";
access_log off;
}
# media file location
location /api/media/ {
alias /home/{username}/{project_name}/backend/media/;
expires 1M;
add_header Cache-Control "public";
}
location / {
include proxy_params;
proxy_pass http://unix:/run/{project_name}.sock; # exact same name inside `{project_name}.socket`
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;
proxy_connect_timeout 30s;
proxy_send_timeout 30s;
proxy_read_timeout 30s;
}
}
Now, using the following command, you can renew the SSL certificate every 60-90 days automatically:
sudo certbot renew --dry-run
🎉 Deployment Complete!
Your Django application is now successfully deployed and accessible via Nginx on your VPS.
Let's Build Something Scalable
We apply these same engineering principles to client projects. Ready to upgrade your stack?