Skip to main content
HTTPS is essential for production FastAPI applications. Understanding how HTTPS works will help you configure it correctly.

Why HTTPS Matters

HTTPS (HTTP over TLS) provides:
  • Encryption - Protects data in transit from eavesdropping
  • Authentication - Verifies the server’s identity
  • Integrity - Prevents tampering with data
  • Trust - Required by browsers for modern web features
  • SEO - Search engines favor HTTPS sites
Running production APIs without HTTPS exposes sensitive data and user credentials to attackers.

How HTTPS Works

HTTPS is not just “HTTP with encryption enabled”. It’s more complex than that.

Key Concepts

1

Certificates Required

The server needs TLS certificates from a trusted third party (Certificate Authority).
2

Certificates Expire

Certificates have a lifetime (typically 90 days with Let’s Encrypt) and must be renewed.
3

TCP-Level Encryption

Encryption happens at the TCP level, below HTTP. The certificate is used before HTTP communication begins.
4

Domain-Specific

Each certificate is tied to a specific domain name, not an IP address.
5

SNI Extension

Server Name Indication (SNI) allows multiple HTTPS certificates on a single IP address.
For an interactive explanation of HTTPS basics, visit https://howhttps.works/.

TLS Termination Proxy

The standard approach is using a TLS Termination Proxy:
Client (HTTPS) → TLS Proxy → FastAPI App (HTTP)
The proxy:
  1. Receives encrypted HTTPS requests
  2. Decrypts them using TLS certificates
  3. Forwards plain HTTP to your FastAPI app
  4. Encrypts responses before sending to client

Why Use a Proxy?

  • Certificate management - Centralized certificate storage and renewal
  • Multiple applications - One proxy can handle HTTPS for many apps
  • Zero downtime - Renew certificates without restarting your app
  • Simplicity - FastAPI app doesn’t need to handle TLS
Your FastAPI application runs plain HTTP internally. The TLS proxy handles all HTTPS complexity.

Let’s Encrypt

Let’s Encrypt provides free, automated TLS certificates.

Benefits

  • Free - No cost for certificates
  • Automated - Automatic issuance and renewal
  • Trusted - Accepted by all major browsers
  • Short-lived - 90-day lifetime improves security
  • Standard - Uses industry-standard cryptography

How It Works

1

Domain Verification

Prove you control the domain (DNS challenge or HTTP challenge).
2

Certificate Issuance

Let’s Encrypt issues a certificate valid for 90 days.
3

Automatic Renewal

Renewal happens automatically before expiration.
Let’s Encrypt certificates are identical in functionality to paid certificates, just with automated issuance.

TLS Termination Proxy Options

Traefik is a modern reverse proxy with automatic HTTPS. docker-compose.yml:
services:
  traefik:
    image: traefik:v3.0
    command:
      - "--api.insecure=true"
      - "--providers.docker=true"
      - "--entrypoints.web.address=:80"
      - "--entrypoints.websecure.address=:443"
      - "--certificatesresolvers.letsencrypt.acme.email=your-email@example.com"
      - "--certificatesresolvers.letsencrypt.acme.storage=/letsencrypt/acme.json"
      - "--certificatesresolvers.letsencrypt.acme.httpchallenge.entrypoint=web"
    ports:
      - "80:80"
      - "443:443"
    volumes:
      - "/var/run/docker.sock:/var/run/docker.sock:ro"
      - "./letsencrypt:/letsencrypt"

  app:
    build: .
    labels:
      - "traefik.enable=true"
      - "traefik.http.routers.app.rule=Host(`api.example.com`)"
      - "traefik.http.routers.app.entrypoints=websecure"
      - "traefik.http.routers.app.tls.certresolver=letsencrypt"
Traefik automatically:
  • Obtains Let’s Encrypt certificates
  • Renews certificates before expiration
  • Routes requests to your application
  • Redirects HTTP to HTTPS
Traefik is the easiest option for Docker-based deployments with automatic certificate management.

Caddy

Caddy automatically enables HTTPS with zero configuration. Caddyfile:
api.example.com {
    reverse_proxy localhost:8000
}
That’s it! Caddy automatically:
  • Obtains certificates from Let’s Encrypt
  • Renews certificates
  • Redirects HTTP to HTTPS
  • Configures secure TLS settings
Docker Compose:
services:
  caddy:
    image: caddy:2
    ports:
      - "80:80"
      - "443:443"
    volumes:
      - ./Caddyfile:/etc/caddy/Caddyfile
      - caddy_data:/data
      - caddy_config:/config

  app:
    build: .
    expose:
      - "8000"

volumes:
  caddy_data:
  caddy_config:
Caddy is the simplest option - HTTPS is automatic with just the domain name in the config.

Nginx with Certbot

Nginx is a popular web server that can act as a reverse proxy. nginx.conf:
server {
    listen 80;
    server_name api.example.com;
    
    location /.well-known/acme-challenge/ {
        root /var/www/certbot;
    }
    
    location / {
        return 301 https://$server_name$request_uri;
    }
}

server {
    listen 443 ssl;
    server_name api.example.com;
    
    ssl_certificate /etc/letsencrypt/live/api.example.com/fullchain.pem;
    ssl_certificate_key /etc/letsencrypt/live/api.example.com/privkey.pem;
    
    location / {
        proxy_pass http://localhost:8000;
        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;
    }
}
docker-compose.yml:
services:
  nginx:
    image: nginx:latest
    ports:
      - "80:80"
      - "443:443"
    volumes:
      - ./nginx.conf:/etc/nginx/conf.d/default.conf
      - ./certbot/conf:/etc/letsencrypt
      - ./certbot/www:/var/www/certbot
    depends_on:
      - app
  
  certbot:
    image: certbot/certbot
    volumes:
      - ./certbot/conf:/etc/letsencrypt
      - ./certbot/www:/var/www/certbot
    entrypoint: "/bin/sh -c 'trap exit TERM; while :; do certbot renew; sleep 12h & wait $${!}; done;'"
  
  app:
    build: .
    expose:
      - "8000"
Obtain initial certificate:
docker compose run --rm certbot certonly --webroot --webroot-path=/var/www/certbot -d api.example.com
Nginx requires more manual configuration than Traefik or Caddy but offers maximum flexibility.

HAProxy

HAProxy is a high-performance load balancer. haproxy.cfg:
frontend https_front
    bind *:443 ssl crt /etc/haproxy/certs/
    default_backend fastapi_backend

backend fastapi_backend
    balance roundrobin
    server app1 localhost:8000 check
Certificate setup:
# Combine cert and key
cat /etc/letsencrypt/live/api.example.com/fullchain.pem \
    /etc/letsencrypt/live/api.example.com/privkey.pem \
    > /etc/haproxy/certs/api.example.com.pem

Proxy Headers Configuration

When behind a proxy, FastAPI needs to know about the original request.

The Problem

Your FastAPI app receives:
  • HTTP requests (not HTTPS)
  • From proxy IP (not client IP)
  • On internal domain (not public domain)

The Solution

Proxies send forwarded headers:
  • X-Forwarded-For - Original client IP
  • X-Forwarded-Proto - Original protocol (https)
  • X-Forwarded-Host - Original host header

FastAPI Configuration

Enable proxy header trust:
# Trust proxy headers from all IPs (when behind trusted proxy)
fastapi run --proxy-headers main.py

# Or with Uvicorn
uvicorn main:app --proxy-headers
Docker Dockerfile:
CMD ["fastapi", "run", "app/main.py", "--port", "80", "--proxy-headers"]
Only enable --proxy-headers when running behind a trusted proxy. Never enable it for directly exposed applications.

Advanced Proxy Configuration

For specific proxy IPs:
uvicorn main:app --forwarded-allow-ips="192.168.1.100,192.168.1.101"
Programmatic configuration:
import uvicorn

if __name__ == "__main__":
    uvicorn.run(
        "main:app",
        host="0.0.0.0",
        port=8000,
        proxy_headers=True,
        forwarded_allow_ips="*",  # Trust all IPs (only when behind proxy)
    )

Cloud Platform HTTPS

Most cloud platforms handle HTTPS automatically.

AWS (Application Load Balancer)

# Configure ALB with SSL certificate
# FastAPI runs plain HTTP internally
ALB (HTTPS) → Target Group → ECS/EC2 (HTTP:8000)
Your FastAPI app:
uvicorn main:app --host 0.0.0.0 --port 8000 --proxy-headers

Google Cloud Run

Cloud Run provides automatic HTTPS:
# Deploy container
gcloud run deploy --image gcr.io/project/app --platform managed

# HTTPS URL automatically provided
# https://app-xxx-uc.a.run.app

Azure App Service

App Service includes automatic HTTPS:
az webapp up --name myapp --runtime "PYTHON:3.12"

# HTTPS enabled at https://myapp.azurewebsites.net
Cloud platforms typically handle HTTPS, certificates, and renewal automatically. Your app just needs --proxy-headers.

Kubernetes Ingress

Kubernetes uses Ingress controllers for HTTPS.

With cert-manager

ingress.yaml:
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
  name: fastapi-ingress
  annotations:
    cert-manager.io/cluster-issuer: "letsencrypt-prod"
spec:
  tls:
  - hosts:
    - api.example.com
    secretName: fastapi-tls
  rules:
  - host: api.example.com
    http:
      paths:
      - path: /
        pathType: Prefix
        backend:
          service:
            name: fastapi-service
            port:
              number: 80
cluster-issuer.yaml:
apiVersion: cert-manager.io/v1
kind: ClusterIssuer
metadata:
  name: letsencrypt-prod
spec:
  acme:
    server: https://acme-v02.api.letsencrypt.org/directory
    email: [email protected]
    privateKeySecretRef:
      name: letsencrypt-prod
    solvers:
    - http01:
        ingress:
          class: nginx
Apply:
kubectl apply -f cluster-issuer.yaml
kubectl apply -f ingress.yaml
cert-manager automates Let’s Encrypt certificates in Kubernetes, handling both issuance and renewal.

Local Development with HTTPS

For local development, use mkcert for trusted certificates.

Install mkcert

# macOS
brew install mkcert
brew install nss  # For Firefox

# Linux
sudo apt install libnss3-tools
wget https://github.com/FiloSottile/mkcert/releases/download/v1.4.4/mkcert-v1.4.4-linux-amd64
chmod +x mkcert-v1.4.4-linux-amd64
sudo mv mkcert-v1.4.4-linux-amd64 /usr/local/bin/mkcert

Create Certificates

# Install local CA
mkcert -install

# Generate certificate
mkdir certs
cd certs
mkcert localhost 127.0.0.1 ::1

Use with Uvicorn

uvicorn main:app \
  --ssl-keyfile=./certs/localhost+2-key.pem \
  --ssl-certfile=./certs/localhost+2.pem
Access at: https://localhost:8000
Only use self-signed certificates for development. Production must use certificates from trusted CAs like Let’s Encrypt.

Security Best Practices

Modern TLS Configuration

Nginx example with strong security:
ssl_protocols TLSv1.2 TLSv1.3;
ssl_prefer_server_ciphers on;
ssl_ciphers 'ECDHE-ECDSA-AES128-GCM-SHA256:ECDHE-RSA-AES128-GCM-SHA256';
ssl_session_cache shared:SSL:10m;
ssl_session_timeout 10m;
ssl_stapling on;
ssl_stapling_verify on;

add_header Strict-Transport-Security "max-age=31536000; includeSubDomains" always;

HTTP Strict Transport Security (HSTS)

Force HTTPS for all future requests:
from fastapi import FastAPI
from fastapi.middleware.trustedhost import TrustedHostMiddleware
from fastapi.middleware.httpsredirect import HTTPSRedirectMiddleware

app = FastAPI()

# Redirect HTTP to HTTPS
app.add_middleware(HTTPSRedirectMiddleware)

# Only allow specific hosts
app.add_middleware(TrustedHostMiddleware, allowed_hosts=["api.example.com"])
Or configure in proxy:
add_header Strict-Transport-Security "max-age=63072000; includeSubDomains; preload" always;
Enable HSTS after confirming HTTPS works correctly. HSTS is difficult to reverse once enabled.

Troubleshooting

Certificate Not Trusted

Problem: Browser shows “Certificate not trusted” error. Solutions:
  • Verify domain DNS points to your server
  • Check certificate includes correct domain
  • Ensure certificate chain is complete
  • Wait for DNS propagation (up to 48 hours)

Mixed Content Warnings

Problem: HTTPS page loading HTTP resources. Solution: Ensure all resources use HTTPS:
<!-- ❌ Wrong -->
<img src="http://example.com/image.jpg">

<!-- ✅ Correct -->
<img src="https://example.com/image.jpg">

<!-- ✅ Protocol-relative -->
<img src="//example.com/image.jpg">

Certificate Renewal Failing

Problem: Let’s Encrypt renewal fails. Solutions:
  • Check port 80 is accessible (HTTP challenge)
  • Verify DNS records are correct
  • Check certbot/Traefik logs
  • Ensure webroot path is correct

Proxy Headers Not Working

Problem: FastAPI sees proxy IP instead of client IP. Solution: Verify both proxy and FastAPI configuration:
# Nginx - ensure headers are set
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
proxy_set_header X-Forwarded-Host $host;
# FastAPI - enable proxy headers
fastapi run --proxy-headers main.py

Recap

HTTPS for FastAPI involves:
  • TLS Termination Proxy - Handles HTTPS externally (Traefik, Caddy, Nginx)
  • Let’s Encrypt - Free automated certificates
  • Proxy Headers - Enable with --proxy-headers flag
  • Automatic Renewal - Use tools that renew certificates automatically
  • Security Headers - Add HSTS and other security headers
Best approach:
  1. Development: Use mkcert for local HTTPS
  2. Production (Docker): Use Traefik or Caddy for automatic HTTPS
  3. Production (Cloud): Use platform’s built-in HTTPS
  4. Production (Kubernetes): Use Ingress with cert-manager
Start with Traefik or Caddy for the simplest automatic HTTPS setup. Both handle certificates with zero configuration.

Build docs developers (and LLMs) love