From IP to Identity: The Complete Guide to Deploying Django with SSL, Nginx, and Docker

Published: (December 26, 2025 at 10:10 PM EST)
4 min read
Source: Dev.to

Source: Dev.to

Ajit Kumar

1. The Architectural Blueprint

Before we type a single command, understand the Flow of Traffic:

ComponentRole
UserTypes yourdomain.com.
GoDaddy DNSPoints the domain to your AWS Elastic IP.
AWS Security GroupActs as a firewall, allowing traffic on ports 80 (HTTP) and 443 (HTTPS).
Nginx (inside Docker)Receives the request, handles the SSL handshake, and proxies traffic to Daphne (the Django ASGI server).

2. Infrastructure Setup (GoDaddy & AWS)

Step 1 – Lock your IP in AWS

By default, EC2 IPs change on reboot.

  1. Go to EC2 Dashboard → Elastic IPs.
  2. Click Allocate Elastic IP and then Associate it with your instance.
  3. Your instance now has a permanent “Home Address”.

Step 2 – Update GoDaddy DNS

Create two records so both yourdomain.com and www.yourdomain.com work.

Record TypeHostValue
A@Your_Elastic_IP
CNAMEwww@ (points www to the main domain)

3. Obtaining SSL with Certbot (Let’s Encrypt)

Certbot talks to Let’s Encrypt to prove domain ownership and issue a certificate.
We use the Standalone method because Nginx runs inside Docker and we don’t want to interfere with the host‑level Nginx.

Installation & Linking

# 1. Update system
sudo apt update

# 2. Install Snap (the recommended way for Certbot)
sudo snap install core; sudo snap refresh core

# 3. Install Certbot
sudo snap install --classic certbot

# 4. Create a symbolic link so you can run 'certbot' globally
sudo ln -s /snap/bin/certbot /usr/bin/certbot

Issuing the Certificate

Important: Stop any service using port 80 (e.g., a running Nginx container) before running this.

# Obtain the certificate
sudo certbot certonly --standalone -d yourdomain.com -d www.yourdomain.com

Successful Output

You should see a message similar to:

Congratulations! Your certificate and chain have been saved at:
/etc/letsencrypt/live/yourdomain.com/fullchain.pem

What just happened?
Certbot created /etc/letsencrypt/ on your EC2 instance. This folder contains:

FileDescription
fullchain.pemPublic certificate (includes chain).
privkey.pemPrivate key.

We will mount this folder into Docker so Nginx can read the certificates.

4. Production Configuration Files

nginx.conf – The Traffic Cop

Create this file in your project root. It redirects HTTP (port 80) to HTTPS (port 443) and proxies requests to Daphne.

upstream app_server {
    server web:8000;
}

/* 1. Redirect ALL HTTP traffic to HTTPS */
server {
    listen 80;
    server_name yourdomain.com www.yourdomain.com;
    return 301 https://$host$request_uri;
}

/* 2. The Secure Server */
server {
    listen 443 ssl;
    server_name yourdomain.com www.yourdomain.com;

    # Paths are INSIDE the container (mapped via docker‑compose)
    ssl_certificate /etc/letsencrypt/live/yourdomain.com/fullchain.pem;
    ssl_certificate_key /etc/letsencrypt/live/yourdomain.com/privkey.pem;

    location /static/ {
        alias /app/staticfiles/;
    }

    location / {
        proxy_pass http://app_server;
        proxy_set_header Host $host;
        proxy_set_header X-Forwarded-Proto https;

        # WebSocket support (essential for real‑time apps)
        proxy_http_version 1.1;
        proxy_set_header Upgrade $http_upgrade;
        proxy_set_header Connection "upgrade";
    }
}

docker-compose.yml – The Orchestrator

The crucial part is the volumes section, which mounts the host’s SSL folder into the Nginx container.

services:
  web:
    build: .
    command: ["daphne", "-b", "0.0.0.0", "-p", "8000", "myproject.asgi:application"]
    env_file: .env
    restart: always

  nginx:
    image: nginx:latest
    ports:
      - "80:80"
      - "443:443"
    volumes:
      - ./nginx.conf:/etc/nginx/conf.d/default.conf:ro
      - static_volume:/app/staticfiles:ro
      # Mount the host certificates here:
      - /etc/letsencrypt:/etc/letsencrypt:ro
    depends_on:
      - web
    restart: always

volumes:
  static_volume:

5. Django Security Settings (.env & settings.py)

When Nginx terminates SSL, Django receives plain HTTP from the proxy. We must tell Django that the original request was HTTPS.

.env

ALLOWED_HOSTS=yourdomain.com,www.yourdomain.com,your_ip
CSRF_TRUSTED_ORIGINS=https://yourdomain.com,https://www.yourdomain.com

settings.py

import os

ALLOWED_HOSTS = os.getenv("ALLOWED_HOSTS", "").split(",")

# Trust the X-Forwarded-Proto header from Nginx
SECURE_PROXY_SSL_HEADER = ('HTTP_X_FORWARDED_PROTO', 'https')

# Django 4.0+ – required for HTTPS form submissions
CSRF_TRUSTED_ORIGINS = os.getenv("CSRF_TRUSTED_ORIGINS", "").split(",")

6. Operational Commands

ActionCommand
Deploy / Updatedocker compose up -d --force-recreate
Stop Everythingdocker compose down
Check Logsdocker compose logs -f nginx
Verify Certsls -la /etc/letsencrypt/live/yourdomain.com/
Check SSL Healthcurl -Iv https://yourdomain.com

7. Troubleshooting Common Errors

SymptomLikely CauseFix
502 Bad GatewayNginx can’t reach the web container.Ensure the upstream name (web) matches the service name in docker‑compose.yml. Verify the container is running (docker compose ps).
SSL handshake failureWrong certificate paths or missing mount.Confirm /etc/letsencrypt is correctly mounted into the Nginx container and that ssl_certificate/ssl_certificate_key point to the right files.
Redirect loopBoth HTTP and HTTPS blocks listen on the same port.Make sure the HTTP server block only listens on 80 and the HTTPS block only on 443.
CSRF errorsCSRF_TRUSTED_ORIGINS not set or missing SECURE_PROXY_SSL_HEADER.Verify both environment variables are loaded and that SECURE_PROXY_SSL_HEADER is present in settings.py.
Domain not reachableDNS not propagated or Elastic IP not associated.Double‑check GoDaddy A/CNAME records and that the Elastic IP is attached to the running EC2 instance.

Additional Issues

500 Internal Server Error
Usually a Django crash. Check the logs with:

docker compose logs web

The most common cause is a DisallowedHost error (verify your .env settings).

Nginx fails to start
Inspect the Nginx logs:

docker compose logs nginx

If the log contains “file not found,” the volume mapping for /etc/letsencrypt is likely incorrect.

Connection Refused
An AWS Security Group is probably blocking port 443. Adjust the security group rules to allow inbound traffic on that port.

Happy deploying! 🚀

Back to Blog

Related posts

Read more »