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
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
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
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
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
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
| Symptom | Cause | Fix |
|---|---|---|
| Service won't start | Permission or path issues | Check ownership with sudo systemctl status djangoapp and fix with sudo chown -R djangoapp:djangoapp /opt/djangoapp |
| Database connection error | Wrong credentials or PostgreSQL not running | Verify PostgreSQL is running: sudo systemctl status postgresql and check credentials in .env file |
| Static files not loading | collectstatic not run or wrong permissions | Run python manage.py collectstatic --noinput and check static directory permissions |
| 502 Bad Gateway with reverse proxy | Gunicorn not listening on correct address | Check bind address in gunicorn.conf.py matches proxy configuration |
| Application crashes on start | Missing dependencies or import errors | Check error logs: journalctl -u djangoapp -f and verify virtual environment |
Next steps
- Setup nginx reverse proxy with SSL certificates and security hardening
- Optimize PostgreSQL connection pooling with PgBouncer for high-traffic applications
- Configure Django Redis caching and session storage
- Implement Django continuous deployment with Git hooks
- Monitor Django applications with Prometheus and Grafana
Automated install script
Run this to automate the entire setup
#!/usr/bin/env bash
set -euo pipefail
# Django + Gunicorn + PostgreSQL Deployment Script
# Usage: ./install.sh [domain] [db_password]
# Color codes
RED='\033[0;31m'
GREEN='\033[0;32m'
YELLOW='\033[0;33m'
NC='\033[0m'
# Default values
DOMAIN=${1:-"localhost"}
DB_PASSWORD=${2:-"SecurePassword123!"}
DJANGO_USER="djangoapp"
DJANGO_HOME="/opt/djangoapp"
PROJECT_NAME="myproject"
# Cleanup function
cleanup() {
echo -e "${RED}[ERROR] Installation failed. Cleaning up...${NC}"
systemctl stop gunicorn.service 2>/dev/null || true
systemctl disable gunicorn.service 2>/dev/null || true
userdel -r $DJANGO_USER 2>/dev/null || true
sudo -u postgres dropdb djangoapp 2>/dev/null || true
sudo -u postgres dropuser djangouser 2>/dev/null || true
exit 1
}
trap cleanup ERR
# Check root privileges
if [[ $EUID -ne 0 ]]; then
echo -e "${RED}This script must be run as root${NC}"
exit 1
fi
# Auto-detect distribution
if [ -f /etc/os-release ]; then
. /etc/os-release
case "$ID" in
ubuntu|debian)
PKG_MGR="apt"
PKG_UPDATE="apt update && apt upgrade -y"
PKG_INSTALL="apt install -y"
PG_SERVICE="postgresql"
PG_SETUP=""
PYTHON_DEV="python3.12-dev"
PG_DEV="libpq-dev"
BUILD_TOOLS="build-essential"
;;
almalinux|rocky|centos|rhel|ol)
PKG_MGR="dnf"
PKG_UPDATE="dnf update -y"
PKG_INSTALL="dnf install -y"
PG_SERVICE="postgresql"
PG_SETUP="postgresql-setup --initdb"
PYTHON_DEV="python3.12-devel"
PG_DEV="postgresql-devel"
BUILD_TOOLS="gcc"
;;
fedora)
PKG_MGR="dnf"
PKG_UPDATE="dnf update -y"
PKG_INSTALL="dnf install -y"
PG_SERVICE="postgresql"
PG_SETUP=""
PYTHON_DEV="python3.12-devel"
PG_DEV="postgresql-devel"
BUILD_TOOLS="gcc"
;;
amzn)
PKG_MGR="yum"
PKG_UPDATE="yum update -y"
PKG_INSTALL="yum install -y"
PG_SERVICE="postgresql"
PG_SETUP=""
PYTHON_DEV="python3.12-devel"
PG_DEV="postgresql-devel"
BUILD_TOOLS="gcc"
;;
*)
echo -e "${RED}Unsupported distribution: $ID${NC}"
exit 1
;;
esac
else
echo -e "${RED}Cannot detect OS distribution${NC}"
exit 1
fi
echo -e "${GREEN}Starting Django deployment on $ID...${NC}"
# Step 1: Update system
echo -e "${YELLOW}[1/10] Updating system packages...${NC}"
$PKG_UPDATE
# Step 2: Install Python and dependencies
echo -e "${YELLOW}[2/10] Installing Python 3.12 and dependencies...${NC}"
$PKG_INSTALL python3.12 python3.12-venv $PYTHON_DEV python3-pip $PG_DEV $BUILD_TOOLS
# Step 3: Install PostgreSQL
echo -e "${YELLOW}[3/10] Installing PostgreSQL...${NC}"
$PKG_INSTALL postgresql-server postgresql-contrib
if [[ -n "$PG_SETUP" ]]; then
sudo $PG_SETUP 2>/dev/null || true
fi
systemctl enable --now $PG_SERVICE
# Step 4: Create database and user
echo -e "${YELLOW}[4/10] Setting up PostgreSQL database...${NC}"
sleep 3 # Wait for PostgreSQL to start
sudo -u postgres psql -c "CREATE DATABASE djangoapp;" 2>/dev/null || true
sudo -u postgres psql -c "CREATE USER djangouser WITH PASSWORD '$DB_PASSWORD';" 2>/dev/null || true
sudo -u postgres psql -c "ALTER ROLE djangouser SET client_encoding TO 'utf8';"
sudo -u postgres psql -c "ALTER ROLE djangouser SET default_transaction_isolation TO 'read committed';"
sudo -u postgres psql -c "ALTER ROLE djangouser SET timezone TO 'UTC';"
sudo -u postgres psql -c "GRANT ALL PRIVILEGES ON DATABASE djangoapp TO djangouser;"
# Step 5: Create application user and directories
echo -e "${YELLOW}[5/10] Creating application user and directories...${NC}"
useradd --system --shell /bin/bash --home $DJANGO_HOME --create-home $DJANGO_USER 2>/dev/null || true
mkdir -p $DJANGO_HOME/{app,logs,static,media}
chown -R $DJANGO_USER:$DJANGO_USER $DJANGO_HOME
chmod 755 $DJANGO_HOME
chmod 755 $DJANGO_HOME/{app,logs,static,media}
# Step 6: Set up Python virtual environment
echo -e "${YELLOW}[6/10] Setting up Python virtual environment...${NC}"
sudo -u $DJANGO_USER python3.12 -m venv $DJANGO_HOME/venv
sudo -u $DJANGO_USER $DJANGO_HOME/venv/bin/pip install --upgrade pip
# Step 7: Install Django and dependencies
echo -e "${YELLOW}[7/10] Installing Django and dependencies...${NC}"
sudo -u $DJANGO_USER $DJANGO_HOME/venv/bin/pip install Django==5.0 gunicorn psycopg2-binary python-decouple whitenoise
# Step 8: Create Django project
echo -e "${YELLOW}[8/10] Creating Django project...${NC}"
cd $DJANGO_HOME/app
sudo -u $DJANGO_USER $DJANGO_HOME/venv/bin/django-admin startproject $PROJECT_NAME .
# Create settings.py
sudo -u $DJANGO_USER tee $DJANGO_HOME/app/$PROJECT_NAME/settings.py > /dev/null << 'EOF'
from pathlib import Path
import os
from decouple import config
BASE_DIR = Path(__file__).resolve().parent.parent
SECRET_KEY = config('SECRET_KEY', default='django-insecure-change-me-in-production')
DEBUG = config('DEBUG', default=False, cast=bool)
ALLOWED_HOSTS = ['*']
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',
],
},
},
]
DATABASES = {
'default': {
'ENGINE': 'django.db.backends.postgresql',
'NAME': 'djangoapp',
'USER': 'djangouser',
'PASSWORD': 'SecurePassword123!',
'HOST': 'localhost',
'PORT': '5432',
}
}
STATIC_URL = '/static/'
STATIC_ROOT = '/opt/djangoapp/static'
MEDIA_URL = '/media/'
MEDIA_ROOT = '/opt/djangoapp/media'
STATICFILES_STORAGE = 'whitenoise.storage.CompressedManifestStaticFilesStorage'
EOF
# Run migrations and collect static files
sudo -u $DJANGO_USER $DJANGO_HOME/venv/bin/python $DJANGO_HOME/app/manage.py migrate
sudo -u $DJANGO_USER $DJANGO_HOME/venv/bin/python $DJANGO_HOME/app/manage.py collectstatic --noinput
# Step 9: Create systemd service
echo -e "${YELLOW}[9/10] Creating systemd service...${NC}"
tee /etc/systemd/system/gunicorn.service > /dev/null << EOF
[Unit]
Description=Gunicorn daemon for Django project
Requires=network.target
After=network.target
[Service]
User=$DJANGO_USER
Group=$DJANGO_USER
WorkingDirectory=$DJANGO_HOME/app
Environment="PATH=$DJANGO_HOME/venv/bin"
ExecStart=$DJANGO_HOME/venv/bin/gunicorn --workers 3 --bind 0.0.0.0:8000 $PROJECT_NAME.wsgi:application
ExecReload=/bin/kill -s HUP \$MAINPID
KillMode=mixed
TimeoutStopSec=5
PrivateTmp=true
[Install]
WantedBy=multi-user.target
EOF
chmod 644 /etc/systemd/system/gunicorn.service
systemctl daemon-reload
systemctl enable gunicorn.service
systemctl start gunicorn.service
# Step 10: Verification
echo -e "${YELLOW}[10/10] Running verification checks...${NC}"
sleep 2
# Check service status
if systemctl is-active --quiet gunicorn.service; then
echo -e "${GREEN}✓ Gunicorn service is running${NC}"
else
echo -e "${RED}✗ Gunicorn service failed to start${NC}"
exit 1
fi
# Check PostgreSQL connection
if sudo -u $DJANGO_USER $DJANGO_HOME/venv/bin/python $DJANGO_HOME/app/manage.py check --database default; then
echo -e "${GREEN}✓ Database connection successful${NC}"
else
echo -e "${RED}✗ Database connection failed${NC}"
exit 1
fi
# Check HTTP response
if curl -s -o /dev/null -w "%{http_code}" http://localhost:8000/ | grep -q "200\|301\|302"; then
echo -e "${GREEN}✓ HTTP server responding${NC}"
else
echo -e "${RED}✗ HTTP server not responding${NC}"
exit 1
fi
echo -e "${GREEN}Django deployment completed successfully!${NC}"
echo -e "${GREEN}Application running at: http://$DOMAIN:8000${NC}"
echo -e "${YELLOW}Don't forget to:${NC}"
echo -e "${YELLOW}1. Configure your reverse proxy (Nginx/Apache)${NC}"
echo -e "${YELLOW}2. Set up SSL certificates${NC}"
echo -e "${YELLOW}3. Change the SECRET_KEY in production${NC}"
echo -e "${YELLOW}4. Configure firewall rules${NC}"
Review the script before running. Execute with: bash install.sh