The Complete Production Deployment Guide for Django Applications
This comprehensive guide walks you through deploying a production-ready Django application on Ubuntu. You'll learn to configure Gunicorn as the application server, Nginx as a reverse proxy, Celery for background tasks, Redis as a message broker, PostgreSQL as the database, and secure everything with a free SSL certificate from Let's Encrypt.
┌───────────────────────────┐
│ INTERNET (WWW) │
└─────────────┬─────────────┘
│ HTTPS (443)
▼
┌───────────────────────────┐
│ NGINX (Reverse Proxy) │
│ SSL + Static Files │
└─────────────┬─────────────┘
│ Unix Socket
▼
┌───────────────────────────┐
│ GUNICORN (WSGI Server) │
│ Django Application │
└──────┬─────────────┬──────┘
│ │
┌────────────┘ └────────────┐
▼ ▼
┌───────────────────────────┐ ┌───────────────────────────┐
│ POSTGRESQL │ │ CELERY + REDIS │
│ Database │ │ Background Tasks │
└───────────────────────────┘ └───────────────────────────┘
- 📋 Prerequisites
- 🖥️ Part 1: Initial Server Setup
- 🔑 Part 2: SSH Keys & Environment Configuration
- ⚡ Part 3: Gunicorn & Nginx Setup
- 📦 Part 4: Celery, Redis & Supervisor
- 🌐 Part 5: Domain & SSL Certificate
- 🔧 Troubleshooting
- 📚 Quick Reference
Before starting, ensure you have:
- 🖥️ A Ubuntu server (18.04, 20.04, 22.04, or 24.04 LTS)
- 🔐 Root or sudo access to the server
- 🐍 A Django project ready for deployment
- 💻 Basic familiarity with Linux command line
- 🌐 A domain name (for SSL setup - optional for initial deployment)
⏱️ Estimated Time: 45-60 minutes for complete setup
Create a new Ubuntu server on your preferred cloud provider:
- ☁️ DigitalOcean
- ☁️ AWS EC2
- ☁️ Linode
- ☁️ Vultr
Choose a location closest to your target users for optimal latency.
ssh root@your-server-ip-address🛡️ Security Best Practice: Never run applications as root. Create a dedicated user for deployment.
# Create new user
adduser ubuntu
# Grant sudo privileges
usermod -aG sudo ubuntu# Allow SSH connections
ufw allow OpenSSH
# Enable firewall
ufw enable
# Verify status
ufw status# From now on, connect as the new user
ssh ubuntu@your-server-ip-address
# Run commands with sudo when needed
sudo your-command✅ Checkpoint: You should now be able to SSH into your server as the new user.
# Generate SSH key
ssh-keygen -t ed25519 -C "your-email@example.com"
# Display public key (copy this)
cat ~/.ssh/id_ed25519.pub💡 Note: If your system doesn't support Ed25519, use RSA instead:
ssh-keygen -t rsa -b 4096 -C "your-email@example.com"
- 📋 Copy the public key from the terminal
- 🔗 Navigate to your Git provider's SSH settings:
- GitHub: github.com/settings/keys
- GitLab: gitlab.com/-/profile/keys
- ➕ Click "New SSH Key" and paste your key
# Clone repository
git clone git@github.com:username/your-project.git
# Navigate to project directory
cd your-project# Update package list
sudo apt update
# Install required packages
sudo apt install -y \
python3-venv \
python3-dev \
libpq-dev \
postgresql \
postgresql-contrib \
nginx \
curl \
build-essential \
libssl-dev \
libffi-dev# Create virtual environment
python3 -m venv venv
# Activate virtual environment
source venv/bin/activate
# Upgrade pip
pip install --upgrade pip
# Install Gunicorn and PostgreSQL adapter
pip install gunicorn psycopg2-binary wheel
# Install project dependencies
pip install -r requirements.txt# Access PostgreSQL
sudo -u postgres psqlRun the following SQL commands:
-- Create database
CREATE DATABASE your_db_name;
-- Create user with secure password
CREATE USER your_db_user WITH PASSWORD 'your_secure_password';
-- Configure user settings
ALTER ROLE your_db_user SET client_encoding TO 'utf8';
ALTER ROLE your_db_user SET default_transaction_isolation TO 'read committed';
ALTER ROLE your_db_user SET timezone TO 'UTC';
-- Grant privileges
GRANT ALL PRIVILEGES ON DATABASE your_db_name TO your_db_user;
-- Exit PostgreSQL
\qUpdate your settings.py (or use environment variables):
# Database configuration
DATABASES = {
'default': {
'ENGINE': 'django.db.backends.postgresql',
'NAME': 'your_db_name',
'USER': 'your_db_user',
'PASSWORD': 'your_secure_password',
'HOST': 'localhost',
'PORT': '5432',
}
}
# Allowed hosts
ALLOWED_HOSTS = ['your-server-ip', 'yourdomain.com', 'www.yourdomain.com']
# Static files
STATIC_URL = '/static/'
STATIC_ROOT = BASE_DIR / 'static'
# Media files
MEDIA_URL = '/media/'
MEDIA_ROOT = BASE_DIR / 'media'🔐 Security Tip: Use environment variables for sensitive data:
import os SECRET_KEY = os.environ.get('DJANGO_SECRET_KEY')
# Apply migrations
python manage.py migrate
# Collect static files
python manage.py collectstatic --noinput
# Create superuser (optional)
python manage.py createsuperuser# Allow port 8000 temporarily
sudo ufw allow 8000
# Run development server
python manage.py runserver 0.0.0.0:8000🌐 Visit http://your-server-ip:8000 to verify the application works.
# Test Gunicorn (replace 'yourproject' with your project name)
gunicorn --bind 0.0.0.0:8000 yourproject.wsgi:applicationPress Ctrl+C to stop, then deactivate the virtual environment:
deactivate✅ Checkpoint: Django app runs successfully with Gunicorn.
sudo nano /etc/systemd/system/gunicorn.socketAdd the following content:
[Unit]
Description=Gunicorn socket for Django
[Socket]
ListenStream=/run/gunicorn.sock
[Install]
WantedBy=sockets.targetsudo nano /etc/systemd/system/gunicorn.serviceAdd the following content (update paths as needed):
[Unit]
Description=Gunicorn daemon for Django
Requires=gunicorn.socket
After=network.target
[Service]
User=ubuntu
Group=www-data
WorkingDirectory=/home/ubuntu/your-project
ExecStart=/home/ubuntu/your-project/venv/bin/gunicorn \
--access-logfile - \
--error-logfile - \
--workers 3 \
--bind unix:/run/gunicorn.sock \
yourproject.wsgi:application
[Install]
WantedBy=multi-user.target💡 Performance Tip: Set workers to
(2 × CPU cores) + 1for optimal performance.
# Start Gunicorn socket
sudo systemctl start gunicorn.socket
sudo systemctl enable gunicorn.socket
# Verify socket is running
sudo systemctl status gunicorn.socket
# Check socket file exists
file /run/gunicorn.sockCreate a new Nginx configuration:
sudo nano /etc/nginx/sites-available/yourprojectAdd the following configuration:
server {
listen 80;
server_name your-server-ip yourdomain.com www.yourdomain.com;
# 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;
# Favicon
location = /favicon.ico {
access_log off;
log_not_found off;
}
# Static files
location /static/ {
alias /home/ubuntu/your-project/static/;
expires 30d;
add_header Cache-Control "public, immutable";
}
# Media files
location /media/ {
alias /home/ubuntu/your-project/media/;
expires 7d;
}
# Proxy to Gunicorn
location / {
include proxy_params;
proxy_pass http://unix:/run/gunicorn.sock;
proxy_read_timeout 300;
proxy_connect_timeout 300;
proxy_send_timeout 300;
}
# Max upload size
client_max_body_size 10M;
}# Create symbolic link
sudo ln -s /etc/nginx/sites-available/yourproject /etc/nginx/sites-enabled/
# Test configuration
sudo nginx -t
# Restart Nginx
sudo systemctl restart nginx# Remove temporary port 8000 rule
sudo ufw delete allow 8000
# Allow Nginx
sudo ufw allow 'Nginx Full'🎉 Visit http://your-server-ip in your browser. Your Django application should be running!
✅ Checkpoint: Django app is accessible via Nginx.
⏭️ Skip this section if your application doesn't use background tasks.
# Install Redis
sudo apt install -y redis-server
# Install Supervisor
sudo apt install -y supervisor
# Verify Redis is running
sudo systemctl status redissudo nano /etc/supervisor/conf.d/celery_worker.confAdd the following content:
[program:celeryworker]
command=/home/ubuntu/your-project/venv/bin/celery -A yourproject worker --loglevel=INFO
directory=/home/ubuntu/your-project
user=ubuntu
numprocs=1
stdout_logfile=/var/log/celery/worker.log
stderr_logfile=/var/log/celery/worker.log
autostart=true
autorestart=true
startsecs=10
stopwaitsecs=600
killasgroup=true
priority=998For scheduled tasks:
sudo nano /etc/supervisor/conf.d/celery_beat.confAdd the following content:
[program:celerybeat]
command=/home/ubuntu/your-project/venv/bin/celery -A yourproject beat --loglevel=INFO
directory=/home/ubuntu/your-project
user=ubuntu
numprocs=1
stdout_logfile=/var/log/celery/beat.log
stderr_logfile=/var/log/celery/beat.log
autostart=true
autorestart=true
startsecs=10
priority=999# Create log directory
sudo mkdir -p /var/log/celery
sudo chown ubuntu:ubuntu /var/log/celery
# Update Supervisor
sudo supervisorctl reread
sudo supervisorctl update
# Start services
sudo supervisorctl start all
# Check status
sudo supervisorctl status# Restart all services
sudo supervisorctl restart all
# View worker logs
sudo tail -f /var/log/celery/worker.log
# View beat logs
sudo tail -f /var/log/celery/beat.log✅ Checkpoint: Celery workers are running in the background.
Configure your domain's DNS settings:
- A Record:
@→your-server-ip - A Record:
www→your-server-ip
⏳ Note: DNS propagation can take up to 48 hours, but usually completes within minutes.
sudo nano /etc/nginx/sites-available/yourprojectUpdate the server_name directive:
server_name yourdomain.com www.yourdomain.com;ALLOWED_HOSTS = ['yourdomain.com', 'www.yourdomain.com']sudo nginx -t
sudo systemctl reload nginx# Install Certbot
sudo snap install --classic certbot
# Create symbolic link
sudo ln -s /snap/bin/certbot /usr/bin/certbotsudo certbot --nginx -d yourdomain.com -d www.yourdomain.comFollow the prompts:
- 📧 Enter your email address
- ✅ Agree to terms of service (Y)
- 📰 Choose whether to share email with EFF
# Test renewal process
sudo certbot renew --dry-run
# Check renewal timer
sudo systemctl status snap.certbot.renew.timer🎉 Your site is now accessible via https://yourdomain.com!
✅ Checkpoint: SSL certificate installed and auto-renewal configured.
❌ 502 Bad Gateway
-
Check if Gunicorn is running:
sudo systemctl status gunicorn
-
Check Gunicorn logs:
sudo journalctl -u gunicorn
-
Verify socket file exists:
file /run/gunicorn.sock
-
Restart services:
sudo systemctl daemon-reload sudo systemctl restart gunicorn sudo systemctl restart nginx
🖼️ Static Files Not Loading
- Verify
STATIC_ROOTin settings.py - Run
python manage.py collectstatic - Check Nginx configuration paths
- Verify file permissions:
sudo chown -R ubuntu:www-data /home/ubuntu/your-project/static
🚫 Permission Denied Errors
-
Check file ownership:
ls -la /home/ubuntu/your-project
-
Fix permissions:
sudo chown -R ubuntu:www-data /home/ubuntu/your-project sudo chmod -R 755 /home/ubuntu/your-project
🐘 Database Connection Failed
-
Verify PostgreSQL is running:
sudo systemctl status postgresql
-
Test database connection:
sudo -u postgres psql -c "\l" -
Check credentials in settings.py
🥬 Celery Tasks Not Running
-
Check Supervisor status:
sudo supervisorctl status
-
View Celery logs:
sudo tail -f /var/log/celery/worker.log
-
Restart Celery:
sudo supervisorctl restart celeryworker
| Action | Command |
|---|---|
| 🔄 Restart Gunicorn | sudo systemctl restart gunicorn |
| 🔄 Restart Nginx | sudo systemctl restart nginx |
| 🔄 Restart Celery | sudo supervisorctl restart all |
| 📋 View Gunicorn logs | sudo journalctl -u gunicorn -f |
| 📋 View Nginx error log | sudo tail -f /var/log/nginx/error.log |
| 📋 View Celery logs | sudo tail -f /var/log/celery/worker.log |
- 🖥️ Server created and SSH access configured
- 👤 Non-root user created with sudo privileges
- 🔥 Firewall configured (UFW)
- 🔑 Git SSH key added to repository
- 📦 Project cloned and dependencies installed
- 🐘 PostgreSQL database configured
- ⚙️ Django settings updated for production
- 📁 Static files collected
- 🦄 Gunicorn socket and service configured
- 🔀 Nginx configured as reverse proxy
- 🥬 Celery and Supervisor configured (if needed)
- 🌐 Domain pointed to server
- 🔒 SSL certificate installed
- 🔄 Auto-renewal verified
When you push new code:
cd ~/your-project
git pull origin main
source venv/bin/activate
pip install -r requirements.txt
python manage.py migrate
python manage.py collectstatic --noinput
sudo systemctl restart gunicorn
sudo supervisorctl restart all # If using Celery
deactivateFound an issue or have a suggestion? Open an issue or submit a pull request.
This guide is released under the MIT License.
⭐ If this guide helped you, please give it a star! ⭐
Made with ❤️ for the Django community
Remember to remove the unwanted AAAA registers. It may be used by Cerbot instead of your A register.
Let's Encrypt unauthorized 403 forbidden