Custom websites, software and digital solutions for business growth. Start a Project

Deploy Django on Ubuntu VPS with GitHub, Gunicorn & Nginx

Learn how to deploy a Python Django application on an Ubuntu VPS using Git and GitHub. Set up SSH keys, PostgreSQL, Gunicorn, Nginx, HTTPS, and automated updates.

Ugochukwu Nomeh J
Updated 1 hour, 4 minutes ago
31 min read
Share
Deploy Django on Ubuntu VPS with GitHub, Gunicorn & Nginx

Photo: Photo by Reza Asadi on Unsplash

U
Ugochukwu Nomeh J
Last Updated on · Oct 28, 2026 · 31 min read
Follow

How to Deploy a Python Django Application on an Ubuntu VPS Using Git and GitHub

Deploying a Python Django application to a Virtual Private Server (VPS) involves more than uploading your source code. We need to configure the server, connect it to GitHub, install Python dependencies, prepare the database, configure environment variables, serve the application through Gunicorn, and use Nginx to handle incoming web requests.

In this guide, we will walk through the complete process of deploying a Django application on an Ubuntu VPS using Git and GitHub. We will start with a fresh server setup, configure SSH authentication, push our local project to GitHub, and retrieve it on the server. We will then configure the application for production and establish a repeatable workflow for deploying future updates.

This guide is designed for developers who build Django applications locally and want to host them on their own VPS rather than depend entirely on managed application hosting platforms.

By the end, we will have a foundation for running a Django application with:

  • Git and GitHub for source code management.
  • SSH keys for secure server and GitHub access.
  • Python and a virtual environment for application dependencies.
  • PostgreSQL for production database storage.
  • Gunicorn as the Django application server.
  • Nginx as the public-facing web server and reverse proxy.
  • Environment variables for production configuration and secrets.
  • HTTPS for encrypted connections.
  • Git-based updates whenever we change our application locally.

The commands below assume Ubuntu, a Django project with a conventional layout, and a domain name that points to the VPS. Some commands must be adjusted for your project's actual settings module, application structure, Python version, and deployment requirements.

1. Understand the Django Deployment Architecture

Before configuring the server, let's understand how the different components work together.

During development, we usually run Django locally using a command such as:

python manage.py runserver

This development server is intended for local development and testing. It should not be exposed as the production web server.

In production, our architecture will look like this:

  • Local computer: We develop, test, and modify our Django project.
  • Git: We track changes and create commits.
  • GitHub: We store the remote repository.
  • Ubuntu VPS: We install and configure the production environment.
  • Virtual environment: We isolate the Python dependencies required by the application.
  • PostgreSQL: We store production application data.
  • Gunicorn: We run Django as an application service.
  • Nginx: We receive public HTTP and HTTPS requests and forward application requests to Gunicorn.
  • systemd: We manage the Gunicorn service so it starts automatically and can be restarted when necessary.

Our deployment workflow will be:

  1. Develop and test the Django application locally.
  2. Commit changes using Git.
  3. Push the changes to GitHub.
  4. Pull the intended version onto the VPS.
  5. Install any new dependencies.
  6. Apply database migrations where required.
  7. Collect static files.
  8. Restart Gunicorn.
  9. Verify that the application is working correctly.

This gives us a repeatable deployment process without manually uploading the entire project for every change.

2. Prepare Your Ubuntu VPS

Start with an Ubuntu VPS that you can access through SSH. You will also need a domain name if you want to serve the application through your own domain and configure a trusted HTTPS certificate.

Connect to your server from your local computer:

ssh YOUR_SERVER_USER@YOUR_SERVER_IP

Replace the placeholders with your actual VPS username and IP address.

Before accepting a new SSH host key, verify its fingerprint through your hosting provider or another trusted source.

After connecting, update the package list:

sudo apt update

Install available package updates:

sudo apt upgrade -y

Install the core packages we will need:

sudo apt install -y \
    git \
    python3 \
    python3-pip \
    python3-venv \
    python3-dev \
    build-essential \
    libpq-dev \
    nginx \
    postgresql \
    postgresql-contrib \
    curl

These packages provide Git, Python, virtual environment support, common compilation tools, PostgreSQL development headers, Nginx, PostgreSQL, and utilities useful during setup.

Some Python packages require additional system libraries. If installation fails because a dependency is missing, identify the specific package requirement and install the appropriate operating system library.

Confirm the installed versions:

python3 --version
git --version
nginx -v
psql --version

Check that the systemd service manager is available:

systemctl --version

We will use systemd to manage the Gunicorn process later.

3. Create a Dedicated Deployment User

It is generally better to run application deployment commands as a dedicated Linux user rather than using the root account for everything.

Create a user named deploy:

sudo adduser deploy

Follow the prompts to set a password and complete the user creation process.

We will use this account to manage the application's source code and virtual environment.

Create the application directory:

sudo mkdir -p /var/www/my-django-app

Give the deployment user ownership of the application directory:

sudo chown deploy:deploy /var/www/my-django-app

Switch to the deployment user:

sudo -iu deploy

From this point, commands are run as the deploy user unless otherwise indicated. Administrative commands that require root privileges should be executed by an authorized administrator.

For a production environment, configure SSH access and administrative privileges according to your security requirements. Do not grant the deployment user unrestricted administrative access simply for convenience.

4. Install and Configure Git on the VPS

We installed Git earlier, but we should configure the identity used for commits made by this Linux account.

Set your preferred name:

git config --global user.name "Your Name"

Set your preferred email address:

git config --global user.email "you@example.com"

Check the configuration:

git config --global --list

These settings identify commit authors. They do not authenticate the VPS with GitHub.

For a deployment server that only retrieves code, we generally do not need to create commits on the VPS. However, setting a Git identity can be useful for administrative work performed under this account.

5. Generate an SSH Key for GitHub Access

We need to authorize the VPS to retrieve our private GitHub repository.

While logged in as the deploy user, generate a dedicated Ed25519 SSH key:

ssh-keygen -t ed25519 -C "django-production-vps" -f ~/.ssh/github_django

This creates two files:

  • ~/.ssh/github_django: the private key.
  • ~/.ssh/github_django.pub: the public key.

Set appropriate permissions:

chmod 700 ~/.ssh
chmod 600 ~/.ssh/github_django
chmod 644 ~/.ssh/github_django.pub

Display the public key:

cat ~/.ssh/github_django.pub

Copy the entire public key line.

Do not share the private key or add it to your repository.

6. Add the SSH Key to GitHub

For a production server that only needs to retrieve one Django application repository, a read-only deploy key is often a suitable choice.

  1. Open your Django repository on GitHub.
  2. Go to the repository settings.
  3. Open the Deploy keys section.
  4. Select the option to add a deploy key.
  5. Enter a descriptive title such as Django Production VPS.
  6. Paste the public key generated on the VPS.
  7. Leave write access disabled.
  8. Save the key.

Read the official GitHub deploy key documentation for further details.

Now configure SSH on the VPS to use the key:

nano ~/.ssh/config

Add:

Host github.com
    HostName github.com
    User git
    IdentityFile ~/.ssh/github_django
    IdentitiesOnly yes

Save the file, then restrict its permissions:

chmod 600 ~/.ssh/config

Test the connection:

ssh -T git@github.com

On the first connection, verify GitHub's SSH host key fingerprint against the fingerprints published in the official GitHub SSH key fingerprints documentation.

If authentication succeeds, the server is ready to access the repository using the configured key.

7. Prepare Your Django Project Locally

Before pushing our application to GitHub, we need to ensure that the repository contains the correct project files and excludes sensitive information.

On your local computer, navigate to the directory containing manage.py:

cd /path/to/your/django-project

Inspect the project files:

ls -la

A conventional Django project may look like this:

my-django-project/
├── manage.py
├── config/
│   ├── __init__.py
│   ├── settings.py
│   ├── urls.py
│   ├── asgi.py
│   └── wsgi.py
├── myapp/
│   ├── migrations/
│   ├── models.py
│   ├── views.py
│   └── urls.py
├── requirements.txt
├── .gitignore
└── .env.example

This is an example structure. Your project may use a different settings module, multiple Django apps, or a different dependency-management tool.

In this example, the Django settings module is config.settings, and the WSGI application is config.wsgi:application. We will use those names in subsequent commands. Replace them with the actual module names in your project.

Create a .gitignore File

We should exclude local environments, secret files, temporary files, and generated output that should not be tracked.

A possible starting point for a Django project is:

__pycache__/
*.py[cod]
*$py.class

.venv/
venv/
env/

.env
.env.*
!.env.example

db.sqlite3
*.log

staticfiles/
media/

.pytest_cache/
.coverage
htmlcov/

.idea/
.vscode/

Review these rules against your project before using them.

For example, if your application intentionally stores user-uploaded files inside a directory named media/, ensure that directory is backed up and persisted on the server. Excluding it from Git is appropriate in many deployments, but Git must not become the only storage location for important user uploads.

Similarly, do not exclude migrations indiscriminately. Django migration files are normally part of the application source code and should be committed.

Prepare the Requirements File

If your project uses requirements.txt, ensure it lists the packages required to run the application.

If you already maintain a complete, tested dependency file, use that rather than generating a new file unnecessarily.

For example, a Django project using PostgreSQL and Gunicorn might include dependencies such as:

Django
gunicorn
psycopg[binary]

This is an illustrative dependency list, not a production dependency lockfile. In a real application, use compatible versions that you have tested, and consider a reproducible dependency-management approach. Do not replace your existing dependency list with this minimal example if the project needs additional packages.

8. Push the Django Project to GitHub

If the project is not already a Git repository, initialize it:

git init

Check the repository status:

git status

Stage the files you want to commit:

git add .

Review the staged changes:

git diff --cached
git status

Make sure no secrets, local database files, private keys, or other sensitive information have been staged.

Create the initial commit:

git commit -m "Initial Django application commit"

Create a repository on GitHub if you have not already done so. For a project that already exists locally, an empty remote repository is often the simplest starting point.

Add the remote URL, replacing the placeholders:

git remote add origin git@github.com:YOUR_GITHUB_USERNAME/YOUR_REPOSITORY_NAME.git

Check the remote:

git remote -v

Check the current branch:

git branch --show-current

If your branch is named main, push it:

git push -u origin main

If your branch has another name, use that branch name instead.

Open GitHub and confirm that the project files have been uploaded.

9. Clone the Django Repository onto the VPS

We are now ready to download the project onto the server.

Confirm that the application directory exists and is owned by the deployment user. If you followed the earlier steps, it should already be available at /var/www/my-django-app.

If the directory is empty, clone the repository into it:

git clone git@github.com:YOUR_GITHUB_USERNAME/YOUR_REPOSITORY_NAME.git /var/www/my-django-app

If the directory already contains application files or a Git repository, stop and inspect it before cloning. Do not clone over an existing project directory without understanding what is already there.

Navigate into the application directory:

cd /var/www/my-django-app

Check the repository status:

git status

Check the latest commit:

git log -1 --oneline

At this point, the VPS has a local copy of the Django source code.

10. Create a Python Virtual Environment

Python virtual environments allow each project to maintain its own dependencies without mixing them with packages installed for other applications on the server.

From the application directory, create a virtual environment:

python3 -m venv .venv

Activate it:

source .venv/bin/activate

Your terminal prompt may now indicate that the virtual environment is active.

Upgrade the packaging tools:

python -m pip install --upgrade pip setuptools wheel

Install the project dependencies:

pip install -r requirements.txt

If the project does not have a requirements.txt file, use its actual dependency-management method rather than assuming this command will work.

Verify the installed Django version:

python -m django --version

When running Python commands for this project, make sure you use the correct virtual environment. This prevents the application from accidentally using packages installed for another project or for the operating system.

11. Configure PostgreSQL for Django

SQLite is useful for local development and smaller use cases, but for a typical production Django application, we will use PostgreSQL.

PostgreSQL was installed earlier. Check that the service is running:

sudo systemctl status postgresql

If necessary, start it:

sudo systemctl enable --now postgresql

Next, create a database and a dedicated database user.

Open the PostgreSQL administrative shell:

sudo -u postgres psql

Inside the PostgreSQL shell, create a database user. Replace the example password with a strong, unique password:

CREATE USER django_user WITH PASSWORD 'REPLACE_WITH_A_STRONG_PASSWORD';

Create the application database:

CREATE DATABASE django_db OWNER django_user;

Exit PostgreSQL:

\q

Do not use the example password in a real deployment. Generate a unique password and store it in a secure configuration location. Avoid putting the real password in shell history, source code, or documentation.

The database name and username used above are examples. You can choose different names, but they must match your Django configuration.

12. Configure Django Environment Variables

Production settings should not depend on hard-coded secrets stored in the repository.

Our application will need to know values such as:

  • The production secret key.
  • Whether Django is running in debug mode.
  • The domain names permitted to serve the application.
  • The database connection details.
  • The location of static and uploaded media files.

There are different ways to provide environment variables to Django. For this deployment, we will use a dedicated environment file outside the Git repository and load it through systemd.

Create a configuration directory:

sudo mkdir -p /etc/my-django-app

Assign ownership to root and restrict access:

sudo chown root:root /etc/my-django-app
sudo chmod 700 /etc/my-django-app

Create the environment file as an administrator:

sudo nano /etc/my-django-app/app.env

Add the required values:

DJANGO_SECRET_KEY=REPLACE_WITH_A_LONG_RANDOM_SECRET
DJANGO_DEBUG=False
DJANGO_ALLOWED_HOSTS=example.com,www.example.com
DB_NAME=django_db
DB_USER=django_user
DB_PASSWORD=REPLACE_WITH_YOUR_DATABASE_PASSWORD
DB_HOST=127.0.0.1
DB_PORT=5432

Replace the example domain names and credentials with your actual values.

Generate a suitable Django secret key using a trusted method, such as a secure password generator or Python's cryptographic facilities. Keep the key private and do not reuse a development secret key in production.

Restrict the environment file:

sudo chown root:root /etc/my-django-app/app.env
sudo chmod 600 /etc/my-django-app/app.env

We will configure systemd to read this file and provide the values to Gunicorn.

Important: Django does not automatically load arbitrary .env files. Your settings must explicitly read environment variables, or your deployment must provide them through another supported mechanism.

13. Update Django Settings for Production

Now we need to make sure that our Django settings use the production environment variables.

The exact implementation depends on your project. The following examples illustrate the required configuration for a conventional Django project.

Read the Secret Key and Debug Setting

In your Django settings module, read the secret key from the environment:

import os

SECRET_KEY = os.environ["DJANGO_SECRET_KEY"]

DEBUG = os.environ.get("DJANGO_DEBUG", "False").lower() == "true"

This approach intentionally requires the secret key to be present. If it is missing, Django will fail to start rather than silently using an insecure fallback.

Configure Allowed Hosts

Set the allowed host names from the environment:

ALLOWED_HOSTS = [
    host.strip()
    for host in os.environ.get("DJANGO_ALLOWED_HOSTS", "").split(",")
    if host.strip()
]

For our example, the environment file supplies:

DJANGO_ALLOWED_HOSTS=example.com,www.example.com

Do not set ALLOWED_HOSTS to a wildcard simply to bypass configuration errors in production.

Configure PostgreSQL

Update the database configuration in your Django settings module:

DATABASES = {
    "default": {
        "ENGINE": "django.db.backends.postgresql",
        "NAME": os.environ["DB_NAME"],
        "USER": os.environ["DB_USER"],
        "PASSWORD": os.environ["DB_PASSWORD"],
        "HOST": os.environ.get("DB_HOST", "127.0.0.1"),
        "PORT": os.environ.get("DB_PORT", "5432"),
    }
}

This configuration uses the PostgreSQL driver installed through your project's dependency file.

Make sure the database credentials match the database and user created earlier.

Configure Static Files

Django's static files system collects application assets into a directory that can be served separately from the dynamic application.

A common production configuration is:

STATIC_URL = "/static/"
STATIC_ROOT = BASE_DIR / "staticfiles"

Depending on your project, you may also have a STATICFILES_DIRS setting that points to additional directories containing source static assets.

Keep STATIC_ROOT separate from directories that contain the original static source files.

Configure Uploaded Media Files

If users upload images, documents, or other files, configure the media URL and root directory:

MEDIA_URL = "/media/"
MEDIA_ROOT = BASE_DIR / "media"

For production, consider placing uploaded media in a persistent directory outside version-controlled release directories. Make sure it is backed up and served through an appropriate web server or object storage service.

Never assume that a Git pull will preserve uploaded files if your deployment process replaces the application directory.

Configure HTTPS-Aware Settings

When HTTPS is configured, Django may need to recognize that requests forwarded by Nginx use HTTPS externally.

For a deployment where Nginx terminates TLS and forwards the appropriate trusted header, the settings may include:

SECURE_PROXY_SSL_HEADER = ("HTTP_X_FORWARDED_PROTO", "https")

Only use this setting when the application is behind a trusted proxy that correctly sets or overwrites the relevant header. The application must not blindly trust client-supplied forwarding headers.

After HTTPS is working, consider enabling secure session and CSRF cookies:

SESSION_COOKIE_SECURE = True
CSRF_COOKIE_SECURE = True

Do not enable these settings before understanding the implications for local development and any non-HTTPS environment where the application is expected to run.

14. Run Django's Deployment Checks

Before starting the application, run Django's built-in deployment checks.

Activate the virtual environment if it is not already active:

cd /var/www/my-django-app
source .venv/bin/activate

Load the same environment variables that will be used by the application. For a simple manual check, an authorized administrator can load the environment file in a controlled shell or use a small deployment wrapper that reads it securely.

Do not assume that a shell automatically reads the systemd environment file. The file will be loaded by systemd when Gunicorn starts.

Once the required environment variables are available in the current shell, run:

python manage.py check --deploy

This command checks for several potential deployment issues. The exact warnings depend on your Django version and settings.

Resolve the relevant warnings before going live. A successful check does not guarantee that the entire application is secure, but it is an important part of production preparation.

You can also verify the application configuration with:

python manage.py check

If the application reports a missing setting, database connection error, or missing Python package, correct the underlying configuration before proceeding.

15. Run Database Migrations

Django migrations synchronize the database schema with the application's migration files.

Before running migrations in production, make sure that the database exists, the credentials are correct, and appropriate backups are available when necessary.

With the virtual environment active and the production environment variables loaded, inspect the migration plan:

python manage.py showmigrations

Then apply the migrations:

python manage.py migrate

If the project uses a different database configuration in local development and production, verify that the command is connecting to the intended production database.

Never assume that a successful migration on your local computer means the production database has already been migrated.

16. Collect Static Files

In production, Django's static files should be collected into the directory configured by STATIC_ROOT.

Run:

python manage.py collectstatic --noinput

This command collects eligible static files from installed applications and configured static source directories.

It does not automatically configure Nginx to serve the files. We will do that in the next steps.

If your project uses a frontend build process, run that process before collecting static files when required.

17. Install and Configure Gunicorn

Gunicorn is a Python WSGI HTTP server that can run a Django application in production. Nginx will forward application requests to it.

If Gunicorn is not already listed in the project's dependencies, add a compatible version to the dependency file and commit the change through your normal development workflow.

For a project using requirements.txt, install the dependencies again:

pip install -r requirements.txt

Confirm that Gunicorn is available:

gunicorn --version

For our example, the Django WSGI module is config.wsgi. Replace this with your actual module if it differs.

Test Gunicorn manually using a Unix socket:

gunicorn --bind 127.0.0.1:8000 config.wsgi:application

This starts Gunicorn in the foreground. It is a temporary test, not the final production service.

From another terminal, you can test whether the local application responds:

curl -I http://127.0.0.1:8000/

If the application starts successfully, stop the foreground Gunicorn process using Ctrl + C.

If the command fails, inspect the error message. Common causes include an incorrect WSGI module, missing dependencies, invalid settings, or missing environment variables.

18. Create a systemd Service for Gunicorn

We do not want to start Gunicorn manually every time the server reboots. Instead, we will create a systemd service.

Exit the deployment user's shell if necessary, or open a separate administrator session. Create the service file:

sudo nano /etc/systemd/system/my-django-app.service

Add the following configuration:

[Unit]
Description=Gunicorn service for Django application
After=network.target postgresql.service

[Service]
User=deploy
Group=www-data
WorkingDirectory=/var/www/my-django-app
EnvironmentFile=/etc/my-django-app/app.env
ExecStart=/var/www/my-django-app/.venv/bin/gunicorn \
    --workers 3 \
    --bind unix:/run/my-django-app.sock \
    config.wsgi:application

Restart=on-failure
RestartSec=5

[Install]
WantedBy=multi-user.target

This example assumes:

  • The deployment user is named deploy.
  • The project directory is /var/www/my-django-app.
  • The virtual environment is located at /var/www/my-django-app/.venv.
  • The WSGI module is config.wsgi:application.
  • The application needs access to the environment file created earlier.

Replace any values that do not match your project.

The --workers 3 option is only a starting point. Choose the worker count based on available memory, CPU capacity, request patterns, and application behavior. More workers are not automatically better.

The socket will be created in /run, which is a temporary runtime directory managed by the operating system. In this basic example, verify that the service can create the socket with permissions that allow Nginx to connect. If the socket directory or permissions require additional configuration, use a suitable systemd runtime directory or socket unit.

Save the file and reload systemd:

sudo systemctl daemon-reload

Enable the service at startup:

sudo systemctl enable my-django-app

Start the service:

sudo systemctl start my-django-app

Check its status:

sudo systemctl status my-django-app

If the service fails, inspect the logs:

sudo journalctl -u my-django-app -n 100 --no-pager

These logs are often the fastest way to identify a missing environment variable, incorrect module path, database connection failure, or file permission problem.

19. Configure Nginx as a Reverse Proxy

Gunicorn runs the Django application, but we still need a public-facing web server. We will use Nginx to handle incoming requests and forward dynamic requests to Gunicorn.

Create an Nginx configuration file:

sudo nano /etc/nginx/sites-available/my-django-app

Add the following configuration, replacing the domain names with your actual domain:

server {
    listen 80;
    listen [::]:80;

    server_name example.com www.example.com;

    location /static/ {
        alias /var/www/my-django-app/staticfiles/;
    }

    location /media/ {
        alias /var/www/my-django-app/media/;
    }

    location / {
        include proxy_params;
        proxy_pass http://unix:/run/my-django-app.sock;
    }
}

This configuration:

  • Accepts HTTP requests for the configured domain names.
  • Serves collected static files directly through Nginx.
  • Serves uploaded media files from the configured media directory.
  • Forwards other requests to Gunicorn through the Unix socket.

Make sure the static files directory and media directory exist as expected. If your application does not use uploaded media, you can remove the media location block.

In production, ensure that Nginx can read the files it serves and that the application has appropriate write access to persistent upload directories where required.

Enable the configuration:

sudo ln -s /etc/nginx/sites-available/my-django-app /etc/nginx/sites-enabled/

If the default Nginx site is enabled and conflicts with your configuration, inspect it before disabling it. Do not remove another site's configuration if the server hosts multiple applications.

Test the Nginx configuration:

sudo nginx -t

If the test succeeds, reload Nginx:

sudo systemctl reload nginx

Check that the service is running:

sudo systemctl status nginx

If Nginx returns a gateway error, inspect both the Nginx logs and the Gunicorn service logs.

20. Point Your Domain to the VPS

If you want to access the Django application through a domain name, configure the domain's DNS records to point to the server.

Typically, you will configure:

  • An A record for the root domain, pointing to the VPS IPv4 address.
  • An AAAA record only if the VPS has a properly configured IPv6 address.
  • A record for www if you intend to serve that hostname.

The exact DNS records depend on your domain provider and whether you use a DNS proxy or another hosting service.

After DNS changes propagate, test the domain in your browser.

You can also test the HTTP response from a terminal:

curl -I http://example.com

Replace the example domain with your actual domain.

Before configuring HTTPS, make sure the domain resolves to the correct server and that Nginx is accepting connections on the required port.

21. Configure HTTPS with Let's Encrypt

HTTPS encrypts traffic between your visitors and the server. For a public production application, it should be part of the deployment process.

One common option is Let's Encrypt, which provides trusted TLS certificates.

On Ubuntu, the recommended installation method can vary by Ubuntu release and Certbot packaging. Follow the current official Certbot instructions for your operating system and Nginx.

After installing Certbot according to the official instructions, you can use the Nginx integration to request a certificate for your domain. A common command when the relevant Certbot Nginx plugin is installed is:

sudo certbot --nginx -d example.com -d www.example.com

Use only the domain names that you own and have correctly configured to reach this server.

Follow the prompts to complete certificate issuance and configure HTTPS.

Once the certificate is installed, test the site over HTTPS:

curl -I https://example.com

Check the certificate renewal configuration and perform the supported renewal test for your Certbot installation. Do not assume that certificate renewal will work without verification.

After HTTPS is working, ensure your Django proxy settings and secure cookie settings are configured appropriately.

22. Configure the Firewall

Your VPS should expose only the network services that need to be reachable from outside.

If you use UFW, first inspect the current firewall rules and confirm that you will not lock yourself out of SSH.

Allow SSH:

sudo ufw allow OpenSSH

Allow HTTP:

sudo ufw allow 80/tcp

Allow HTTPS:

sudo ufw allow 443/tcp

Check the current firewall status:

sudo ufw status

If UFW is not enabled and you have verified that the necessary SSH access is allowed, you can enable it:

sudo ufw enable

Do not expose PostgreSQL publicly unless your architecture specifically requires it and you have configured appropriate network restrictions and authentication. For a Django application whose database runs on the same VPS, PostgreSQL should normally remain inaccessible from the public internet.

23. Test the Complete Django Deployment

We have configured the main components. Now we need to confirm that the application is working correctly.

Check the Gunicorn service:

sudo systemctl status my-django-app

Check the Nginx service:

sudo systemctl status nginx

Check the PostgreSQL service:

sudo systemctl status postgresql

Inspect recent Gunicorn logs:

sudo journalctl -u my-django-app -n 100 --no-pager

Inspect Nginx errors:

sudo tail -n 100 /var/log/nginx/error.log

Test the public application:

curl -I https://example.com

Open the domain in your browser and test important parts of the application, including login, forms, database-backed pages, static files, and any integrations.

A successful HTTP response alone does not prove that every part of the application is functioning correctly. Verify the application features that matter to your users.

24. How to Update the Django Application After Pushing Changes to GitHub

Once the application is running, the next task is keeping it updated whenever we make changes to the source code.

Suppose we modify a Django view, add a feature, update a template, or fix a bug on our local computer.

The deployment process begins locally.

Step 1: Commit and Push Changes Locally

Navigate to the local project directory:

cd /path/to/your/django-project

Check the current status:

git status

Stage the intended changes:

git add .

Commit them:

git commit -m "Update Django application"

Push them to GitHub:

git push

Make sure the push succeeds before moving to the server.

Step 2: Connect to the VPS

Connect to the server using your SSH account:

ssh YOUR_SERVER_USER@YOUR_SERVER_IP

Switch to the deployment user if necessary:

sudo -iu deploy

Navigate to the application directory:

cd /var/www/my-django-app

Step 3: Check the Repository State

Before pulling changes, check whether the working tree contains modifications:

git status

If the working tree is clean, retrieve the latest remote information:

git fetch origin

Confirm the current branch:

git branch --show-current

If the application is deployed from main, pull the intended branch:

git pull --ff-only origin main

The --ff-only option allows the update only when Git can advance the current branch without creating a merge commit. If the local branch has diverged, Git stops instead of silently merging unrelated changes.

If your production branch is not main, replace the branch name with the correct one.

If Git refuses to update the repository, investigate the branch history and local changes before proceeding. Do not automatically discard server changes or force the branch to match GitHub.

Step 4: Activate the Virtual Environment

Activate the project's virtual environment:

source .venv/bin/activate

Step 5: Install Updated Dependencies

If the deployment includes changes to the dependency file, install the dependencies:

pip install -r requirements.txt

For a project with a committed, reproducible dependency file, use the dependency-management workflow that matches the project.

Step 6: Apply Database Migrations

If the deployment includes new Django migrations, review them before applying them to the production database.

Inspect the migrations:

python manage.py showmigrations

Apply the required migrations:

python manage.py migrate

Only run database migrations after confirming that the command uses the intended production configuration and database.

Step 7: Collect Static Files

If the application uses Django's static files system, collect the static assets:

python manage.py collectstatic --noinput

This step is particularly important when templates, CSS, JavaScript, or other static assets have changed.

Step 8: Restart Gunicorn

Restart the application service so that the running application loads the updated Python code:

sudo systemctl restart my-django-app

Check the service status:

sudo systemctl status my-django-app

Inspect the logs if the service fails:

sudo journalctl -u my-django-app -n 100 --no-pager

Step 9: Verify the Application

Test the application through its public domain:

curl -I https://example.com

Then open the site and verify the affected functionality.

Our basic manual deployment sequence is now:

git fetch origin
git pull --ff-only origin main
source .venv/bin/activate
pip install -r requirements.txt
python manage.py migrate
python manage.py collectstatic --noinput
sudo systemctl restart my-django-app

This is a reference sequence, not a universal command that should be copied blindly. Run it from the correct application directory, as the appropriate deployment user, with the production environment configured. Review dependency changes and migrations before applying them, and skip steps that are not relevant to the specific release.

25. Common Django Deployment Problems

502 Bad Gateway

A 502 response often means Nginx cannot communicate successfully with Gunicorn.

Check the application service:

sudo systemctl status my-django-app

Inspect the Gunicorn logs:

sudo journalctl -u my-django-app -n 100 --no-pager

Check the Nginx error log:

sudo tail -n 100 /var/log/nginx/error.log

Common causes include a failed Gunicorn process, an incorrect socket path, or socket permissions that prevent Nginx from connecting.

DisallowedHost Error

If Django reports a DisallowedHost error, check the ALLOWED_HOSTS setting.

Ensure that the requested hostname is included in the configured list and that the production environment variables are loaded correctly.

Static Files Are Missing

If CSS, JavaScript, or images are missing, check the static files configuration:

python manage.py collectstatic --noinput

Verify that STATIC_ROOT matches the directory referenced by Nginx and that Nginx can read the files.

For example, if Django uses /var/www/my-django-app/staticfiles, the Nginx alias must point to the corresponding directory.

Database Connection Errors

If Django cannot connect to PostgreSQL, check the database service and environment variables.

sudo systemctl status postgresql

Verify the configured database name, username, password, host, and port.

Also ensure that the PostgreSQL user has the permissions required by the application.

ModuleNotFoundError

If Python reports that a module cannot be found, confirm that you are using the correct virtual environment and that the dependency is installed.

source .venv/bin/activate
pip install -r requirements.txt

If the error references your own Django application module, verify the repository structure, Python import paths, and WSGI configuration.

Gunicorn Fails After a Git Pull

If Gunicorn stops working after an update, inspect its logs before attempting repeated restarts.

sudo journalctl -u my-django-app -n 100 --no-pager

Check whether the update introduced a missing dependency, invalid settings, a database error, an incorrect WSGI path, or a configuration change.

If a deployment causes an outage, follow your recovery procedure. Do not assume that reverting the Git commit alone will undo database migrations or other changes to persistent data.

26. Security Practices for Production Django Applications

Deploying successfully is only one part of running a production application. We also need to protect the server, database, application secrets, and user data.

  • Keep debug mode disabled: Production should use DEBUG=False.
  • Protect the secret key: Keep the production Django secret key outside the repository.
  • Use a restrictive allowed-host list: Configure the hostnames the application is intended to serve.
  • Use HTTPS: Configure a trusted TLS certificate and verify renewal.
  • Protect environment variables: Restrict access to files containing database credentials and application secrets.
  • Restrict database access: Do not expose PostgreSQL to the public internet without a specific, secured requirement.
  • Keep dependencies updated: Review security updates and compatibility before upgrading production packages.
  • Back up the database: Git does not back up production database records.
  • Back up uploaded files: User-uploaded media must be stored persistently and backed up separately.
  • Review deployment changes: Test important changes and understand the effects of database migrations before releasing them.

Run Django's deployment checks before releasing changes:

python manage.py check --deploy

Use the results to identify configuration problems, while remembering that these checks do not replace a complete security review.

27. Can We Automate Django Deployments?

Yes. Once the manual deployment process is reliable, we can automate it using a continuous integration and continuous deployment workflow.

For example, GitHub Actions can run tests when code is pushed and initiate a deployment when changes are merged into a designated production branch.

A Django deployment pipeline can include:

  1. Installing Python dependencies.
  2. Running automated tests.
  3. Checking code quality and configuration.
  4. Building any required frontend assets.
  5. Deploying the approved application version.
  6. Applying reviewed database migrations.
  7. Collecting static files.
  8. Restarting or reloading the application.
  9. Performing a health check.

Deployment credentials must be stored securely, and the workflow should be restricted to trusted branches and authorized deployment events.

For a production application, consider a release-based deployment structure that keeps the current release available until the new release has passed the necessary checks.

Automated deployments should also account for persistent data, database migrations, background workers, and rollback procedures.

28. Final Deployment Checklist

Before considering the Django application ready for production, confirm the following:

  • Git is installed and configured on the VPS.
  • The VPS can authenticate with GitHub using the intended SSH key.
  • The repository excludes secrets and unnecessary local files.
  • The project is deployed into the correct directory.
  • The Python virtual environment is configured correctly.
  • All required dependencies are installed.
  • PostgreSQL is configured and the application can connect to it.
  • Production environment variables are loaded securely.
  • Django's production settings are configured correctly.
  • Database migrations have been reviewed and applied where necessary.
  • Static files have been collected.
  • Gunicorn is running through systemd.
  • Nginx is configured to serve the domain and proxy application requests.
  • DNS points to the correct server.
  • HTTPS is working and certificate renewal is configured.
  • The firewall allows only the required network traffic.
  • Application logs and health checks are available.
  • Database and media backups are configured.
  • The deployment and recovery procedures have been tested.

Conclusion

Deploying a Python Django application on an Ubuntu VPS becomes much easier when we establish a consistent workflow using Git and GitHub.

We have covered the process from preparing the server and configuring SSH authentication to deploying Django with PostgreSQL, Gunicorn, Nginx, environment variables, static files, and HTTPS.

We have also established how to update the application after pushing changes to GitHub, apply migrations, install updated dependencies, restart the application, and troubleshoot common deployment errors.

The most important lesson is that a production deployment is more than a Git pull. Git manages our source code, but a reliable Django deployment must also account for dependencies, configuration, database changes, static assets, running processes, security, and persistent data.

Start by mastering the manual workflow. Once it is predictable and reliable, introduce automated testing and deployment to reduce repetitive work and make releases easier to manage.

With a properly configured environment and a clear deployment process, you can manage updates to your Django applications without manually uploading files every time you make a change.

Official Documentation

Filed Under

Withubb Blog Tech Insights