Implement Django continuous deployment with Git hooks and automated testing

Intermediate 45 min May 16, 2026 415 views
Ubuntu 24.04 Debian 12 AlmaLinux 9 Rocky Linux 9

Set up automated Django deployment with Git hooks, testing pipelines, and production rollbacks. Configure continuous integration with database migrations, static file management, and zero-downtime deployments.

Prerequisites

  • Django application with requirements.txt
  • PostgreSQL database configured
  • sudo access on target server
  • Git repository with Django project

What this solves

Continuous deployment automates your Django application releases, reducing manual errors and deployment time. This tutorial sets up Git hooks that automatically test, build, and deploy your Django application when code is pushed to your repository, including database migrations and static file collection.

Step-by-step configuration

Create deployment user and directories

Set up a dedicated user for deployments and create the necessary directory structure.

sudo adduser --system --group --home /home/deploy deploy
sudo mkdir -p /var/www/myapp/{releases,shared/{logs,media,static}}
sudo chown -R deploy:deploy /var/www/myapp
sudo chmod 755 /var/www/myapp
sudo useradd -r -s /bin/bash -d /home/deploy deploy
sudo mkdir -p /var/www/myapp/{releases,shared/{logs,media,static}}
sudo chown -R deploy:deploy /var/www/myapp
sudo chmod 755 /var/www/myapp

Install required packages

Install Python, Git, and other dependencies needed for Django deployment.

sudo apt update
sudo apt install -y python3 python3-pip python3-venv git nginx postgresql-client redis-tools
pip3 install virtualenv gunicorn
sudo dnf update -y
sudo dnf install -y python3 python3-pip git nginx postgresql redis
pip3 install virtualenv gunicorn

Initialize bare Git repository

Create a bare Git repository that will receive pushes and trigger deployments.

sudo -u deploy git init --bare /home/deploy/myapp.git
sudo -u deploy mkdir -p /home/deploy/myapp.git/hooks
sudo chmod 755 /home/deploy/myapp.git/hooks

Create deployment script

Set up the main deployment script that handles testing, building, and deploying your Django application.

#!/bin/bash

set -e

# Configuration
APP_NAME="myapp"
APP_DIR="/var/www/$APP_NAME"
RELEASE_DIR="$APP_DIR/releases/$(date +%Y%m%d_%H%M%S)"
SHARED_DIR="$APP_DIR/shared"
CURRENT_DIR="$APP_DIR/current"
REPO_DIR="/home/deploy/$APP_NAME.git"
BRANCH="main"

echo "Starting deployment of $APP_NAME..."

# Create release directory
mkdir -p $RELEASE_DIR
cd $RELEASE_DIR

# Clone the repository
git clone $REPO_DIR .
git checkout $BRANCH

# Create virtual environment
python3 -m venv venv
source venv/bin/activate

# Install dependencies
pip install -r requirements.txt

# Run tests
echo "Running tests..."
python manage.py test --settings=myapp.settings.test

if [ $? -ne 0 ]; then
    echo "Tests failed. Aborting deployment."
    rm -rf $RELEASE_DIR
    exit 1
fi

# Link shared directories
ln -s $SHARED_DIR/logs logs
ln -s $SHARED_DIR/media media
ln -s $SHARED_DIR/static staticfiles

# Collect static files
echo "Collecting static files..."
python manage.py collectstatic --noinput --settings=myapp.settings.production

# Run database migrations
echo "Running database migrations..."
python manage.py migrate --settings=myapp.settings.production

# Update current symlink atomically
ln -sfn $RELEASE_DIR $CURRENT_DIR

# Restart application server
sudo systemctl restart gunicorn
sudo systemctl reload nginx

# Keep only last 5 releases
cd $APP_DIR/releases
ls -1t | tail -n +6 | xargs -d '\n' rm -rf --

echo "Deployment completed successfully!"
echo "Release: $(basename $RELEASE_DIR)"

Set deployment script permissions

Make the deployment script executable and set correct ownership.

sudo chown deploy:deploy /home/deploy/deploy.sh
sudo chmod 755 /home/deploy/deploy.sh

Create post-receive Git hook

Set up the Git hook that automatically triggers deployment when code is pushed.

#!/bin/bash

set -e

echo "Received push to repository. Starting deployment..."

# Change to deploy user
sudo -u deploy /home/deploy/deploy.sh

echo "Deployment hook completed."

Set hook permissions

Make the Git hook executable.

sudo chown deploy:deploy /home/deploy/myapp.git/hooks/post-receive
sudo chmod 755 /home/deploy/myapp.git/hooks/post-receive

Configure Django test settings

Create a separate settings file for running tests during deployment.

from .base import *

# Use SQLite for tests
DATABASES = {
    'default': {
        'ENGINE': 'django.db.backends.sqlite3',
        'NAME': ':memory:',
    }
}

# Disable migrations during tests
class DisableMigrations:
    def __contains__(self, item):
        return True
    
    def __getitem__(self, item):
        return None

MIGRATION_MODULES = DisableMigrations()

# Speed up tests
PASSWORD_HASHERS = [
    'django.contrib.auth.hashers.MD5PasswordHasher',
]

# Disable logging during tests
LOGGING_CONFIG = None

# Test-specific settings
DEBUG = False
TEMPLATE_DEBUG = False

Create production settings

Configure Django settings for production deployment with proper security and performance settings.

from .base import *
import os

# Security settings
DEBUG = False
ALLOWED_HOSTS = ['example.com', 'www.example.com']

# Database
DATABASES = {
    'default': {
        'ENGINE': 'django.db.backends.postgresql',
        'NAME': os.environ.get('DB_NAME', 'myapp_prod'),
        'USER': os.environ.get('DB_USER', 'myapp'),
        'PASSWORD': os.environ.get('DB_PASSWORD'),
        'HOST': os.environ.get('DB_HOST', 'localhost'),
        'PORT': os.environ.get('DB_PORT', '5432'),
    }
}

# Cache configuration with Redis
CACHES = {
    'default': {
        'BACKEND': 'django_redis.cache.RedisCache',
        'LOCATION': 'redis://127.0.0.1:6379/1',
        'OPTIONS': {
            'CLIENT_CLASS': 'django_redis.client.DefaultClient',
        }
    }
}

# Session storage
SESSION_ENGINE = 'django.contrib.sessions.backends.cache'
SESSION_CACHE_ALIAS = 'default'

# Static files
STATIC_URL = '/static/'
STATIC_ROOT = '/var/www/myapp/shared/static'
MEDIA_URL = '/media/'
MEDIA_ROOT = '/var/www/myapp/shared/media'

# Logging
LOGGING = {
    'version': 1,
    'disable_existing_loggers': False,
    'handlers': {
        'file': {
            'level': 'INFO',
            'class': 'logging.FileHandler',
            'filename': '/var/www/myapp/shared/logs/django.log',
        },
    },
    'loggers': {
        'django': {
            'handlers': ['file'],
            'level': 'INFO',
            'propagate': True,
        },
    },
}

Configure Gunicorn service

Set up systemd service for running Django with Gunicorn.

[Unit]
Description=Gunicorn daemon for Django myapp
Requires=gunicorn.socket
After=network.target

[Service]
User=deploy
Group=deploy
WorkingDirectory=/var/www/myapp/current
Environment="DJANGO_SETTINGS_MODULE=myapp.settings.production"
ExecStart=/var/www/myapp/current/venv/bin/gunicorn \
          --access-logfile /var/www/myapp/shared/logs/gunicorn-access.log \
          --error-logfile /var/www/myapp/shared/logs/gunicorn-error.log \
          --workers 3 \
          --bind unix:/run/gunicorn.sock \
          myapp.wsgi:application
ExecReload=/bin/kill -s HUP $MAINPID
Restart=on-failure
RestartSec=5
KillMode=mixed
TimeoutStopSec=5
PrivateTmp=true

[Install]
WantedBy=multi-user.target

Create Gunicorn socket

Configure systemd socket for Gunicorn.

[Unit]
Description=Gunicorn socket

[Socket]
ListenStream=/run/gunicorn.sock
SocketUser=www-data
SocketGroup=www-data
SocketMode=0660

[Install]
WantedBy=sockets.target

Configure Nginx

Set up Nginx to serve your Django application with proper static file handling.

upstream myapp_server {
    server unix:/run/gunicorn.sock fail_timeout=0;
}

server {
    listen 80;
    server_name example.com www.example.com;
    
    client_max_body_size 50M;
    keepalive_timeout 5;
    
    # Static files
    location /static/ {
        alias /var/www/myapp/shared/static/;
        expires 1y;
        add_header Cache-Control "public, immutable";
        access_log off;
    }
    
    # Media files
    location /media/ {
        alias /var/www/myapp/shared/media/;
        expires 1y;
        add_header Cache-Control "public";
    }
    
    # Application
    location / {
        proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
        proxy_set_header X-Forwarded-Proto $scheme;
        proxy_set_header Host $http_host;
        proxy_redirect off;
        proxy_buffering off;
        
        proxy_pass http://myapp_server;
    }
    
    # Security headers
    add_header X-Frame-Options "SAMEORIGIN" always;
    add_header X-Content-Type-Options "nosniff" always;
    add_header X-XSS-Protection "1; mode=block" always;
    
    # Gzip compression
    gzip on;
    gzip_types text/plain text/css application/json application/javascript text/xml application/xml application/xml+rss text/javascript;
}

Enable and start services

Enable the site configuration and start all required services.

sudo ln -s /etc/nginx/sites-available/myapp /etc/nginx/sites-enabled/
sudo nginx -t
sudo systemctl daemon-reload
sudo systemctl enable gunicorn.socket
sudo systemctl start gunicorn.socket
sudo systemctl enable nginx
sudo systemctl restart nginx

Create rollback script

Set up a script to quickly rollback to the previous release if needed.

#!/bin/bash

set -e

APP_DIR="/var/www/myapp"
CURRENT_DIR="$APP_DIR/current"
RELEASES_DIR="$APP_DIR/releases"

# Get current and previous releases
CURRENT_RELEASE=$(readlink $CURRENT_DIR)
PREVIOUS_RELEASE=$(ls -1t $RELEASES_DIR | sed -n '2p')

if [ -z "$PREVIOUS_RELEASE" ]; then
    echo "No previous release found. Cannot rollback."
    exit 1
fi

echo "Rolling back from $(basename $CURRENT_RELEASE) to $PREVIOUS_RELEASE"

# Switch to previous release
ln -sfn $RELEASES_DIR/$PREVIOUS_RELEASE $CURRENT_DIR

# Restart services
sudo systemctl restart gunicorn
sudo systemctl reload nginx

echo "Rollback completed successfully!"
echo "Current release: $PREVIOUS_RELEASE"

Set rollback script permissions

Make the rollback script executable.

sudo chown deploy:deploy /home/deploy/rollback.sh
sudo chmod 755 /home/deploy/rollback.sh

Configure sudoers for deployment

Allow the deploy user to restart services without password.

sudo visudo -f /etc/sudoers.d/deploy
deploy ALL=(ALL) NOPASSWD: /bin/systemctl restart gunicorn
deploy ALL=(ALL) NOPASSWD: /bin/systemctl reload nginx
deploy ALL=(ALL) NOPASSWD: /bin/systemctl status gunicorn
deploy ALL=(ALL) NOPASSWD: /bin/systemctl status nginx

Add deployment remote to your project

Configure your local Git repository to push to the deployment server.

cd /path/to/your/django/project
git remote add production deploy@your-server:/home/deploy/myapp.git
git push production main
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.

Configure automated testing pipeline

Create comprehensive test configuration

Set up Django tests to run automatically during deployment with proper database and cache configuration.

from .base import *
import tempfile

# Use in-memory SQLite for speed
DATABASES = {
    'default': {
        'ENGINE': 'django.db.backends.sqlite3',
        'NAME': ':memory:',
        'TEST': {
            'NAME': ':memory:',
        },
    }
}

# Disable cache during tests
CACHES = {
    'default': {
        'BACKEND': 'django.core.cache.backends.dummy.DummyCache',
    }
}

# Use temporary directory for media
MEDIA_ROOT = tempfile.mkdtemp()

# Fast password hashing
PASSWORD_HASHERS = [
    'django.contrib.auth.hashers.MD5PasswordHasher',
]

# Disable migrations
class DisableMigrations:
    def __contains__(self, item):
        return True
    def __getitem__(self, item):
        return None

MIGRATION_MODULES = DisableMigrations()

# Test settings
DEBUG = False
SECRET_KEY = 'test-secret-key-not-for-production'
EMAIL_BACKEND = 'django.core.mail.backends.locmem.EmailBackend'
LOGGING_CONFIG = None

Add pre-deployment checks

Enhance the deployment script with comprehensive checks before deployment.

#!/bin/bash

set -e

echo "Running pre-deployment checks..."

# Check if virtual environment exists and activate it
if [ ! -d "venv" ]; then
    echo "Creating virtual environment..."
    python3 -m venv venv
fi

source venv/bin/activate

# Install dependencies
echo "Installing dependencies..."
pip install -r requirements.txt

# Check Django configuration
echo "Checking Django configuration..."
python manage.py check --settings=myapp.settings.production

# Run Django system checks
echo "Running Django system checks..."
python manage.py check --deploy --settings=myapp.settings.production

# Test database connection
echo "Testing database connection..."
python manage.py dbshell --settings=myapp.settings.production <<< "\q" > /dev/null 2>&1

if [ $? -ne 0 ]; then
    echo "Database connection failed!"
    exit 1
fi

# Run tests
echo "Running test suite..."
python manage.py test --settings=myapp.settings.test --verbosity=2

if [ $? -ne 0 ]; then
    echo "Tests failed!"
    exit 1
fi

# Check for pending migrations
echo "Checking for pending migrations..."
MIGRATIONS_OUTPUT=$(python manage.py showmigrations --plan --settings=myapp.settings.production | grep "\[ \]")

if [ ! -z "$MIGRATIONS_OUTPUT" ]; then
    echo "Pending migrations found:"
    echo "$MIGRATIONS_OUTPUT"
fi

# Run collectstatic dry-run
echo "Testing static file collection..."
python manage.py collectstatic --noinput --dry-run --settings=myapp.settings.production > /dev/null

echo "All pre-deployment checks passed!"

Update main deployment script

Integrate the pre-deployment checks into the main deployment script.

#!/bin/bash

set -e

# Configuration
APP_NAME="myapp"
APP_DIR="/var/www/$APP_NAME"
RELEASE_DIR="$APP_DIR/releases/$(date +%Y%m%d_%H%M%S)"
SHARED_DIR="$APP_DIR/shared"
CURRENT_DIR="$APP_DIR/current"
REPO_DIR="/home/deploy/$APP_NAME.git"
BRANCH="main"

echo "Starting deployment of $APP_NAME at $(date)..."

# Create release directory
mkdir -p $RELEASE_DIR
cd $RELEASE_DIR

# Clone the repository
git clone $REPO_DIR .
git checkout $BRANCH

echo "Repository cloned successfully. Commit: $(git rev-parse --short HEAD)"

# Run pre-deployment checks
/home/deploy/pre_deploy_checks.sh

# Link shared directories
echo "Linking shared directories..."
ln -s $SHARED_DIR/logs logs
ln -s $SHARED_DIR/media media
ln -s $SHARED_DIR/static staticfiles

# Collect static files
echo "Collecting static files..."
source venv/bin/activate
python manage.py collectstatic --noinput --settings=myapp.settings.production

# Run database migrations
echo "Running database migrations..."
python manage.py migrate --settings=myapp.settings.production

# Warm up the application
echo "Warming up application..."
python manage.py check --settings=myapp.settings.production

# Update current symlink atomically
echo "Switching to new release..."
ln -sfn $RELEASE_DIR $CURRENT_DIR

# Restart application server
echo "Restarting services..."
sudo systemctl restart gunicorn
sleep 2

# Verify deployment
echo "Verifying deployment..."
if ! sudo systemctl is-active --quiet gunicorn; then
    echo "Gunicorn failed to start! Rolling back..."
    /home/deploy/rollback.sh
    exit 1
fi

sudo systemctl reload nginx

# Health check
echo "Performing health check..."
sleep 5
if ! curl -f -s http://localhost/health/ > /dev/null; then
    echo "Health check failed! Application may not be responding correctly."
    echo "Manual intervention may be required."
fi

# Keep only last 5 releases
echo "Cleaning up old releases..."
cd $APP_DIR/releases
ls -1t | tail -n +6 | xargs -d '\n' rm -rf -- 2>/dev/null || true

echo "Deployment completed successfully at $(date)!"
echo "Release: $(basename $RELEASE_DIR)"
echo "Commit: $(cd $RELEASE_DIR && git rev-parse --short HEAD)"

Set script permissions

Make all deployment scripts executable.

sudo chown deploy:deploy /home/deploy/pre_deploy_checks.sh
sudo chmod 755 /home/deploy/pre_deploy_checks.sh
sudo chmod 755 /home/deploy/deploy.sh

Configure Redis for caching and sessions

For optimal Django performance, integrate Redis for caching and session storage. You can follow our detailed guide on

Automated install script

Run this to automate the entire setup

不想自己管理这些吗?

我们为依赖稳定运行时间的企业管理基础设施。全托管服务,配备一位熟悉您系统架构的固定联系人。

您将拥有一位了解您整体架构的固定联系人

Rotterdam 05:38 · 一条消息即可联系我们,无需填写工单表单