Complete Production Deployment Guide: From Zero to HTTPS

Phase 1: DNS Configuration

Configure DNS in your provider's website

  • Add the following records (all sensitive values replaced):
Type Host Value TTL
A Record @ <your_server_ip> Automatic
CNAME Record www <your_root_domain> Automatic
  1. Save the records
  2. Wait 5–30 minutes for DNS propagation

Verify DNS

# Check A record
dig <your_root_domain> +short
# Should return: <your_server_ip>

# Check WWW
dig www.<your_root_domain> +short
# Should return: <your_root_domain>.

Phase 2: DigitalOcean Droplet Setup

Create Droplet

  1. Sign in to your hosting provider
  2. Select Create → Droplets
  3. Region: <your_region>
  4. Image: <your_os_version>
  5. Size:
    • Type: Basic
    • CPU: Regular
    • Plan: <your_droplet_specs>
  6. Authentication: SSH keys or password
  7. Hostname: <your_hostname>
  8. Create Droplet

Initial Server Access

# SSH into server
ssh root@<your_server_ip>

# Update system
apt update && apt upgrade -y

# Set timezone (optional)
timedatectl set-timezone <your_timezone>

Create Non-Root User

# Create user
adduser <your_deploy_user>

# Add to sudo group
usermod -aG sudo <your_deploy_user>

# Allow sudo without password (optional)
echo "<your_deploy_user> ALL=(ALL) NOPASSWD:ALL" >> /etc/sudoers.d/<your_deploy_user>

# Switch to new user
su - <your_deploy_user>

Phase 3: SSH Key Setup

Generate SSH Key (Local Machine)

# Generate Ed25519 key
ssh-keygen -t ed25519 -C "<your_email>"

# Save to: ~/.ssh/<your_key_filename>
# Set passphrase: <your_passphrase>

Add Key to SSH Agent

# Start ssh-agent
eval "$(ssh-agent -s)"

# Add key
ssh-add ~/.ssh/<your_key_filename>

Add Key to GitHub

# Copy public key
cat ~/.ssh/<your_key_filename>.pub

# Go to your Git host:
# Settings → SSH keys → Add new key
# Paste your public key

Test GitHub Connection

ssh -T git@github.com
# Expected: confirmation of authentication for <your_github_username>

Phase 4: Docker Installation

Add Docker Repository

# Install prerequisites
sudo apt install -y ca-certificates curl gnupg lsb-release

# Create keyrings directory
sudo mkdir -p /etc/apt/keyrings

# Add Docker's GPG key
curl -fsSL https://download.docker.com/linux/ubuntu/gpg \
  | sudo gpg --dearmor -o /etc/apt/keyrings/docker.gpg

# Add Docker repository
echo \
  "deb [arch=$(dpkg --print-architecture) signed-by=/etc/apt/keyrings/docker.gpg] \
  https://download.docker.com/linux/ubuntu \
  $(lsb_release -cs) stable" \
  | sudo tee /etc/apt/sources.list.d/docker.list > /dev/null

Install Docker

# Update package index
sudo apt update

# Install Docker
sudo apt install -y docker-ce docker-ce-cli containerd.io docker-compose-plugin

# Verify installation
docker --version
docker compose version

Configure Docker Permissions

# Add current user to docker group
sudo usermod -aG docker $USER

# Apply group changes
newgrp docker

# Test Docker (should run without sudo)
docker run hello-world

Phase 5: PostgreSQL Installation & Configuration

Install PostgreSQL

# Install packages
sudo apt install -y postgresql postgresql-contrib libpq-dev python3-dev

# Start service
sudo systemctl start postgresql
sudo systemctl enable postgresql

# Check status
sudo systemctl status postgresql

Create Database & User

# Switch to postgres user
sudo -u postgres psql

# Run SQL commands
CREATE DATABASE <your_db_name>;
CREATE USER <your_db_user> WITH PASSWORD '<your_db_password>';

# Set user parameters
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 database privileges
GRANT ALL PRIVILEGES ON DATABASE <your_db_name> TO <your_db_user>;

# Exit psql
\q

Grant Schema Permissions (PostgreSQL 15+)

# Connect to database
sudo -u postgres psql <your_db_name>

# Grant schema permissions
GRANT ALL ON SCHEMA public TO <your_db_user>;
ALTER SCHEMA public OWNER TO <your_db_user>;
GRANT CREATE, USAGE ON SCHEMA public TO <your_db_user>;

# Exit
\q

Configure Network Access

Edit postgresql.conf

# Find PostgreSQL version
ls /etc/postgresql/

# Edit config
sudo nano /etc/postgresql/<your_pg_version>/main/postgresql.conf

Change:

# Before
# listen_addresses = 'localhost'

# After
listen_addresses = '*'

Edit pg_hba.conf

sudo nano /etc/postgresql/<your_pg_version>/main/pg_hba.conf

Add:

# Docker bridge networks
host all all 172.17.0.0/16 md5
host all all 172.18.0.0/16 md5

Restart PostgreSQL

sudo systemctl restart postgresql

# Verify listening on all interfaces
ss -tln | grep 5432
# Should show binding on 0.0.0.0:5432

Phase 6: Application Deployment

Clone Repository

# Create project directory
sudo mkdir -p <your_project_path>
sudo chown -R $USER:$USER <your_project_path>

# Navigate to directory
cd <your_project_path>

# Clone repository
git clone git@github.com:<your_repo_owner>/<your_repo_name>.git .

Create Environment File

vim .env

Add the following (all secrets replaced with placeholders):

# Django Settings
DJANGO_SECRET_KEY=<your_django_secret_key>
DJANGO_SETTINGS_MODULE=<your_settings_module>

# Database Configuration
DB_NAME=<your_db_name>
DB_USER=<your_db_user>
DB_PASSWORD=<your_db_password>
DB_HOST=<your_db_host>
DB_PORT=<your_db_port>

Generate secret key:

python3 -c 'from django.core.management.utils import get_random_secret_key; print(get_random_secret_key())'

Secure the file:

chmod 600 .env

Build & Start Docker Container

# Build image
docker compose build

# Start container
docker compose up -d

# Check status
docker compose ps

# View logs
docker compose logs -f web

Run Migrations & Setup

# Run database migrations
docker compose exec web python manage.py migrate

# Collect static files
docker compose exec web python manage.py collectstatic --noinput

# Create required pages
docker compose exec web python manage.py <your_page_sync_command>

# Create superuser
docker compose exec web python manage.py createsuperuser

Verify Container

# Test Django response
curl http://localhost:<your_app_port>

# Should return HTML

Phase 7: Nginx Configuration

Install Nginx

sudo apt install -y nginx

# Start service
sudo systemctl start nginx
sudo systemctl enable nginx

# Check status
sudo systemctl status nginx

Create Site Configuration

sudo nano <your_nginx_conf_path>

Add this configuration (all sensitive values replaced):

server {
    listen 80;
    server_name <your_domain> <your_domain_www>;

    # Static files
    location /static/ {
        alias <your_project_path>/staticfiles/;
        expires 1y;
        add_header Cache-Control "public, immutable";
    }

    # Media files
    location /media/ {
        alias <your_project_path>/media/;
        expires 7d;
    }

    # Max upload size
    client_max_body_size 20M;

    # Proxy to application
    location / {
        proxy_pass http://127.0.0.1:<your_app_port>;
        proxy_set_header Host $host;
        proxy_set_header X-Real-IP $remote_addr;
        proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
        proxy_set_header X-Forwarded-Proto $scheme;

        # Timeouts
        proxy_connect_timeout 60s;
        proxy_send_timeout 60s;
        proxy_read_timeout 60s;
    }
}

Enable Site

# Create symlink
sudo ln -s <your_nginx_conf_path> /etc/nginx/sites-enabled/

# Remove default site (optional)
sudo rm /etc/nginx/sites-enabled/default

# Test configuration
sudo nginx -t

# Reload Nginx
sudo systemctl reload nginx

Verify HTTP Access

# Test from server
curl -I http://<your_domain>

# Test from browser:
# http://<your_domain>

Phase 8: SSL Certificate (Let's Encrypt)

Install Certbot

sudo apt install -y certbot python3-certbot-nginx

Obtain Certificate

sudo certbot --nginx -d <your_domain> -d <your_domain_www>

# Prompts:
# 1. Enter <your_email>
# 2. Agree to TOS
# 3. Choose redirect: yes

Certbot will automatically:

  • Generate certificates
  • Update the Nginx server block
  • Add HTTP → HTTPS redirect
  • Insert SSL parameters

Verify SSL

# Test HTTPS
curl -I https://<your_domain>

# Check certificate metadata
echo | openssl s_client -servername <your_domain> -connect <your_domain>:443 2>/dev/null | openssl x509 -noout -dates

Auto-Renewal Setup

Certbot installs a systemd timer automatically.

# Check timer
sudo systemctl status certbot.timer

# Dry-run renewal
sudo certbot renew --dry-run

# Manual renewal (if required)
sudo certbot renew

Phase 9: Django Security Settings

Update Production Settings

Verify the production settings include secure values (all secrets and paths replaced):

# <your_project>/settings/production.py

SECURE_SSL_REDIRECT = True
SESSION_COOKIE_SECURE = True
CSRF_COOKIE_SECURE = True
SECURE_PROXY_SSL_HEADER = ("HTTP_X_FORWARDED_PROTO", "https")

# Optional HSTS (enable only after confirming HTTPS stability)
# SECURE_HSTS_SECONDS = <your_hsts_seconds>
# SECURE_HSTS_INCLUDE_SUBDOMAINS = True
# SECURE_HSTS_PRELOAD = True

Restart Application

docker compose restart web

Phase 10: Verification Checklist

Test All Endpoints

# Homepage
curl -I https://<your_domain>
# Expected: 200 OK

# Static file
curl -I https://<your_domain>/static/<your_static_file>
# Expected: 200 OK

# Media file
curl -I https://<your_domain>/media/<your_media_file>
# Expected: 200 OK

# Admin panel
curl -I https://<your_domain>/admin/
# Expected: 200 OK or redirect

# API
curl https://<your_domain>/api/<your_endpoint>/ | jq
# Expected: JSON output

Security Headers Check

curl -I https://<your_domain> | grep -E "(Strict-Transport|X-Frame|X-Content)"

Expected Headers:

Strict-Transport-Security: max-age=<your_hsts_value>; includeSubDomains
X-Frame-Options: DENY
X-Content-Type-Options: nosniff

Performance Test

# Measure response time
time curl -s https://<your_domain> > /dev/null

# Check static file caching
curl -I https://<your_domain>/static/<your_static_file> | grep -i cache

Common Commands Reference

Restart Services

Service Command When to Use
Nginx sudo systemctl restart nginx Config changes
Nginx (reload) sudo systemctl reload nginx Minor tweaks
PostgreSQL sudo systemctl restart postgresql DB config changes
Docker sudo systemctl restart docker Engine issues
Application docker compose restart web Code changes
Full restart docker compose down && docker compose up -d Major updates

Application Management

# View logs
docker compose logs -f web

# Execute management command
docker compose exec web python manage.py <your_command>

# Shell access
docker compose exec web bash

# Database shell
docker compose exec web python manage.py dbshell

Code Deployment

# Pull latest code
cd <your_project_path>
git pull origin <your_branch>

# Rebuild and restart
docker compose down
docker compose build
docker compose up -d

# Recollect static files
docker compose exec web rm -rf <your_static_dir>/*
docker compose exec web python manage.py collectstatic --noinput

# Run migrations
docker compose exec web python manage.py migrate

Full Reset

# WARNING: Destroys data
docker compose down
docker compose build
docker compose up -d
docker compose exec web rm -rf <your_static_dir>/*
docker compose exec web python manage.py collectstatic --noinput
docker compose exec web python manage.py migrate
docker compose exec web python manage.py createsuperuser

Troubleshooting

Issue: Container Won’t Start

# Check logs
docker compose logs web

# Common checks:
# 1. .env file exists and values are correct (<your_env_values>)
# 2. DB_HOST matches your setup (<your_db_host>)
# 3. PostgreSQL is running
sudo systemctl status postgresql

Issue: Static Files Not Loading

# Check permissions
ls -la <your_static_dir>

# Fix permissions
sudo chown -R $USER:$USER <your_static_dir>
chmod -R 755 <your_static_dir>

# Recollect static files
docker compose exec web python manage.py collectstatic --noinput --clear

# Validate Nginx config
sudo nginx -t

# Reload Nginx
sudo systemctl reload nginx

Issue: Database Connection Failed

# Check PostgreSQL status
sudo systemctl status postgresql

# Verify listener
ss -tln | grep 5432

# Test connection from container
docker compose exec web python manage.py check --database default

# Check pg_hba.conf
sudo nano /etc/postgresql/<your_pg_version>/main/pg_hba.conf

Issue: 502 Bad Gateway

# Check container health
docker compose ps

# Check logs
docker compose logs web

# Verify Gunicorn port
curl http://localhost:<your_app_port>

# Restart app
docker compose restart web

Issue: SSL Renewal Failed

# Check certbot timer
sudo systemctl status certbot.timer

# Manual renewal
sudo certbot renew --nginx

# Check certificate expiration
echo | openssl s_client -servername <your_domain> -connect <your_domain>:443 2>/dev/null | openssl x509 -noout -dates