Last updated June 23, 2026
SSL & Reverse Proxy Guide
This guide covers how to configure SSL/HTTPS and use your own reverse proxy with the platform.
Table of Contents
- Overview
- SSL/HTTPS Setup
- Using Your Own Reverse Proxy
- Routing Table
- Example Configurations
- Security Recommendations
Related Guides:
Overview
The default deployment includes an nginx container as a reverse proxy. This handles:
- Routing requests to the appropriate service (web frontend or API server)
- Load balancing
- Static file serving
- WebSocket connections
You can either:
- Use the built-in nginx with SSL certificates
- Use your own reverse proxy (Traefik, Caddy, HAProxy, etc.)
SSL/HTTPS Setup
Option 1: SSL Termination at Load Balancer
If you have an AWS ALB, Azure Application Gateway, or similar:
- Configure SSL certificate on load balancer
- Forward port 443 → port 80 on the platform
- No changes needed to the platform configuration
Advantages:
- Centralized certificate management
- Automatic certificate renewal (with ACM, Let's Encrypt, etc.)
- Offload SSL processing from application
Option 2: Custom nginx Configuration
Replace nginx.conf with an SSL-enabled version:
# Redirect HTTP to HTTPS
server {
listen 80;
server_name your-domain.com;
return 301 https://$host$request_uri;
}
# HTTPS server
server {
listen 443 ssl http2;
server_name your-domain.com;
# SSL certificates
ssl_certificate /etc/nginx/ssl/cert.pem;
ssl_certificate_key /etc/nginx/ssl/key.pem;
# SSL configuration
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;
ssl_session_cache shared:SSL:10m;
ssl_session_timeout 1d;
# Security headers
add_header Strict-Transport-Security "max-age=31536000; includeSubDomains" always;
add_header X-Frame-Options DENY;
add_header X-Content-Type-Options nosniff;
# API routes
location /api/ {
proxy_pass http://server:3001;
proxy_http_version 1.1;
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;
}
# Health check
location /health {
proxy_pass http://server:3001;
}
# Platform routes
location /platform/ {
proxy_pass http://server:3001;
}
# Well-known routes
location /.well-known/ {
proxy_pass http://server:3001;
}
# WebSocket
location /socket.io/ {
proxy_pass http://server:3001;
proxy_http_version 1.1;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection "upgrade";
proxy_set_header Host $host;
}
# Frontend (catch-all)
location / {
proxy_pass http://web:3000;
proxy_http_version 1.1;
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;
}
}Mount certificates in docker-compose:
services:
proxy:
# ... existing config
volumes:
- ./nginx.conf:/etc/nginx/conf.d/default.conf:ro
- ./ssl:/etc/nginx/ssl:ro
ports:
- "80:80"
- "443:443"Option 3: Let's Encrypt with Certbot
Use Certbot for free, automatic SSL certificates:
# Install certbot
apt-get install certbot
# Get certificate
certbot certonly --standalone -d your-domain.com
# Certificates are stored in:
# /etc/letsencrypt/live/your-domain.com/fullchain.pem
# /etc/letsencrypt/live/your-domain.com/privkey.pemMount Let's Encrypt certificates:
services:
proxy:
volumes:
- /etc/letsencrypt/live/your-domain.com/fullchain.pem:/etc/nginx/ssl/cert.pem:ro
- /etc/letsencrypt/live/your-domain.com/privkey.pem:/etc/nginx/ssl/key.pem:roUsing Your Own Reverse Proxy
If you already have a reverse proxy (Traefik, Caddy, HAProxy, etc.), you can skip the built-in nginx container and route traffic directly to the application containers.
Why Use a Reverse Proxy?
The web container serves the Next.js frontend, but browser API calls need to reach the server container. A reverse proxy routes requests based on path:
/api/*routes to the backend server- Everything else routes to the web frontend
This allows both containers to appear as a single origin to the browser, avoiding CORS issues.
Skipping the Built-in nginx Container
To use your own proxy, modify your docker-compose to:
- Remove or comment out the
proxyservice - Expose the server and web ports directly
services:
server:
# ... existing config
ports:
- "3001:3001" # Expose for your proxy
web:
# ... existing config
ports:
- "3000:3000" # Expose for your proxy
# Comment out or remove:
# proxy:
# image: ...Or if your proxy is on the same Docker network, you can access containers by name without exposing ports.
Routing Table
Route these paths to the appropriate containers:
| Path | Destination | Notes |
|---|---|---|
/api/* | server:3001 | All API endpoints |
/health | server:3001 | Health check (no /api prefix) |
/platform/* | server:3001 | Portal API (no /api prefix) |
/.well-known/* | server:3001 | JWKS/OpenID (no /api prefix) |
/socket.io/* | server:3001 | WebSocket - requires upgrade headers |
/* | web:3000 | Frontend (catch-all, lowest priority) |
WebSocket Requirements
The /socket.io/* path requires WebSocket upgrade headers:
Upgrade: websocket
Connection: upgradeExample Configurations
Traefik
labels:
# Enable Traefik
- "traefik.enable=true"
# Route server paths
- "traefik.http.routers.api.rule=PathPrefix(`/api`) || PathPrefix(`/health`) || PathPrefix(`/platform`) || PathPrefix(`/.well-known`) || PathPrefix(`/socket.io`)"
- "traefik.http.routers.api.service=server"
- "traefik.http.services.server.loadbalancer.server.port=3001"
# Route frontend (catch-all)
- "traefik.http.routers.web.rule=PathPrefix(`/`)"
- "traefik.http.routers.web.priority=1"
- "traefik.http.routers.web.service=web"
- "traefik.http.services.web.loadbalancer.server.port=3000"
# HTTPS redirect
- "traefik.http.routers.api.entrypoints=websecure"
- "traefik.http.routers.api.tls=true"
- "traefik.http.routers.web.entrypoints=websecure"
- "traefik.http.routers.web.tls=true"Caddy
your-domain.com {
# Server routes
handle /api/* {
reverse_proxy server:3001
}
handle /health {
reverse_proxy server:3001
}
handle /platform/* {
reverse_proxy server:3001
}
handle /.well-known/* {
reverse_proxy server:3001
}
handle /socket.io/* {
reverse_proxy server:3001
}
# Frontend (catch-all)
handle {
reverse_proxy web:3000
}
}Caddy automatically handles SSL with Let's Encrypt!
HAProxy
global
maxconn 4096
ssl-default-bind-ciphers ECDHE-ECDSA-AES128-GCM-SHA256:ECDHE-RSA-AES128-GCM-SHA256
ssl-default-bind-options no-sslv3 no-tlsv10 no-tlsv11
defaults
mode http
timeout connect 5s
timeout client 50s
timeout server 50s
frontend http
bind *:80
redirect scheme https if !{ ssl_fc }
frontend https
bind *:443 ssl crt /etc/haproxy/certs/your-domain.pem
# Server routes
acl is_api path_beg /api/
acl is_health path /health
acl is_platform path_beg /platform/
acl is_wellknown path_beg /.well-known/
acl is_socketio path_beg /socket.io/
use_backend server if is_api or is_health or is_platform or is_wellknown or is_socketio
default_backend web
backend server
server server1 server:3001 check
backend web
server web1 web:3000 checkAWS Application Load Balancer
Configure rules in order:
- Path: /api/* → Target Group: contextdx-server (port 3001)
- Path: /health → Target Group: contextdx-server (port 3001)
- Path: /platform/* → Target Group: contextdx-server (port 3001)
- Path: /.well-known/* → Target Group: contextdx-server (port 3001)
- Path: /socket.io/* → Target Group: contextdx-server (port 3001)
- Default → Target Group: contextdx-web (port 3000)
Enable sticky sessions for WebSocket support.
Security Recommendations
Network Security
-
Firewall configuration
- Restrict access to port 80/443 to known IP ranges if possible
- Block direct access to ports 3000/3001 from the internet
- Only allow outbound traffic to required destinations
-
Load balancer
- Use a load balancer for SSL termination
- Enable DDoS protection
- Configure rate limiting
-
Internal network
- Use Docker internal networks
- Don't expose container ports unnecessarily
SSL/TLS Best Practices
-
Use TLS 1.2 or higher
NGINXssl_protocols TLSv1.2 TLSv1.3; -
Strong cipher suites
NGINXssl_ciphers ECDHE-ECDSA-AES128-GCM-SHA256:ECDHE-RSA-AES128-GCM-SHA256; ssl_prefer_server_ciphers off; -
HSTS (HTTP Strict Transport Security)
NGINXadd_header Strict-Transport-Security "max-age=31536000; includeSubDomains" always; -
Certificate renewal
- Set up automatic certificate renewal
- Monitor certificate expiration dates
Data Security
- Encrypt backups containing sensitive data
- Use strong passwords for database
- Rotate credentials periodically
- Audit access logs regularly
Updates
- Subscribe to security announcements
- Apply updates promptly
- Test upgrades in staging environment first
- Keep Docker and host OS updated
Quick Reference
| Task | Command/Config |
|---|---|
| Test SSL config | openssl s_client -connect your-domain.com:443 |
| Check certificate expiry | openssl s_client -connect your-domain.com:443 2>/dev/null | openssl x509 -noout -dates |
| Reload nginx config | docker compose -f docker-compose.production.yml exec proxy nginx -s reload |
| Test nginx config | docker compose -f docker-compose.production.yml exec proxy nginx -t |
| Check routing | curl -I http://localhost/api/health |