Effortlessly Deploy a Django Application on Ubuntu Using PostgreSQL, Gunicorn, and Nginx

By Rajiv Raut July 26, 2026
cover picture [Django & PostgreSQL]

Overview

This knowledge base article provides a comprehensive guide for deploying a Django application in a production environment on an Ubuntu-based VPS using PostgreSQL, Gunicorn, and Nginx.

If you are looking for reliable infrastructure to host your application, consider deploying it on our high-performance VPS hosting. A VPS provides dedicated resources, full root access, and the flexibility needed to run production workloads securely and efficiently. You can explore our VPS hosting plans here: ehostingServer

Unlike Django’s built-in development server, which is intended only for local development and testing, a production deployment requires multiple components working together to provide security, scalability, reliability, and performance.

This guide explains not only how to deploy the application, but also why each component is required and how they interact within the deployment architecture. Whether you are deploying your first Django project or managing multiple production applications, the steps in this article follow widely accepted best practices for VPS-based deployments.

By following this guide, you will deploy a Django application that:

  • Uses PostgreSQL as the production database.
  • Serves application requests through Gunicorn.
  • Uses Nginx as a reverse proxy and static file server.
  • Automatically starts after system reboot using systemd.
  • Supports secure HTTPS communication (optional).
  • Follows recommended production deployment practices.
  • Runs reliably on a production-ready VPS environment with full administrative control.

Production Architecture

The deployment described in this article follows a layered architecture.

                   Client Browser
                         │
                    HTTP / HTTPS
                         │
                  ┌──────────────┐
                  │    Nginx     │
                  │ Reverse Proxy│
                  └──────┬───────┘
                         │
                  Unix Socket / TCP
                         │
                  ┌──────────────┐
                  │  Gunicorn    │
                  │ WSGI Server  │
                  └──────┬───────┘
                         │
                  ┌──────────────┐
                  │    Django    │
                  │ Application  │
                  └──────┬───────┘
                         │
                  ┌──────────────┐
                  │ PostgreSQL   │
                  │  Database    │
                  └──────────────┘
request processing flow of django application

Each component performs a dedicated role, creating a secure and maintainable production environment.

How the Deployment Works

A typical client request follows this sequence:

  1. A user accesses the application’s URL from a web browser.
  2. The request reaches the Nginx web server.
  3. Nginx serves static files directly when applicable.
  4. Dynamic requests are forwarded to Gunicorn.
  5. Gunicorn executes the Django application.
  6. Django processes the request and communicates with PostgreSQL if database access is required.
  7. PostgreSQL returns the requested data.
  8. Django generates an HTTP response.
  9. Gunicorn returns the response to Nginx.
  10. Nginx sends the response back to the client.
application deployment request flow

This architecture separates responsibilities among components, improving security, scalability, and performance.

Components Used in This Deployment

Ubuntu Server

Ubuntu provides the operating system for hosting the application. It offers long-term support (LTS) releases, regular security updates, and broad community support, making it a common choice for production deployments.

Django

Django is a high-level Python web framework that implements the application’s business logic, URL routing, authentication, templates, and database interactions.

PostgreSQL

PostgreSQL is an enterprise-grade relational database management system (RDBMS). Compared to SQLite, PostgreSQL supports concurrent connections, advanced indexing, transactions, and higher scalability, making it suitable for production environments.

Gunicorn

Gunicorn (Green Unicorn) is a WSGI application server responsible for running the Django application. It listens for requests from Nginx and executes the Django code to generate responses.

Nginx

Nginx acts as a reverse proxy positioned between clients and the Django application. It efficiently serves static assets, manages HTTPS connections, and forwards dynamic requests to Gunicorn.

systemd

systemd is the Linux service manager used to control Gunicorn. It ensures that the application starts automatically during system boot and can restart it if the service unexpectedly stops.

Why Not Use Django’s Development Server?

Django includes a lightweight development server (manage.py runserver) intended only for development and testing.

It should not be used in production because it:

  • is not optimized for high request volumes,
  • lacks advanced process management,
  • does not efficiently serve static files,
  • provides limited security features, and
  • is not designed for long-running production workloads.

Using Gunicorn together with Nginx provides a more secure, scalable, and reliable deployment suitable for production environments.

Prerequisites

Before beginning the deployment, ensure that the following requirements are met.

Server Requirements

  • Ubuntu Server 22.04 LTS or later
  • Minimum 2 GB RAM (4 GB recommended)
  • At least 20 GB available storage
  • Stable internet connection
  • SSH access to the server
  • A non-root user with sudo privileges

Software Requirements

  • Python 3.x
  • pip
  • PostgreSQL
  • Nginx
  • Gunicorn
  • Git (if deploying from a repository)

Note: Commands below assume a Debian/Ubuntu system and use apt. Always run sudo apt update before installing new packages so you get the latest package index.

Deployment

Step 1: Update the System

bash command

sudo apt update && sudo apt upgrade -y
Updating the local repo list and upgrading the updates

Keeps installed packages and the package index current before adding new software.

Step 2: Install Required System Packages

bash command

sudo apt install python3 python3-pip python3-venv nginx postgresql postgresql-contrib git -y

This installs:

  • python3 / python3-pip / python3-venv — Python runtime, package manager, and virtual environment support
  • nginx — reverse proxy / web server
  • postgresql / postgresql-contrib — database server and common extensions
  • git — for cloning the application repository
Installing the packages required by the Django

Verify the installation:

bash command

python3 --version
nginx -v
psql --version
git --version
verifying the installed packages versions

Step 3: Create the PostgreSQL Database and User

Open the PostgreSQL interactive shell:

bash command

sudo -u postgres psql
Opening the PostgreSQL interactive shell

Inside the psql prompt, run:

sql query

CREATE DATABASE myprojectdb;
CREATE USER myprojectuser WITH PASSWORD 'StrongPassword123!';
GRANT ALL PRIVILEGES ON DATABASE myprojectdb TO myprojectuser;
\q

Note: Choose a strong, unique password — don’t reuse the example above in production.

creating database, database_user and granting privileges

Step 4: Prepare the Deployment Directory

bash command

sudo mkdir -p /var/www
cd /var/www/
Creating deployment directory and navigating to it

bash command

sudo git clone https://github.com/gituser/project.git 
cd project
ls

Replace the repository URL with your project’s actual Git URL.

cloning the project from the github

Step 5: Create and Activate a Virtual Environment

bash command

sudo python3 -m venv venv

Note: The command is python3 -m venv venv (the -m venv module flag). Writing it as python3 venv venv — without -m — will fail with python3: can't find '__main__' module.

Creating virtual environment

bash command

source venv/bin/activate
Activating the virtual environment

Step 6: Set Ownership of the Project Directory

bash command

sudo chown -R owner:group /var/www/project

Replace owner:group with the actual user and group that should own the files — typically your deploy user initially, and www-data:www-data later once Gunicorn/Nginx need to serve the files (see Step 12).

setting the ownership of the project directory

Step 7: Install Python Dependencies

bash command

pip install -r requirements.txt
Installing project's required packages

bash command

pip install gunicorn psycopg2-binary
Installing gunicorn and pysopg2-binary packages
  • gunicorn — the production-grade WSGI HTTP server that will run your Django app
  • psycopg2-binary — the PostgreSQL adapter for Python/Django

Step 8: Configure Environment Variables

Create a .env file in the project root:

bash command

sudo vim .env

Add the following, adjusting values for your environment:

ini code

# ------------------------------------------------------------------------------ #
# Core Django Settings
# ------------------------------------------------------------------------------ #
# Generate a secure key in bash using:
# python3 -c 'from django.core.management.utils import get_random_secret_key;print(get_random_secret_key())'

DJANGO_SECRET_KEY=^@6=eoa$+w#me(@2(ttq^enk3d9p98@5uv26y^1rhk6=fdhfdr9lusrj*4ty
# Debug mode disabled for security
DJANGO_DEBUG=False

# Allowed hosts (comma-separated for environment variable parsing)
DJANGO_ALLOWED_HOSTS=example.com,www.example.com,203.0.113.10

# ------------------------------------------------------------------------------ #
# PostgreSQL Database Configuration
# ------------------------------------------------------------------------------ #
POSTGRES_DB=myprojectdb
POSTGRES_USER=myprojectuser
POSTGRES_PASSWORD=StrongPassword123!
POSTGRES_HOST=localhost
POSTGRES_PORT=5432

Critical security notes:

  • Never commit .env to version control. Add it to .gitignore immediately.
  • Always generate a real secret key using the command shown above — never ship the django-insecure-... placeholder key to production. Django prefixes auto-generated dev keys with django-insecure- specifically so they’re easy to spot and rotate.
  • DJANGO_DEBUG must be False in production. Leaving debug mode on exposes stack traces, environment details, and settings to any visitor who triggers an error.
  • Your settings.py needs to actually read these variables (e.g., via os.environ or a library like django-environ / python-decouple) for the .env file to have any effect.
dotenv configuration for django application

Step 9: Grant Schema Privileges to the Database User

Django 15+/PostgreSQL 15+ changed default schema permissions, so the app user usually needs explicit grants on the public schema:

bash command

sudo -u postgres psql -d myprojectdb -c "GRANT ALL ON SCHEMA public TO myprojectuser;"
sudo -u postgres psql -d myprojectdb -c "ALTER SCHEMA public OWNER TO myprojectuser;"
Granting schema privileges to the database user

Step 10: Run Migrations and Create a Superuser

bash command

python manage.py migrate
Django migrate command execution

bash command

python manage.py createsuperuser

Follow the interactive prompts to set the admin username, email, and password.

Django createsuperuser command execution

Step 11: Collect Static Files

bash command

python manage.py collectstatic

This copies all static assets into the directory defined by STATIC_ROOT in settings.py (referenced later as /var/www/project/staticfiles/ in the Nginx config).

Django collectstatic command execution

Step 12: Smoke-Test the Application

First with the Django development server (for a quick sanity check only — never use this in production):

bash command

python manage.py runserver 0.0.0.0:8000
verifying whether the django server is working or not

Visit http://<server-ip>:8000 in a browser, then stop the server with Ctrl+C.

Checking whether the django application is being served to the server IP or not

Next, confirm Gunicorn can serve the app directly:

bash command

gunicorn myproject.wsgi:application

Replace myproject with your actual Django project’s module name (the folder containing wsgi.py). Stop with Ctrl+C once confirmed.

testing whether the Gunicorn can serve the app or not

Step 13: Set Production Ownership

bash command

sudo chown -R www-data:www-data /var/www/project

www-data is the default user Nginx and Gunicorn run as on Debian/Ubuntu — the app files need to be readable (and the socket writable) by this user.

setting the production ownership

Step 14: Create a Gunicorn systemd Service

bash command

sudo vim /etc/systemd/system/gunicorn.service

ini code

[Unit]
Description=Gunicorn daemon for Django project
After=network.target

[Service]
User=www-data
Group=www-data
WorkingDirectory=/var/www/project

ExecStart=/var/www/project/venv/bin/gunicorn \
    --workers 3 \
    --bind unix:/var/www/project/gunicorn.sock \
    myproject.wsgi:application

[Install]
WantedBy=multi-user.target

Replace myproject.wsgi:application with your project’s actual WSGI path.

Note: A common rule of thumb for --workers is (2 × CPU cores) + 1. Three workers is a reasonable default for a small single-core/dual-core VPS.

Creating the Gunicorn service

Enable and start the service:

bash command

sudo systemctl daemon-reload
sudo systemctl enable gunicorn.service
sudo systemctl start gunicorn.service
reloading the systemd daemon, enabling and starting the gunicorn service

bash command

sudo systemctl status gunicorn.service

status should show active (running). If it doesn’t, see Troubleshooting below.

Verifying the Gunicorn.service status

Step 15: Configure Nginx as a Reverse Proxy

bash command

sudo vim /etc/nginx/sites-available/project

nginx config

server {
    listen 80;
    server_name example.com www.example.com;

    location /static/ {
        alias /var/www/project/staticfiles/;
    }

    location / {
        include proxy_params;
        proxy_pass http://unix:/var/www/project/gunicorn.sock;
    }
}
Configuring the nginx reverse proxy

Enable the site and disable the default one:

bash command

sudo ln -s /etc/nginx/sites-available/project /etc/nginx/sites-enabled
enabling the site by linking the nginx configuration

bash command

sudo rm /etc/nginx/sites-enabled/default
Removing nginx default site

bash command

sudo nginx -t

nginx -t tests the configuration for syntax errors before you reload — always run it before restarting Nginx.

testing nginx configuration whether the syntax is ok

bash command

sudo systemctl restart nginx
Restarting the nginx service

Step 16: Configure the Firewall (UFW)

bash command

sudo ufw app list
sudo ufw allow OpenSSH
sudo ufw allow 'Nginx Full'
sudo ufw enable
sudo ufw status

Note: Use straight quotes around 'Nginx Full', not smart quotes ("Nginx Full"). Smart/curly quotes will cause ufw to fail to parse the app profile name. Also, always allow SSH before enabling UFW — enabling the firewall first without an SSH rule can lock you out of the server.

Allowing services through ufw firewall, enabling the firewall and viewing its status

Step 17: Enable HTTPS with Let’s Encrypt (Certbot)

bash command

sudo apt install certbot python3-certbot-nginx -y
Installing the certbot package for SSL

bash command

sudo certbot --nginx -d example.com -d www.example.com
Generating SSL certificates for the domain and subdomains

bash command

sudo certbot renew --dry-run

Certbot automatically edits the Nginx config to redirect HTTP to HTTPS and sets up a renewal cron job/systemd timer. The --dry-run confirms auto-renewal will work without actually issuing a new certificate.

Testing the SSL auto-renewal

Visit https://example.com to confirm the deployment is live and secured.

SIte being live on the internet

Verification Checklist

  • sudo systemctl status gunicorn shows active (running)
  • sudo systemctl status nginx shows active (running)
  • sudo nginx -t passes with no errors
  • The site loads over https:// with a valid certificate (padlock icon, no browser warnings)
  • Static files (CSS/JS/images) load correctly
  • /admin login works with the superuser created in Step 10
  • DJANGO_DEBUG=False and a unique, non-default DJANGO_SECRET_KEY are set

Troubleshooting

  • pip install psycopg2-binary fails to build

Cause: Missing PostgreSQL development headers.

Fix:

bash command

sudo apt install libpq-dev python3-dev build-essential -y
pip install psycopg2-binary
  • 502 Bad Gateway from Nginx

Cause: Gunicorn isn’t running, or Nginx can’t reach the socket.

Fix:

  1. Check Gunicorn is active: sudo systemctl status gunicorn
  2. Check the socket file exists: ls -l /var/www/project/gunicorn.sock
  3. Check the Nginx error log for the exact reason: sudo tail -n 50 /var/log/nginx/error.log
  4. Confirm the proxy_pass path in the Nginx config exactly matches the --bind path in the systemd service file.
  • Permission denied connecting to the Gunicorn socket

Cause: Nginx (running as www-data) can’t read/write the socket, usually due to ownership or directory permissions.

Fix:

bash command

sudo chown -R www-data:www-data /var/www/project
sudo chmod 750 /var/www/project
sudo systemctl restart gunicorn nginx

Also confirm User=www-data and Group=www-data are set in gunicorn.service.

  • django.db.utils.OperationalError: could not connect to server / connection refused

Cause: PostgreSQL isn’t running, or the host/port in .env is wrong.

Fix:

bash command

sudo systemctl status postgresql
sudo systemctl start postgresql

Confirm POSTGRES_HOST=localhost and POSTGRES_PORT=5432 match your PostgreSQL setup, and that settings.py is actually reading these values.

  • django.db.utils.OperationalError: FATAL: password authentication failed

Cause: Wrong password, wrong username, or PostgreSQL’s pg_hba.conf authentication method doesn’t allow password auth for local connections.

Fix: Re-check the .env credentials against what you set in Step 3. If they’re correct, check /etc/postgresql/<version>/main/pg_hba.conf — the line for local/127.0.0.1 connections should use md5 or scram-sha-256, not peer. Restart PostgreSQL after any change: sudo systemctl restart postgresql.

  • permission denied for schema public during migrate

Cause: On PostgreSQL 15+, the public schema is no longer writable by all users by default.

Fix: Re-run Step 9’s grant/ownership commands:

bash command

sudo -u postgres psql -d myprojectdb -c "GRANT ALL ON SCHEMA public TO myprojectuser;"
sudo -u postgres psql -d myprojectdb -c "ALTER SCHEMA public OWNER TO myprojectuser;"
  • Static files return 404 or don’t load

Cause: collectstatic wasn’t run, STATIC_ROOT doesn’t match the Nginx alias path, or file permissions block Nginx from reading them.

Fix:

  1. Confirm STATIC_ROOT in settings.py matches /var/www/project/staticfiles/ (or update the Nginx config to match).
  2. Re-run python manage.py collectstatic.
  3. Ensure the directory is readable by www-data: sudo chown -R www-data:www-data /var/www/project/staticfiles.
  • DisallowedHost error (“Invalid HTTP_HOST header”)

Cause: The domain/IP being used to access the site isn’t listed in DJANGO_ALLOWED_HOSTS.

Fix: Add the exact domain(s) and/or IP to DJANGO_ALLOWED_HOSTS in .env, then restart Gunicorn: sudo systemctl restart gunicorn.

  • ufw blocks the site or locks you out of SSH

Cause: Rules were added in the wrong order, or a curly-quoted app name ("Nginx Full" instead of 'Nginx Full') failed to register.

Fix: If you still have console/VNC access:

bash command

sudo ufw allow OpenSSH
sudo ufw allow 'Nginx Full'
sudo ufw reload
sudo ufw status verbose

If you’re fully locked out, use your hosting provider’s out-of-band console access to fix the rules.

  • certbot --nginx fails with “Could not automatically find a matching server block”

Cause: The server_name in the Nginx config doesn’t match the domain passed to Certbot, or the site isn’t enabled/reloaded yet.

Fix: Confirm the domain in server_name matches exactly, that the site is symlinked into sites-enabled, and that sudo nginx -t && sudo systemctl reload nginx completes cleanly before retrying Certbot.

  • nginx: [emerg] ... duplicate default server

Cause: The default Nginx site (/etc/nginx/sites-enabled/default) wasn’t removed and conflicts with the new site.

Fix:

bash command

sudo rm /etc/nginx/sites-enabled/default
sudo nginx -t
sudo systemctl restart nginx

Notes and Best Practices

  • Never run manage.py runserver in production. It’s single-threaded, unoptimized, and not designed to handle real traffic or serve static files securely — Gunicorn + Nginx exists specifically to replace it.
  • Rotate the Django secret key if it was ever committed to version control or shared insecurely, and keep it out of .env files that get backed up to unencrypted locations.
  • Keep .env out of Git. Add it to .gitignore before your first commit, and consider using a secrets manager for larger deployments.
  • Use --workers sized to your CPU, not an arbitrary number — over-provisioning workers on a small VPS can exhaust memory.
  • Automate certificate renewal verification periodically (sudo certbot renew --dry-run) even though Certbot sets up automatic renewal, since DNS or config changes can silently break it.
  • Back up your PostgreSQL database regularly (e.g., pg_dump) independent of this deployment process — this guide covers deployment, not disaster recovery.
  • Watch logs during first deployment: sudo journalctl -u gunicorn -f and sudo tail -f /var/log/nginx/error.log are the fastest way to catch issues live as you test.
  • Prefer scoped database privileges over GRANT ALL PRIVILEGES in real production environments — the broad grant in Step 3 is convenient for getting started but grants more than most applications need; consider tightening it once the deployment is stable.

Conclusion

By following the steps outlined above, you can deploy a Django application using a proven production stack consisting of Gunicorn, Nginx, PostgreSQL, and HTTPS. This architecture is well suited for small to medium-sized deployments, providing a reliable, secure, and maintainable foundation.

The key request flow is straightforward: Nginx terminates TLS, serves static assets directly, and proxies dynamic requests through a Unix socket to Gunicorn. Gunicorn runs the Django application, which interacts with PostgreSQL to store and retrieve data. Understanding this request path makes it easier to configure, troubleshoot, and scale your deployment as your application grows.

R
Rajiv Raut

eHostingServer team member.

Leave a Reply

Your email address will not be published. Required fields are marked *