================================ SSL/TLS (HTTPS) deployment guide ================================ :Author: Zhenyu Yang :Last updated: Jan 24, 2026 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/html`` with SPA routing support via ``try_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.pem`` - ``chmod 644 ./nginx/certs/fullchain.pem`` - Update ``server_name`` in 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: .. code-block:: bash docker compose -f docker-compose.prod.yml exec nginx nginx -t docker compose -f docker-compose.prod.yml exec nginx nginx -s reload Option B: Self-signed certificate (development / internal testing only) ----------------------------------------------------------------------- Self-signed certs cause browser warnings and should not be used for public production, but they are useful for quick functional validation: .. code-block:: bash 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/html`` and falls back to ``/index.html`` for client-side routing. - ``/api/``, ``/media/``, ``/admin/`` are reverse-proxied to the upstream and include standard forwarding headers (including ``X-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: .. code-block:: nginx 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: .. code-block:: nginx add_header Strict-Transport-Security "max-age=31536000; includeSubDomains" always; Only enable HSTS when you are sure HTTPS works correctly for your domain. Add modern security headers (recommended) ----------------------------------------- Typical baseline: .. code-block:: nginx 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 = True`` - ``CSRF_COOKIE_SECURE = True`` Operational Checklist ===================== Before go-live -------------- - Confirm certificate files exist: - ``./nginx/certs/fullchain.pem`` - ``./nginx/certs/privkey.pem`` - Confirm Nginx loads cleanly: .. code-block:: bash docker compose -f docker-compose.prod.yml exec nginx nginx -t - Confirm HTTPS reachability: .. code-block:: bash 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.