TLS (HTTPS) Deployment Guide
This document describes how HTTPS is implemented for the Classroom Manager production deployment using Nginx as the TLS termination point and reverse proxy in a Docker Compose stack.
Architecture Overview
In production, a dedicated Nginx container is responsible for:
- Terminating TLS on port 443 (HTTPS) and serving the certificate and private key.
- Serving the built frontend as static files from
/usr/share/nginx/htmlwith SPA routing support viatry_files. - Reverse-proxying backend requests (Django/Gunicorn) to the internal upstream
backend:8000. - Forwarding common application paths to the backend:
/api/,/media/,/admin/, and/static/.
From the Compose perspective:
- The Nginx service publishes host ports 80 and 443.
- Nginx mounts the production config and the certificate directory as read-only volumes.
- Nginx depends on the backend container and joins the same Docker network for service discovery.
Files and Paths
Nginx Config
Nginx loads the production virtual host config from:
- Host path:
./nginx/nginx.conf - Container path:
/etc/nginx/conf.d/default.conf(read-only)
Certificate Directory
Certificates are expected at:
- Host path:
./nginx/certs/ - Container path:
/etc/nginx/certs/(read-only)
And referenced by Nginx as:
/etc/nginx/certs/fullchain.pem(certificate chain)/etc/nginx/certs/privkey.pem(private key)
Prerequisites
You must have a valid TLS certificate + private key pair available as:
./nginx/certs/fullchain.pem./nginx/certs/privkey.pem
Ensure file permissions are appropriate on the host (private key should be restricted):
chmod 600 ./nginx/certs/privkey.pemchmod 644 ./nginx/certs/fullchain.pem
Update
server_namein the Nginx config to match your real domain.
Certificate Provisioning
Option A: Use a Trusted CA (Recommended)
Obtain a certificate from a trusted CA (e.g., via ACME/Let's Encrypt) on the host (or a dedicated cert management container), then copy/symlink the resulting files into ./nginx/certs/:
- Place the full chain as
./nginx/certs/fullchain.pem - Place the private key as
./nginx/certs/privkey.pem
After renewal, reload Nginx so it picks up the new files:
docker compose -f docker-compose.prod.yml exec nginx nginx -t
docker compose -f docker-compose.prod.yml exec nginx nginx -s reloadOption B: Self-signed Certificate (Development / Internal Testing Only)
WARNING
Self-signed certs cause browser warnings and should not be used for public production, but they are useful for quick functional validation.
mkdir -p ./nginx/certs
openssl req -x509 -nodes -newkey rsa:2048 -days 365 \
-keyout ./nginx/certs/privkey.pem \
-out ./nginx/certs/fullchain.pem \
-subj "/CN=localhost"Nginx TLS Configuration (Current Behavior)
TLS Listener
The server block listens on 443 with SSL enabled.
Certificates
Nginx loads the certificate chain and key from the mounted cert directory.
Protocols and Session Settings
The config enables TLS 1.2 and TLS 1.3, uses an SSL session cache, sets session timeout, and disables session tickets.
Routing Behavior
/serves the SPA frontend from/usr/share/nginx/htmland falls back to/index.htmlfor client-side routing./api/,/media/,/admin/are reverse-proxied to the upstream and include standard forwarding headers (includingX-Forwarded-Proto)./static/is also proxied to the backend.
Hardening and Production Recommendations
Add an HTTP (Port 80) Redirect to HTTPS
If port 80 is exposed, add a separate server block to redirect all HTTP traffic to HTTPS:
server {
listen 80;
server_name example.com;
return 301 https://$host$request_uri;
}Use a Real Domain and Consider HSTS
Replace server_name localhost; with your actual domain. After you confirm HTTPS is stable, consider adding HSTS:
add_header Strict-Transport-Security "max-age=31536000; includeSubDomains" always;DANGER
Only enable HSTS when you are sure HTTPS works correctly for your domain.
Add Modern Security Headers (Recommended)
Typical baseline:
add_header X-Content-Type-Options nosniff always;
add_header X-Frame-Options DENY always;
add_header Referrer-Policy strict-origin-when-cross-origin always;Validate Django Settings Behind a Reverse Proxy
Because Nginx forwards X-Forwarded-Proto, ensure Django is configured to trust that header so it correctly detects secure requests and generates HTTPS URLs. Common settings include:
SECURE_PROXY_SSL_HEADER = ("HTTP_X_FORWARDED_PROTO", "https")CSRF_TRUSTED_ORIGINS = ["https://example.com"]SESSION_COOKIE_SECURE = TrueCSRF_COOKIE_SECURE = True
Operational Checklist
Before Go-live
Confirm certificate files exist:
./nginx/certs/fullchain.pem./nginx/certs/privkey.pem
Confirm Nginx loads cleanly:
docker compose -f docker-compose.prod.yml exec nginx nginx -t- Confirm HTTPS reachability:
curl -vkI https://YOUR_DOMAIN/
curl -vkI https://YOUR_DOMAIN/api/After Go-live
- Enable automatic certificate renewal (if using ACME).
- Reload Nginx after renewal.
- Periodically verify TLS and headers from an external network.