Django Deployment Guide¶
Comprehensive guide to deploying Django applications to Azure App Service, AWS App Runner, and VPS/EC2 instances.
Pre-Deployment Checklist¶
Before deploying to any platform, ensure your Django app is production-ready.
Security Settings¶
# settings/prod.py
import os
from dotenv import load_dotenv
load_dotenv()
# Security
DEBUG = False
SECRET_KEY = os.getenv('SECRET_KEY')
ALLOWED_HOSTS = [h.strip() for h in os.getenv('ALLOWED_HOSTS', '').split(',') if h.strip()]
# HTTPS
SECURE_SSL_REDIRECT = True
SESSION_COOKIE_SECURE = True
CSRF_COOKIE_SECURE = True
SECURE_BROWSER_XSS_FILTER = True
SECURE_CONTENT_TYPE_NOSNIFF = True
X_FRAME_OPTIONS = 'DENY'
# HSTS
SECURE_HSTS_SECONDS = 31536000
SECURE_HSTS_INCLUDE_SUBDOMAINS = True
SECURE_HSTS_PRELOAD = True
Static Files Configuration¶
# settings/prod.py
import os
STATIC_URL = '/static/'
STATIC_ROOT = os.path.join(BASE_DIR, 'staticfiles')
MEDIA_URL = '/media/'
MEDIA_ROOT = os.path.join(BASE_DIR, 'media')
# WhiteNoise for static files
MIDDLEWARE = [
'django.middleware.security.SecurityMiddleware',
'whitenoise.middleware.WhiteNoiseMiddleware', # Add this
# ... other middleware
]
STATICFILES_STORAGE = 'whitenoise.storage.CompressedManifestStaticFilesStorage'
Requirements File¶
# requirements.txt
Django==4.2.0
gunicorn==21.2.0
whitenoise==6.5.0
python-dotenv==1.0.0
psycopg2-binary==2.9.9
django-cors-headers==4.3.0
Azure App Service Deployment¶
Deploy Django to Azure App Service (PaaS).
Prerequisites¶
# Install Azure CLI
curl -sL https://aka.ms/InstallAzureCLIDeb | sudo bash
# Login to Azure
az login
# Create resource group
az group create \
--name myResourceGroup \
--location eastus
Create PostgreSQL Database¶
# Create PostgreSQL server
az postgres flexible-server create \
--resource-group myResourceGroup \
--name mypostgresserver \
--location eastus \
--admin-user myadmin \
--admin-password <YourPassword> \
--sku-name Standard_B1ms \
--tier Burstable \
--version 14 \
--storage-size 32
# Create database
az postgres flexible-server db create \
--resource-group myResourceGroup \
--server-name mypostgresserver \
--database-name mydatabase
# Configure firewall to allow Azure services
az postgres flexible-server firewall-rule create \
--resource-group myResourceGroup \
--name mypostgresserver \
--rule-name AllowAzureServices \
--start-ip-address 0.0.0.0 \
--end-ip-address 0.0.0.0
Create App Service¶
# Create App Service plan
az appservice plan create \
--name myAppServicePlan \
--resource-group myResourceGroup \
--sku B1 \
--is-linux
# Create web app
az webapp create \
--resource-group myResourceGroup \
--plan myAppServicePlan \
--name mydjango-app \
--runtime "PYTHON:3.11"
Configure App Service¶
# Set environment variables
az webapp config appsettings set \
--resource-group myResourceGroup \
--name mydjango-app \
--settings \
SECRET_KEY="your-secret-key" \
DEBUG="False" \
ALLOWED_HOSTS="mydjango-app.azurewebsites.net" \
DB_NAME="mydatabase" \
DB_USER="myadmin" \
DB_PASSWORD="<YourPassword>" \
DB_HOST="mypostgresserver.postgres.database.azure.com" \
DB_PORT="5432" \
DJANGO_SETTINGS_MODULE="myproject.settings.prod"
Deployment Files¶
# startup.sh
#!/bin/bash
# Collect static files
python manage.py collectstatic --noinput
# Run migrations
python manage.py migrate --noinput
# Start Gunicorn
gunicorn myproject.wsgi:application \
--bind=0.0.0.0:8000 \
--workers=4 \
--timeout=600 \
--access-logfile=- \
--error-logfile=-
Configure Startup Command¶
# Set startup command
az webapp config set \
--resource-group myResourceGroup \
--name mydjango-app \
--startup-file "startup.sh"
Deploy with Git¶
# Get deployment credentials
az webapp deployment user set \
--user-name <username> \
--password <password>
# Get Git URL
az webapp deployment source config-local-git \
--name mydjango-app \
--resource-group myResourceGroup
# Add Azure remote
git remote add azure <git-url>
# Deploy
git add .
git commit -m "Initial deployment"
git push azure main
Deploy with ZIP¶
# Create deployment package
zip -r deploy.zip . -x "*.git*" "venv/*" "*.pyc" "__pycache__/*"
# Deploy
az webapp deployment source config-zip \
--resource-group myResourceGroup \
--name mydjango-app \
--src deploy.zip
Custom Domain and SSL¶
# Add custom domain
az webapp config hostname add \
--webapp-name mydjango-app \
--resource-group myResourceGroup \
--hostname www.example.com
# Enable HTTPS
az webapp update \
--name mydjango-app \
--resource-group myResourceGroup \
--https-only true
AWS App Runner Deployment¶
Deploy Django to AWS App Runner (Container-based PaaS).
Prerequisites¶
# Install AWS CLI
curl "https://awscli.amazonaws.com/awscli-exe-linux-x86_64.zip" -o "awscliv2.zip"
unzip awscliv2.zip
sudo ./aws/install
# Configure AWS CLI
aws configure
Create RDS Database¶
# Create RDS PostgreSQL database
aws rds create-db-instance \
--db-instance-identifier mydjango-db \
--db-instance-class db.t3.micro \
--engine postgres \
--engine-version 14.7 \
--master-username myadmin \
--master-user-password <YourPassword> \
--allocated-storage 20 \
--db-name mydatabase \
--backup-retention-period 7 \
--publicly-accessible
# Get database endpoint
aws rds describe-db-instances \
--db-instance-identifier mydjango-db \
--query 'DBInstances[0].Endpoint.Address'
Create ECR Repository¶
# Create ECR repository
aws ecr create-repository \
--repository-name mydjango-app \
--region us-east-1
# Get login credentials
aws ecr get-login-password --region us-east-1 | \
docker login --username AWS --password-stdin \
<account-id>.dkr.ecr.us-east-1.amazonaws.com
Build and Push Docker Image¶
# Dockerfile (see Docker section below for complete file)
FROM python:3.11-slim
WORKDIR /app
COPY requirements.txt .
RUN pip install --no-cache-dir -r requirements.txt
COPY . .
RUN python manage.py collectstatic --noinput
EXPOSE 8000
CMD ["gunicorn", "myproject.wsgi:application", "--bind", "0.0.0.0:8000"]
# Build image
docker build -t mydjango-app .
# Tag image
docker tag mydjango-app:latest \
<account-id>.dkr.ecr.us-east-1.amazonaws.com/mydjango-app:latest
# Push to ECR
docker push <account-id>.dkr.ecr.us-east-1.amazonaws.com/mydjango-app:latest
Create App Runner Service¶
# Create apprunner.yaml
cat > apprunner.yaml << 'EOF'
version: 1.0
runtime: python3
build:
commands:
build:
- pip install -r requirements.txt
- python manage.py collectstatic --noinput
run:
runtime-version: 3.11
command: gunicorn myproject.wsgi:application --bind 0.0.0.0:8000
network:
port: 8000
env:
- name: DJANGO_SETTINGS_MODULE
value: myproject.settings.prod
EOF
# Create App Runner service using AWS Console or CLI
aws apprunner create-service \
--service-name mydjango-app \
--source-configuration '{
"ImageRepository": {
"ImageIdentifier": "<account-id>.dkr.ecr.us-east-1.amazonaws.com/mydjango-app:latest",
"ImageRepositoryType": "ECR",
"ImageConfiguration": {
"Port": "8000",
"RuntimeEnvironmentVariables": {
"SECRET_KEY": "your-secret-key",
"DEBUG": "False",
"ALLOWED_HOSTS": "*",
"DB_NAME": "mydatabase",
"DB_USER": "myadmin",
"DB_PASSWORD": "<YourPassword>",
"DB_HOST": "<rds-endpoint>",
"DB_PORT": "5432"
}
}
},
"AutoDeploymentsEnabled": true
}' \
--instance-configuration '{
"Cpu": "1 vCPU",
"Memory": "2 GB"
}'
Auto-Deploy from GitHub¶
# Create service with GitHub source
aws apprunner create-service \
--service-name mydjango-app \
--source-configuration '{
"CodeRepository": {
"RepositoryUrl": "https://github.com/username/mydjango-app",
"SourceCodeVersion": {
"Type": "BRANCH",
"Value": "main"
},
"CodeConfiguration": {
"ConfigurationSource": "API",
"CodeConfigurationValues": {
"Runtime": "PYTHON_3",
"BuildCommand": "pip install -r requirements.txt && python manage.py collectstatic --noinput",
"StartCommand": "gunicorn myproject.wsgi:application --bind 0.0.0.0:8000",
"Port": "8000",
"RuntimeEnvironmentVariables": {
"SECRET_KEY": "your-secret-key"
}
}
}
},
"AutoDeploymentsEnabled": true
}'
VPS/EC2 Deployment¶
Deploy Django to a Virtual Private Server or AWS EC2 instance.
Launch EC2 Instance¶
# Create key pair
aws ec2 create-key-pair \
--key-name mydjango-key \
--query 'KeyMaterial' \
--output text > mydjango-key.pem
chmod 400 mydjango-key.pem
# Create security group
aws ec2 create-security-group \
--group-name mydjango-sg \
--description "Django app security group"
# Allow SSH (port 22)
aws ec2 authorize-security-group-ingress \
--group-name mydjango-sg \
--protocol tcp \
--port 22 \
--cidr 0.0.0.0/0
# Allow HTTP (port 80)
aws ec2 authorize-security-group-ingress \
--group-name mydjango-sg \
--protocol tcp \
--port 80 \
--cidr 0.0.0.0/0
# Allow HTTPS (port 443)
aws ec2 authorize-security-group-ingress \
--group-name mydjango-sg \
--protocol tcp \
--port 443 \
--cidr 0.0.0.0/0
# Launch instance (Ubuntu 22.04)
aws ec2 run-instances \
--image-id ami-0c7217cdde317cfec \
--instance-type t2.micro \
--key-name mydjango-key \
--security-groups mydjango-sg \
--tag-specifications 'ResourceType=instance,Tags=[{Key=Name,Value=mydjango-server}]'
Connect to Server¶
# Get instance public IP
aws ec2 describe-instances \
--filters "Name=tag:Name,Values=mydjango-server" \
--query 'Reservations[0].Instances[0].PublicIpAddress'
# SSH into server
ssh -i mydjango-key.pem ubuntu@<public-ip>
Server Setup¶
# Update system
sudo apt update
sudo apt upgrade -y
# Install Python and dependencies
sudo apt install -y python3.11 python3.11-venv python3-pip
sudo apt install -y postgresql postgresql-contrib
sudo apt install -y nginx
sudo apt install -y git
# Install system dependencies for Python packages
sudo apt install -y build-essential libpq-dev
Setup PostgreSQL¶
# Switch to postgres user
sudo -u postgres psql
# In PostgreSQL shell
CREATE DATABASE mydatabase;
CREATE USER myuser WITH PASSWORD 'mypassword';
ALTER ROLE myuser SET client_encoding TO 'utf8';
ALTER ROLE myuser SET default_transaction_isolation TO 'read committed';
ALTER ROLE myuser SET timezone TO 'UTC';
GRANT ALL PRIVILEGES ON DATABASE mydatabase TO myuser;
\q
Deploy Application¶
# Create app directory
sudo mkdir -p /var/www/mydjango
sudo chown -R $USER:$USER /var/www/mydjango
# Clone repository
cd /var/www/mydjango
git clone https://github.com/username/mydjango-app.git .
# Create virtual environment
python3.11 -m venv venv
source venv/bin/activate
# Install dependencies
pip install --upgrade pip
pip install -r requirements.txt
# Create .env file
cat > .env << 'EOF'
SECRET_KEY=your-secret-key-here
DEBUG=False
ALLOWED_HOSTS=your-domain.com,www.your-domain.com
DB_NAME=mydatabase
DB_USER=myuser
DB_PASSWORD=mypassword
DB_HOST=localhost
DB_PORT=5432
EOF
# Run migrations
python manage.py migrate
# Collect static files
python manage.py collectstatic --noinput
# Create superuser
python manage.py createsuperuser
Setup Gunicorn¶
# Test Gunicorn
gunicorn myproject.wsgi:application --bind 0.0.0.0:8000
# Create Gunicorn systemd service
sudo nano /etc/systemd/system/gunicorn.service
[Unit]
Description=Gunicorn daemon for Django project
After=network.target
[Service]
User=ubuntu
Group=www-data
WorkingDirectory=/var/www/mydjango
Environment="PATH=/var/www/mydjango/venv/bin"
EnvironmentFile=/var/www/mydjango/.env
ExecStart=/var/www/mydjango/venv/bin/gunicorn \
--workers 3 \
--bind unix:/var/www/mydjango/gunicorn.sock \
myproject.wsgi:application
[Install]
WantedBy=multi-user.target
# Start and enable Gunicorn
sudo systemctl start gunicorn
sudo systemctl enable gunicorn
sudo systemctl status gunicorn
Setup Nginx¶
server {
listen 80;
server_name your-domain.com www.your-domain.com;
location = /favicon.ico { access_log off; log_not_found off; }
location /static/ {
alias /var/www/mydjango/staticfiles/;
}
location /media/ {
alias /var/www/mydjango/media/;
}
location / {
include proxy_params;
proxy_pass http://unix:/var/www/mydjango/gunicorn.sock;
}
}
# Enable site
sudo ln -s /etc/nginx/sites-available/mydjango /etc/nginx/sites-enabled/
# Test Nginx configuration
sudo nginx -t
# Restart Nginx
sudo systemctl restart nginx
SSL with Let's Encrypt¶
# Install Certbot
sudo apt install -y certbot python3-certbot-nginx
# Get SSL certificate
sudo certbot --nginx -d your-domain.com -d www.your-domain.com
# Auto-renewal test
sudo certbot renew --dry-run
Automated Deployment Script¶
# deploy.sh
#!/bin/bash
echo "Starting deployment..."
# Navigate to project directory
cd /var/www/mydjango
# Pull latest changes
git pull origin main
# Activate virtual environment
source venv/bin/activate
# Install/update dependencies
pip install -r requirements.txt
# Run migrations
python manage.py migrate --noinput
# Collect static files
python manage.py collectstatic --noinput
# Restart Gunicorn
sudo systemctl restart gunicorn
# Restart Nginx
sudo systemctl restart nginx
echo "Deployment complete!"
Dockerizing Django Application¶
Complete guide to containerizing your Django application.
Production Dockerfile¶
# Dockerfile
FROM python:3.11-slim
# Set environment variables
ENV PYTHONDONTWRITEBYTECODE=1
ENV PYTHONUNBUFFERED=1
# Set work directory
WORKDIR /app
# Install system dependencies
RUN apt-get update && apt-get install -y \
postgresql-client \
build-essential \
libpq-dev \
&& rm -rf /var/lib/apt/lists/*
# Install Python dependencies
COPY requirements.txt .
RUN pip install --upgrade pip && \
pip install --no-cache-dir -r requirements.txt
# Copy project
COPY . .
# Collect static files
RUN python manage.py collectstatic --noinput
# Create non-root user
RUN useradd -m -u 1000 django && \
chown -R django:django /app
USER django
# Expose port
EXPOSE 8000
# Run gunicorn
CMD ["gunicorn", "myproject.wsgi:application", \
"--bind", "0.0.0.0:8000", \
"--workers", "4", \
"--timeout", "120"]
Multi-Stage Dockerfile¶
# Dockerfile.multistage
# Build stage
FROM python:3.11-slim as builder
WORKDIR /app
# Install build dependencies
RUN apt-get update && apt-get install -y \
build-essential \
libpq-dev \
&& rm -rf /var/lib/apt/lists/*
# Install Python dependencies
COPY requirements.txt .
RUN pip install --user --no-cache-dir -r requirements.txt
# Runtime stage
FROM python:3.11-slim
WORKDIR /app
# Install runtime dependencies
RUN apt-get update && apt-get install -y \
postgresql-client \
libpq-dev \
&& rm -rf /var/lib/apt/lists/*
# Copy Python dependencies from builder
COPY --from=builder /root/.local /root/.local
ENV PATH=/root/.local/bin:$PATH
# Copy project
COPY . .
# Collect static files
RUN python manage.py collectstatic --noinput
# Create non-root user
RUN useradd -m -u 1000 django && \
chown -R django:django /app
USER django
EXPOSE 8000
CMD ["gunicorn", "myproject.wsgi:application", \
"--bind", "0.0.0.0:8000"]
Docker Compose¶
# docker-compose.yml
version: '3.8'
services:
db:
image: postgres:15-alpine
volumes:
- postgres_data:/var/lib/postgresql/data
environment:
- POSTGRES_DB=mydatabase
- POSTGRES_USER=myuser
- POSTGRES_PASSWORD=mypassword
healthcheck:
test: ["CMD-SHELL", "pg_isready -U myuser -d mydatabase"]
interval: 10s
timeout: 5s
retries: 5
web:
build: .
command: gunicorn myproject.wsgi:application --bind 0.0.0.0:8000
volumes:
- .:/app
- static_volume:/app/staticfiles
- media_volume:/app/media
ports:
- "8000:8000"
env_file:
- .env
depends_on:
db:
condition: service_healthy
nginx:
image: nginx:alpine
volumes:
- ./nginx.conf:/etc/nginx/nginx.conf
- static_volume:/app/staticfiles
- media_volume:/app/media
ports:
- "80:80"
- "443:443"
depends_on:
- web
volumes:
postgres_data:
static_volume:
media_volume:
Production Docker Compose¶
# docker-compose.prod.yml
version: '3.8'
services:
db:
image: postgres:15-alpine
volumes:
- postgres_data:/var/lib/postgresql/data
environment:
- POSTGRES_DB=${DB_NAME}
- POSTGRES_USER=${DB_USER}
- POSTGRES_PASSWORD=${DB_PASSWORD}
restart: unless-stopped
networks:
- django-network
web:
build:
context: .
dockerfile: Dockerfile
command: >
sh -c "python manage.py migrate --noinput &&
python manage.py collectstatic --noinput &&
gunicorn myproject.wsgi:application
--bind 0.0.0.0:8000
--workers 4
--timeout 120
--access-logfile -
--error-logfile -"
volumes:
- static_volume:/app/staticfiles
- media_volume:/app/media
env_file:
- .env.prod
depends_on:
- db
restart: unless-stopped
networks:
- django-network
nginx:
image: nginx:alpine
volumes:
- ./nginx/nginx.conf:/etc/nginx/nginx.conf
- ./nginx/ssl:/etc/nginx/ssl
- static_volume:/app/staticfiles
- media_volume:/app/media
ports:
- "80:80"
- "443:443"
depends_on:
- web
restart: unless-stopped
networks:
- django-network
volumes:
postgres_data:
static_volume:
media_volume:
networks:
django-network:
driver: bridge
Nginx Configuration for Docker¶
# nginx/nginx.conf
upstream django {
server web:8000;
}
server {
listen 80;
server_name your-domain.com www.your-domain.com;
location /.well-known/acme-challenge/ {
root /var/www/certbot;
}
location / {
return 301 https://$host$request_uri;
}
}
server {
listen 443 ssl http2;
server_name your-domain.com www.your-domain.com;
ssl_certificate /etc/nginx/ssl/fullchain.pem;
ssl_certificate_key /etc/nginx/ssl/privkey.pem;
client_max_body_size 100M;
location /static/ {
alias /app/staticfiles/;
}
location /media/ {
alias /app/media/;
}
location / {
proxy_pass http://django;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
proxy_set_header Host $host;
proxy_redirect off;
}
}
Docker Commands¶
# Build image
docker build -t mydjango-app .
# Run container
docker run -p 8000:8000 --env-file .env mydjango-app
# Docker Compose commands
docker-compose up -d # Start services
docker-compose down # Stop services
docker-compose logs -f web # View logs
docker-compose exec web python manage.py migrate
docker-compose exec web python manage.py createsuperuser
docker-compose restart web # Restart service
# Production
docker-compose -f docker-compose.prod.yml up -d --build
docker-compose -f docker-compose.prod.yml down
docker-compose -f docker-compose.prod.yml exec web python manage.py migrate
.dockerignore¶
# .dockerignore
*.pyc
__pycache__
db.sqlite3
.env
.git
.gitignore
.vscode
venv/
.pytest_cache
.coverage
htmlcov/
*.log
media/
staticfiles/
Environment Variables for Docker¶
# .env.prod
SECRET_KEY=your-production-secret-key
DEBUG=False
ALLOWED_HOSTS=your-domain.com,www.your-domain.com
DB_ENGINE=django.db.backends.postgresql
DB_NAME=mydatabase
DB_USER=myuser
DB_PASSWORD=mypassword
DB_HOST=db
DB_PORT=5432
DJANGO_SETTINGS_MODULE=myproject.settings.prod
CI/CD Pipeline¶
GitHub Actions for Azure¶
# .github/workflows/azure-deploy.yml
name: Deploy to Azure App Service
on:
push:
branches: [ main ]
jobs:
deploy:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
- name: Set up Python
uses: actions/setup-python@v4
with:
python-version: '3.11'
- name: Install dependencies
run: |
pip install -r requirements.txt
- name: Run tests
run: |
python manage.py test
- name: Deploy to Azure
uses: azure/webapps-deploy@v2
with:
app-name: mydjango-app
publish-profile: ${{ secrets.AZURE_WEBAPP_PUBLISH_PROFILE }}
GitHub Actions for AWS¶
# .github/workflows/aws-deploy.yml
name: Deploy to AWS App Runner
on:
push:
branches: [ main ]
jobs:
deploy:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
- name: Configure AWS credentials
uses: aws-actions/configure-aws-credentials@v2
with:
aws-access-key-id: ${{ secrets.AWS_ACCESS_KEY_ID }}
aws-secret-access-key: ${{ secrets.AWS_SECRET_ACCESS_KEY }}
aws-region: us-east-1
- name: Login to Amazon ECR
id: login-ecr
uses: aws-actions/amazon-ecr-login@v1
- name: Build and push Docker image
env:
ECR_REGISTRY: ${{ steps.login-ecr.outputs.registry }}
ECR_REPOSITORY: mydjango-app
IMAGE_TAG: ${{ github.sha }}
run: |
docker build -t $ECR_REGISTRY/$ECR_REPOSITORY:$IMAGE_TAG .
docker push $ECR_REGISTRY/$ECR_REPOSITORY:$IMAGE_TAG
- name: Deploy to App Runner
run: |
aws apprunner update-service \
--service-arn ${{ secrets.APP_RUNNER_SERVICE_ARN }} \
--source-configuration ImageRepository={ImageIdentifier=${{ steps.login-ecr.outputs.registry }}/mydjango-app:${{ github.sha }}}
Monitoring and Maintenance¶
Health Check Endpoint¶
# views.py
from django.http import JsonResponse
from django.db import connection
def health_check(request):
try:
# Check database connection
with connection.cursor() as cursor:
cursor.execute("SELECT 1")
return JsonResponse({'status': 'healthy', 'database': 'connected'})
except Exception as e:
return JsonResponse(
{'status': 'unhealthy', 'error': str(e)},
status=500
)
# urls.py
urlpatterns = [
path('health/', health_check),
]
Logging¶
# settings/prod.py
LOGGING = {
'version': 1,
'disable_existing_loggers': False,
'formatters': {
'verbose': {
'format': '{levelname} {asctime} {module} {message}',
'style': '{',
},
},
'handlers': {
'file': {
'level': 'ERROR',
'class': 'logging.handlers.RotatingFileHandler',
'filename': '/var/log/django/error.log',
'maxBytes': 1024 * 1024 * 15, # 15MB
'backupCount': 10,
'formatter': 'verbose',
},
'console': {
'level': 'INFO',
'class': 'logging.StreamHandler',
'formatter': 'verbose',
},
},
'root': {
'handlers': ['console', 'file'],
'level': 'INFO',
},
'loggers': {
'django': {
'handlers': ['console', 'file'],
'level': 'INFO',
'propagate': False,
},
},
}