Deploy Django application with Gunicorn and PostgreSQL database migrations

Intermediate 35 min Apr 04, 2026 276 views
Ubuntu 24.04 Debian 12 AlmaLinux 9 Rocky Linux 9

Learn how to deploy a production-ready Django application with Gunicorn WSGI server, PostgreSQL database, and automated database migrations on Ubuntu, Debian, AlmaLinux, and Rocky Linux systems.

Prerequisites

  • Root or sudo access
  • Basic Linux command line knowledge
  • Understanding of Django framework basics

What this solves

This tutorial walks you through deploying a Django application in production using Gunicorn as the WSGI server and PostgreSQL as the database backend. You'll configure systemd services for automatic startup, handle database migrations, and serve static files efficiently.

Step-by-step installation

Update system packages

Start by updating your package manager to ensure you get the latest versions of all packages.

sudo apt update && sudo apt upgrade -y
sudo dnf update -y

Install Python 3.12 and dependencies

Install Python 3.12, pip, and development tools needed for Django and PostgreSQL integration.

sudo apt install -y python3.12 python3.12-venv python3.12-dev python3-pip libpq-dev build-essential
sudo dnf install -y python3.12 python3.12-devel python3-pip postgresql-devel gcc

Install and configure PostgreSQL

Install PostgreSQL server and create a database and user for your Django application.

sudo apt install -y postgresql postgresql-contrib
sudo systemctl enable --now postgresql
sudo dnf install -y postgresql-server postgresql-contrib
sudo postgresql-setup --initdb
sudo systemctl enable --now postgresql

Create PostgreSQL database and user

Switch to the postgres user and create a dedicated database and user for your Django application.

sudo -u postgres psql

Run these SQL commands in the PostgreSQL shell:

CREATE DATABASE djangoapp;
CREATE USER djangouser WITH PASSWORD 'SecurePassword123!';
ALTER ROLE djangouser SET client_encoding TO 'utf8';
ALTER ROLE djangouser SET default_transaction_isolation TO 'read committed';
ALTER ROLE djangouser SET timezone TO 'UTC';
GRANT ALL PRIVILEGES ON DATABASE djangoapp TO djangouser;
\q

Create application user and directory

Create a dedicated user for running your Django application and set up the application directory structure.

sudo useradd --system --shell /bin/bash --home /opt/djangoapp --create-home djangoapp
sudo mkdir -p /opt/djangoapp/{app,logs,static,media}
sudo chown -R djangoapp:djangoapp /opt/djangoapp

Set up Python virtual environment

Switch to the djangoapp user and create a Python virtual environment for isolated package management.

sudo -u djangoapp bash
cd /opt/djangoapp
python3.12 -m venv venv
source venv/bin/activate
pip install --upgrade pip

Install Django and dependencies

Install Django, Gunicorn, PostgreSQL adapter, and other common Django dependencies.

pip install Django==5.0 gunicorn psycopg2-binary python-decouple whitenoise

Create Django project

Create a new Django project and configure the basic settings for production deployment.

cd /opt/djangoapp/app
django-admin startproject myproject .
exit

Configure Django settings

Create a production settings file that includes database configuration and static file handling.

import os
from decouple import config

BASE_DIR = Path(__file__).resolve().parent.parent

SECRET_KEY = config('SECRET_KEY', default='your-secret-key-here-change-in-production')

DEBUG = config('DEBUG', default=False, cast=bool)

ALLOWED_HOSTS = ['example.com', '203.0.113.10', 'localhost']

INSTALLED_APPS = [
    'django.contrib.admin',
    'django.contrib.auth',
    'django.contrib.contenttypes',
    'django.contrib.sessions',
    'django.contrib.messages',
    'whitenoise.runserver_nostatic',
    'django.contrib.staticfiles',
]

MIDDLEWARE = [
    'django.middleware.security.SecurityMiddleware',
    'whitenoise.middleware.WhiteNoiseMiddleware',
    'django.contrib.sessions.middleware.SessionMiddleware',
    'django.middleware.common.CommonMiddleware',
    'django.middleware.csrf.CsrfViewMiddleware',
    'django.contrib.auth.middleware.AuthenticationMiddleware',
    'django.contrib.messages.middleware.MessageMiddleware',
    'django.middleware.clickjacking.XFrameOptionsMiddleware',
]

ROOT_URLCONF = 'myproject.urls'

TEMPLATES = [
    {
        'BACKEND': 'django.template.backends.django.DjangoTemplates',
        'DIRS': [],
        'APP_DIRS': True,
        'OPTIONS': {
            'context_processors': [
                'django.template.context_processors.debug',
                'django.template.context_processors.request',
                'django.contrib.auth.context_processors.auth',
                'django.contrib.messages.context_processors.messages',
            ],
        },
    },
]

WSGI_APPLICATION = 'myproject.wsgi.application'

DATABASES = {
    'default': {
        'ENGINE': 'django.db.backends.postgresql',
        'NAME': config('DB_NAME', default='djangoapp'),
        'USER': config('DB_USER', default='djangouser'),
        'PASSWORD': config('DB_PASSWORD', default='SecurePassword123!'),
        'HOST': config('DB_HOST', default='localhost'),
        'PORT': config('DB_PORT', default='5432'),
    }
}

STATIC_URL = '/static/'
STATIC_ROOT = '/opt/djangoapp/static'
STATICFILES_STORAGE = 'whitenoise.storage.CompressedManifestStaticFilesStorage'

MEDIA_URL = '/media/'
MEDIA_ROOT = '/opt/djangoapp/media'

LOGGING = {
    'version': 1,
    'disable_existing_loggers': False,
    'handlers': {
        'file': {
            'level': 'INFO',
            'class': 'logging.FileHandler',
            'filename': '/opt/djangoapp/logs/django.log',
        },
    },
    'loggers': {
        'django': {
            'handlers': ['file'],
            'level': 'INFO',
            'propagate': True,
        },
    },
}

Create environment configuration

Create a .env file to store environment variables for database connection and Django settings.

SECRET_KEY=your-very-long-random-secret-key-here-change-this
DEBUG=False
DB_NAME=djangoapp
DB_USER=djangouser
DB_PASSWORD=SecurePassword123!
DB_HOST=localhost
DB_PORT=5432
sudo chown djangoapp:djangoapp /opt/djangoapp/app/.env
sudo chmod 600 /opt/djangoapp/app/.env

Run database migrations

Apply Django's default migrations to create the necessary database tables.

sudo -u djangoapp bash
cd /opt/djangoapp/app
source ../venv/bin/activate
python manage.py makemigrations
python manage.py migrate
python manage.py collectstatic --noinput
exit

Create Django superuser

Create an admin user for accessing the Django admin interface.

sudo -u djangoapp bash
cd /opt/djangoapp/app
source ../venv/bin/activate
python manage.py createsuperuser
exit

Configure Gunicorn

Create a Gunicorn configuration file with optimized settings for production deployment.

import multiprocessing

bind = "127.0.0.1:8000"
workers = multiprocessing.cpu_count() * 2 + 1
worker_class = "sync"
worker_connections = 1000
max_requests = 1000
max_requests_jitter = 100
preload_app = True
timeout = 30
keepalive = 2
user = "djangoapp"
group = "djangoapp"
tmp_upload_dir = None
logfile = "/opt/djangoapp/logs/gunicorn.log"
loglevel = "info"
proc_name = "djangoapp"
accesslog = "/opt/djangoapp/logs/gunicorn_access.log"
errorlog = "/opt/djangoapp/logs/gunicorn_error.log"
sudo chown djangoapp:djangoapp /opt/djangoapp/gunicorn.conf.py

Create systemd service for Gunicorn

Set up a systemd service to automatically start and manage your Gunicorn server.

[Unit]
Description=Gunicorn instance to serve Django App
After=network.target

[Service]
User=djangoapp
Group=djangoapp
WorkingDirectory=/opt/djangoapp/app
Environment="PATH=/opt/djangoapp/venv/bin"
ExecStart=/opt/djangoapp/venv/bin/gunicorn --config /opt/djangoapp/gunicorn.conf.py myproject.wsgi:application
ExecReload=/bin/kill -s HUP $MAINPID
Restart=always
RestartSec=3
KillMode=mixed
TimeoutStopSec=5
PrivateTmp=true
ProtectSystem=strict
ProtectHome=true
ReadWritePaths=/opt/djangoapp

[Install]
WantedBy=multi-user.target

Set up log rotation

Configure logrotate to manage Django and Gunicorn log files and prevent disk space issues.

/opt/djangoapp/logs/*.log {
    daily
    missingok
    rotate 52
    compress
    delaycompress
    notifempty
    create 0640 djangoapp djangoapp
    postrotate
        systemctl reload djangoapp
    endscript
}

Set correct file permissions

Ensure all files have the correct ownership and permissions for security and functionality.

sudo chown -R djangoapp:djangoapp /opt/djangoapp
sudo chmod 755 /opt/djangoapp
sudo chmod 644 /opt/djangoapp/app/manage.py
sudo chmod +x /opt/djangoapp/app/manage.py
sudo chmod 755 /opt/djangoapp/logs
sudo chmod 755 /opt/djangoapp/static
sudo chmod 755 /opt/djangoapp/media
Never use chmod 777. It gives every user on the system full access to your files. Instead, fix ownership with chown and use minimal permissions like 755 for directories and 644 for files.

Start and enable the service

Enable the systemd service to start automatically on boot and start it now.

sudo systemctl daemon-reload
sudo systemctl enable --now djangoapp
sudo systemctl status djangoapp

Configure firewall

Open the necessary firewall ports for your Django application (if using a reverse proxy, only internal access is needed).

sudo ufw allow 8000/tcp
sudo ufw reload
sudo firewall-cmd --permanent --add-port=8000/tcp
sudo firewall-cmd --reload

Verify your setup

Test your Django deployment by checking the service status, testing the application, and verifying database connectivity.

sudo systemctl status djangoapp
curl -I http://127.0.0.1:8000/
sudo -u djangoapp bash -c "cd /opt/djangoapp/app && source ../venv/bin/activate && python manage.py check"
sudo -u djangoapp bash -c "cd /opt/djangoapp/app && source ../venv/bin/activate && python manage.py showmigrations"

Database migration workflow

When you need to apply new migrations to your Django application, follow this workflow to ensure zero-downtime deployments.

# Create and apply migrations
sudo -u djangoapp bash -c "cd /opt/djangoapp/app && source ../venv/bin/activate && python manage.py makemigrations"
sudo -u djangoapp bash -c "cd /opt/djangoapp/app && source ../venv/bin/activate && python manage.py migrate"

Collect static files after code changes

sudo -u djangoapp bash -c "cd /opt/djangoapp/app && source ../venv/bin/activate && python manage.py collectstatic --noinput"

Reload the application

sudo systemctl reload djangoapp

Common issues

SymptomCauseFix
Service won't startPermission or path issuesCheck ownership with sudo systemctl status djangoapp and fix with sudo chown -R djangoapp:djangoapp /opt/djangoapp
Database connection errorWrong credentials or PostgreSQL not runningVerify PostgreSQL is running: sudo systemctl status postgresql and check credentials in .env file
Static files not loadingcollectstatic not run or wrong permissionsRun python manage.py collectstatic --noinput and check static directory permissions
502 Bad Gateway with reverse proxyGunicorn not listening on correct addressCheck bind address in gunicorn.conf.py matches proxy configuration
Application crashes on startMissing dependencies or import errorsCheck error logs: journalctl -u djangoapp -f and verify virtual environment

Next steps

Automated install script

Run this to automate the entire setup

Need help?

Don't want to manage this yourself?

We handle managed cloud infrastructure for businesses that depend on uptime. From initial setup to ongoing operations.