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

  1. Overview
  2. SSL/HTTPS Setup
  3. Using Your Own Reverse Proxy
  4. Routing Table
  5. Example Configurations
  6. 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:

  1. Use the built-in nginx with SSL certificates
  2. 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:

  1. Configure SSL certificate on load balancer
  2. Forward port 443 → port 80 on ContextDX
  3. 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:

YAML
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:

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:

YAML
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: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:

  1. Remove or comment out the proxy service
  2. Expose the server and web ports directly
YAML
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:

PathDestinationNotes
/api/*server:3001All API endpoints
/healthserver:3001Health check (no /api prefix)
/platform/*server:3001Portal API (no /api prefix)
/.well-known/*server:3001JWKS/OpenID (no /api prefix)
/socket.io/*server:3001WebSocket - requires upgrade headers
/*web:3000Frontend (catch-all, lowest priority)

WebSocket Requirements

The /socket.io/* path requires WebSocket upgrade headers:

Upgrade: websocket
Connection: upgrade

Example Configurations

Traefik

YAML
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 check

AWS Application Load Balancer

Configure rules in order:

  1. Path: /api/* → Target Group: contextdx-server (port 3001)
  2. Path: /health → Target Group: contextdx-server (port 3001)
  3. Path: /platform/* → Target Group: contextdx-server (port 3001)
  4. Path: /.well-known/* → Target Group: contextdx-server (port 3001)
  5. Path: /socket.io/* → Target Group: contextdx-server (port 3001)
  6. Default → Target Group: contextdx-web (port 3000)

Enable sticky sessions for WebSocket support.


Security Recommendations

Network Security

  1. 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
  2. Load balancer

    • Use a load balancer for SSL termination
    • Enable DDoS protection
    • Configure rate limiting
  3. Internal network

    • Use Docker internal networks
    • Don't expose container ports unnecessarily

SSL/TLS Best Practices

  1. Use TLS 1.2 or higher

    NGINX
    ssl_protocols TLSv1.2 TLSv1.3;
  2. Strong cipher suites

    NGINX
    ssl_ciphers ECDHE-ECDSA-AES128-GCM-SHA256:ECDHE-RSA-AES128-GCM-SHA256;
    ssl_prefer_server_ciphers off;
  3. HSTS (HTTP Strict Transport Security)

    NGINX
    add_header Strict-Transport-Security "max-age=31536000; includeSubDomains" always;
  4. Certificate renewal

    • Set up automatic certificate renewal
    • Monitor certificate expiration dates

Data Security

  1. Encrypt backups containing sensitive data
  2. Use strong passwords for database
  3. Rotate credentials periodically
  4. Audit access logs regularly

Updates

  1. Subscribe to security announcements
  2. Apply updates promptly
  3. Test upgrades in staging environment first
  4. Keep Docker and host OS updated

Quick Reference

TaskCommand/Config
Test SSL configopenssl s_client -connect your-domain.com:443
Check certificate expiryopenssl s_client -connect your-domain.com:443 2>/dev/null | openssl x509 -noout -dates
Reload nginx configdocker compose -f docker-compose.production.yml exec proxy nginx -s reload
Test nginx configdocker compose -f docker-compose.production.yml exec proxy nginx -t
Check routingcurl -I http://localhost/api/health