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.
Recommended Approach: Docker Swarm
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:
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:
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.
Update Traefik Configuration
Edit the traefik service in 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
Update Service Labels
Update frontend and backend services to use HTTPS: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"
Add Volume for Certificates
Add the letsencrypt volume at the end of docker-compose.yml: 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.
Install Certbot
sudo apt update
sudo apt install certbot python3-certbot-nginx
Obtain Certificate
sudo certbot certonly --nginx -d unmute.example.com
Certificates will be saved to /etc/letsencrypt/live/unmute.example.com/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";
}
}
Enable and Restart Nginx
sudo ln -s /etc/nginx/sites-available/unmute /etc/nginx/sites-enabled/
sudo nginx -t
sudo systemctl restart nginx
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.
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
Configure Caddyfile
Edit /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
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
Verify Certificate
curl -I https://unmute.example.com
Should show HTTP/2 200 or HTTP/1.1 200Test 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. 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