SSL & Reverse Proxy Guide
This guide covers how to configure SSL/HTTPS and use your own reverse proxy with ContextDX.
ContextDX SSL & Reverse Proxy Guide
This guide covers how to configure SSL/HTTPS and use your own reverse proxy with ContextDX.
Table of Contents
- Overview
- SSL/HTTPS Setup
- Using Your Own Reverse Proxy
- Routing Table
- Example Configurations
- Security Recommendations
Related Guides:
Overview
The default ContextDX 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 ContextDX
- No changes needed to ContextDX 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:
NGINX# 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:
YAMLservices: 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:
BASH# 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.pem
Mount Let's Encrypt certificates:
YAMLservices: 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:ro
Using 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 ContextDX 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
YAMLservices: 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: upgrade
Example Configurations
Traefik
YAMLlabels: # 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 check
AWS 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 |