Skip to main content
HTTPS is essential for production Unmute deployments because browsers require a secure connection (HTTPS) or localhost to grant microphone access. This guide explains how to add HTTPS support to your Unmute deployment.

Why HTTPS?

Modern browsers enforce strict security policies:
  • Microphone access requires either:
    • HTTPS connection with valid certificate, OR
    • Localhost connection (any protocol)
  • WebSocket connections for real-time audio work better over HTTPS
HTTP deployments (without localhost) will be blocked by browser security policies from accessing the microphone.
The easiest way to get HTTPS is to use the Docker Swarm deployment, which includes:
  • Automatic HTTPS via Let’s Encrypt
  • Certificate renewal handled automatically
  • Traefik reverse proxy configured for HTTPS
  • HTTP to HTTPS redirect
From swarm-deploy.yml:
swarm-deploy.yml
traefik:
  command:
    # HTTP and HTTPS entry points
    - "--entrypoints.web.address=:80"
    - "--entrypoints.websecure.address=:443"
    
    # Automatic redirect HTTP → HTTPS
    - "--entrypoints.web.http.redirections.entryPoint.to=websecure"
    - "--entrypoints.web.http.redirections.entryPoint.scheme=https"
    
    # Let's Encrypt automatic certificate management
    - "--certificatesResolvers.letsencrypt_resolver.acme.httpChallenge.entryPoint=web"
    - "--certificatesResolvers.letsencrypt_resolver.acme.storage=/letsencrypt/acme.json"
    - "--certificatesResolvers.letsencrypt_resolver.acme.email=your-email@example.com"
Services automatically get HTTPS:
swarm-deploy.yml
frontend:
  deploy:
    labels:
      - "traefik.enable=true"
      - "traefik.http.routers.frontend.rule=Host(`unmute.example.com`)"
      - "traefik.http.routers.frontend.entrypoints=websecure"
      - "traefik.http.routers.frontend.tls=true"
      - "traefik.http.routers.frontend.tls.certresolver=letsencrypt_resolver"
If you’re using Docker Swarm, HTTPS is already configured. No additional setup needed.

Manual HTTPS Setup

For Docker Compose or Dockerless deployments, you’ll need to add HTTPS support manually.

Prerequisites

  • Domain name pointing to your server’s public IP
  • Ports 80 and 443 open in your firewall
  • Public IP address (Let’s Encrypt needs to verify domain ownership)

Option 1: Add HTTPS to Docker Compose

Modify your docker-compose.yml to enable HTTPS with Let’s Encrypt.
1

Update Traefik Configuration

Edit the traefik service in docker-compose.yml:
docker-compose.yml
traefik:
  image: traefik:v3.3.1
  command:
    - "--providers.docker=true"
    - "--providers.docker.exposedbydefault=false"
    
    # Add HTTPS support
    - "--entrypoints.web.address=:80"
    - "--entrypoints.websecure.address=:443"
    - "--entrypoints.web.http.redirections.entryPoint.to=websecure"
    - "--entrypoints.web.http.redirections.entryPoint.scheme=https"
    
    # Let's Encrypt configuration
    - "--certificatesResolvers.letsencrypt.acme.httpChallenge.entryPoint=web"
    - "--certificatesResolvers.letsencrypt.acme.storage=/letsencrypt/acme.json"
    - "--certificatesResolvers.letsencrypt.acme.email=your-email@example.com"
  ports:
    - "80:80"
    - "443:443"  # Add HTTPS port
  volumes:
    - "/var/run/docker.sock:/var/run/docker.sock:ro"
    - "letsencrypt:/letsencrypt"  # Persist certificates
2

Update Service Labels

Update frontend and backend services to use HTTPS:
docker-compose.yml
frontend:
  labels:
    - "traefik.enable=true"
    - "traefik.http.routers.frontend.rule=Host(`unmute.example.com`)"
    - "traefik.http.routers.frontend.entrypoints=websecure"
    - "traefik.http.routers.frontend.tls=true"
    - "traefik.http.routers.frontend.tls.certresolver=letsencrypt"
    - "traefik.http.services.frontend.loadbalancer.server.port=3000"
    - "traefik.http.routers.frontend.priority=10"

backend:
  labels:
    - "traefik.enable=true"
    - "traefik.http.routers.backend.rule=Host(`unmute.example.com`) && PathPrefix(`/api`)"
    - "traefik.http.routers.backend.entrypoints=websecure"
    - "traefik.http.routers.backend.tls=true"
    - "traefik.http.routers.backend.tls.certresolver=letsencrypt"
    - "traefik.http.routers.backend.middlewares=strip-api"
    - "traefik.http.middlewares.strip-api.replacepathregex.regex=^/api/(.*)"
    - "traefik.http.middlewares.strip-api.replacepathregex.replacement=/$$1"
    - "traefik.http.services.backend.loadbalancer.server.port=80"
    - "traefik.http.routers.backend.priority=100"
3

Add Volume for Certificates

Add the letsencrypt volume at the end of docker-compose.yml:
docker-compose.yml
volumes:
  letsencrypt:
4

Deploy with HTTPS

docker compose down
docker compose up -d
Access your deployment at https://unmute.example.com
Let’s Encrypt certificates are valid for 90 days. Traefik automatically renews them before expiration.

Option 2: Nginx Reverse Proxy

Use Nginx as a reverse proxy with Let’s Encrypt certificates.
1

Install Certbot

sudo apt update
sudo apt install certbot python3-certbot-nginx
2

Obtain Certificate

sudo certbot certonly --nginx -d unmute.example.com
Certificates will be saved to /etc/letsencrypt/live/unmute.example.com/
3

Configure Nginx

Create /etc/nginx/sites-available/unmute:
/etc/nginx/sites-available/unmute
# Redirect HTTP to HTTPS
server {
    listen 80;
    server_name unmute.example.com;
    return 301 https://$server_name$request_uri;
}

# HTTPS server
server {
    listen 443 ssl http2;
    server_name unmute.example.com;
    
    # SSL certificate
    ssl_certificate /etc/letsencrypt/live/unmute.example.com/fullchain.pem;
    ssl_certificate_key /etc/letsencrypt/live/unmute.example.com/privkey.pem;
    
    # SSL configuration
    ssl_protocols TLSv1.2 TLSv1.3;
    ssl_ciphers HIGH:!aNULL:!MD5;
    ssl_prefer_server_ciphers on;
    
    # Proxy to Docker Compose (Traefik on port 80)
    location / {
        proxy_pass http://localhost:80;
        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;
        
        # WebSocket support
        proxy_http_version 1.1;
        proxy_set_header Upgrade $http_upgrade;
        proxy_set_header Connection "upgrade";
    }
}
4

Enable and Restart Nginx

sudo ln -s /etc/nginx/sites-available/unmute /etc/nginx/sites-enabled/
sudo nginx -t
sudo systemctl restart nginx
5

Set Up Auto-Renewal

Certbot installs a cron job automatically, but verify:
sudo certbot renew --dry-run

Option 3: Caddy (Automatic HTTPS)

Caddy is the simplest option - it automatically obtains and renews certificates.
1

Install Caddy

sudo apt install -y debian-keyring debian-archive-keyring apt-transport-https
curl -1sLf 'https://dl.cloudsmith.io/public/caddy/stable/gpg.key' | sudo gpg --dearmor -o /usr/share/keyrings/caddy-stable-archive-keyring.gpg
curl -1sLf 'https://dl.cloudsmith.io/public/caddy/stable/debian.deb.txt' | sudo tee /etc/apt/sources.list.d/caddy-stable.list
sudo apt update
sudo apt install caddy
2

Configure Caddyfile

Edit /etc/caddy/Caddyfile:
/etc/caddy/Caddyfile
unmute.example.com {
    reverse_proxy localhost:80
}
That’s it! Caddy automatically:
  • Obtains Let’s Encrypt certificate
  • Configures HTTPS
  • Redirects HTTP to HTTPS
  • Renews certificates
3

Restart Caddy

sudo systemctl restart caddy
Caddy is the easiest option for automatic HTTPS. No certificate management needed.

Dockerless HTTPS Setup

For Dockerless deployments, you need to configure the frontend and backend separately.

Option: Nginx for Dockerless

/etc/nginx/sites-available/unmute
server {
    listen 443 ssl http2;
    server_name unmute.example.com;
    
    ssl_certificate /etc/letsencrypt/live/unmute.example.com/fullchain.pem;
    ssl_certificate_key /etc/letsencrypt/live/unmute.example.com/privkey.pem;
    
    # Frontend (Next.js on port 3000)
    location / {
        proxy_pass http://localhost:3000;
        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;
    }
    
    # Backend API (FastAPI on port 8000)
    location /api/ {
        rewrite ^/api/(.*) /$1 break;
        proxy_pass http://localhost:8000;
        proxy_set_header Host $host;
        proxy_set_header X-Real-IP $remote_addr;
        
        # WebSocket support
        proxy_http_version 1.1;
        proxy_set_header Upgrade $http_upgrade;
        proxy_set_header Connection "upgrade";
    }
}

# Redirect HTTP to HTTPS
server {
    listen 80;
    server_name unmute.example.com;
    return 301 https://$server_name$request_uri;
}

Testing HTTPS Configuration

1

Verify Certificate

curl -I https://unmute.example.com
Should show HTTP/2 200 or HTTP/1.1 200
2

Test SSL Configuration

Use SSL Labs to check your configuration:
https://www.ssllabs.com/ssltest/analyze.html?d=unmute.example.com
Aim for an A or A+ rating.
3

Verify Microphone Access

Open https://unmute.example.com in your browser and test that:
  • Page loads correctly
  • No certificate warnings
  • Microphone permission prompt appears

Troubleshooting

Certificate Not Issued

Issue: Let’s Encrypt fails to issue certificate Solutions:
  • Verify domain points to your server: nslookup unmute.example.com
  • Check ports 80 and 443 are open: sudo ufw status
  • Ensure no other service is using port 80/443
  • Check Let’s Encrypt logs: sudo journalctl -u certbot

Mixed Content Errors

Issue: Browser shows mixed content warnings Solution: Ensure all resources (API calls, WebSockets) use HTTPS URLs, not HTTP.

WebSocket Connection Fails

Issue: WebSocket connections fail over HTTPS Solutions:
  • Verify proxy passes Upgrade header:
    proxy_http_version 1.1;
    proxy_set_header Upgrade $http_upgrade;
    proxy_set_header Connection "upgrade";
    
  • Check for timeout settings - WebSocket connections are long-lived
  • Test WebSocket directly: wscat -c wss://unmute.example.com/api/ws

Certificate Renewal Fails

Issue: Certificate expired or auto-renewal failed Solutions:
  • Test renewal: sudo certbot renew --dry-run
  • Check renewal cron job: sudo systemctl status certbot.timer
  • Manually renew: sudo certbot renew
  • Restart web server after renewal

Security Best Practices

Follow these security practices for production deployments:

Strong SSL Configuration

Use modern TLS versions and ciphers:
ssl_protocols TLSv1.2 TLSv1.3;
ssl_ciphers 'ECDHE-ECDSA-AES128-GCM-SHA256:ECDHE-RSA-AES128-GCM-SHA256:ECDHE-ECDSA-AES256-GCM-SHA384:ECDHE-RSA-AES256-GCM-SHA384';
ssl_prefer_server_ciphers off;

HSTS (HTTP Strict Transport Security)

Force browsers to always use HTTPS:
add_header Strict-Transport-Security "max-age=31536000; includeSubDomains" always;

Certificate Monitoring

Set up monitoring to alert before certificate expiration:
# Check expiration
sudo certbot certificates

# Days until expiration
openssl x509 -enddate -noout -in /etc/letsencrypt/live/unmute.example.com/fullchain.pem

Alternative: SSH Tunneling

If setting up HTTPS is too complex, consider using SSH port forwarding instead:
  • No domain name required
  • No certificate management
  • Works immediately
  • Suitable for personal/development use
HTTPS is recommended for production, but SSH tunneling is easier for testing.

Next Steps

Build docs developers (and LLMs) love