From IP to Identity: The Complete Guide to Deploying Django with SSL, Nginx, and Docker
Source: Dev.to
1. The Architectural Blueprint
Before we type a single command, understand the Flow of Traffic:
| Component | Role |
|---|---|
| User | Types yourdomain.com. |
| GoDaddy DNS | Points the domain to your AWS Elastic IP. |
| AWS Security Group | Acts 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.
- Go to EC2 Dashboard → Elastic IPs.
- Click Allocate Elastic IP and then Associate it with your instance.
- 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 Type | Host | Value |
|---|---|---|
| A | @ | Your_Elastic_IP |
| CNAME | www | @ (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:
| File | Description |
|---|---|
fullchain.pem | Public certificate (includes chain). |
privkey.pem | Private 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
| Action | Command |
|---|---|
| Deploy / Update | docker compose up -d --force-recreate |
| Stop Everything | docker compose down |
| Check Logs | docker compose logs -f nginx |
| Verify Certs | ls -la /etc/letsencrypt/live/yourdomain.com/ |
| Check SSL Health | curl -Iv https://yourdomain.com |
7. Troubleshooting Common Errors
| Symptom | Likely Cause | Fix |
|---|---|---|
| 502 Bad Gateway | Nginx 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 failure | Wrong 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 loop | Both 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 errors | CSRF_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 reachable | DNS 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! 🚀
