Sophon Docs
Self-Hosting

SSL & Reverse Proxy

Configure HTTPS for production Sophon deployments with Caddy, Nginx, or Traefik.

Why HTTPS

Production deployments should always use HTTPS:

  • Encrypts API keys and credentials in transit
  • Required for secure WebSocket connections (SignalR uses wss://)
  • Enables HTTP/2 for better performance
  • Browsers block mixed content (HTTP API calls from HTTPS pages)

Sophon runs HTTP internally. A reverse proxy terminates SSL and forwards traffic to the Gateway (port 8081) and Dashboard (port 8080).

Caddy provides automatic HTTPS via Let's Encrypt with zero configuration. It handles certificate issuance, renewal, and OCSP stapling automatically.

Caddyfile
sophon.example.com {
    # Dashboard (static SPA)
    reverse_proxy localhost:8080

    # Gateway API + SignalR WebSocket
    @gateway path /api/* /hubs/*
    handle @gateway {
        reverse_proxy localhost:8081
    }
}

Run Caddy:

# Install
sudo apt install caddy   # Debian/Ubuntu
brew install caddy        # macOS

# Start (auto-obtains SSL certificate)
caddy run --config Caddyfile

Caddy automatically handles WebSocket upgrades — no extra headers needed.

Nginx

/etc/nginx/sites-available/sophon
server {
    listen 443 ssl http2;
    server_name sophon.example.com;

    ssl_certificate     /etc/ssl/certs/sophon.crt;
    ssl_certificate_key /etc/ssl/private/sophon.key;

    # Dashboard
    location / {
        proxy_pass http://localhost:8080;
        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;
    }

    # Gateway API
    location /api/ {
        proxy_pass http://localhost:8081;
        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;
    }

    # SignalR WebSocket — requires upgrade headers
    location /hubs/ {
        proxy_pass http://localhost:8081;
        proxy_http_version 1.1;
        proxy_set_header Upgrade $http_upgrade;
        proxy_set_header Connection "upgrade";
        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;
        proxy_read_timeout 86400;
    }
}

# HTTP redirect
server {
    listen 80;
    server_name sophon.example.com;
    return 301 https://$host$request_uri;
}

Key details:

  • The /hubs/ location must include proxy_http_version 1.1 and Upgrade/Connection headers for SignalR WebSocket transport
  • proxy_read_timeout 86400 keeps long-lived WebSocket connections alive (24 hours)
  • For Let's Encrypt with Nginx, use Certbot

Enable the site:

sudo ln -s /etc/nginx/sites-available/sophon /etc/nginx/sites-enabled/
sudo nginx -t && sudo systemctl reload nginx

Traefik

If you're already running Traefik as your Docker ingress, add labels to the gateway and dashboard services in your compose file:

docker-compose.override.yml
services:
  sophon-gateway:
    labels:
      - "traefik.enable=true"
      - "traefik.http.routers.sophon-api.rule=Host(`sophon.example.com`) && (PathPrefix(`/api`) || PathPrefix(`/hubs`))"
      - "traefik.http.routers.sophon-api.tls.certresolver=letsencrypt"
      - "traefik.http.services.sophon-api.loadbalancer.server.port=8080"

  sophon-dashboard:
    labels:
      - "traefik.enable=true"
      - "traefik.http.routers.sophon-dashboard.rule=Host(`sophon.example.com`) && !PathPrefix(`/api`) && !PathPrefix(`/hubs`)"
      - "traefik.http.routers.sophon-dashboard.tls.certresolver=letsencrypt"
      - "traefik.http.services.sophon-dashboard.loadbalancer.server.port=80"

Traefik handles WebSocket upgrades automatically when TLS is enabled.

Ensure your Traefik static configuration includes a certificate resolver:

traefik.yml
certificatesResolvers:
  letsencrypt:
    acme:
      email: admin@example.com
      storage: /letsencrypt/acme.json
      httpChallenge:
        entryPoint: web