SoxAIDocs
Self-Hosted

Domain, HTTPS & Reverse Proxy

Point a custom domain at your SoxAI server — choose between Cloudflare CDN proxy or a direct acme.sh + nginx setup

Domain, HTTPS & Reverse Proxy

After the one-click install, SoxAI listens on plain HTTP on your server's IP address. This page shows two independent paths to add a real domain with HTTPS:

Option A — CloudflareOption B — acme.sh + nginx
CDN / DDoS protectionYes (orange-cloud domains)No (direct connection)
CertificatesCloudflare Origin Cert (orange-cloud) + Let's Encrypt (gateway)Let's Encrypt via acme.sh for all domains
Reverse proxynginx (routes Cloudflare → Docker)nginx (terminates TLS, routes to Docker)
Streaming timeoutGateway must be grey-cloud; console/API are fine behind CDNNo limit — nginx timeout configurable

Both options require nginx on your server to route traffic to the Docker containers. The difference is where TLS terminates and whether Cloudflare sits in front.


Before you start

You need:

  • A domain name (e.g. yourdomain.com) registered anywhere
  • Three sub-domains planned:
Sub-domainSoxAI servicePort
console.yourdomain.comAdmin UI + user dashboard3000
api.yourdomain.comAPI server (management endpoints)8080
gateway.yourdomain.comAI gateway — receives model traffic8081

Option A — Cloudflare CDN + nginx

Use this option if you want Cloudflare DDoS protection and CDN for the console and API. The gateway sub-domain bypasses Cloudflare (grey-cloud) to avoid the CDN proxy timeout on streaming AI responses.

Architecture:

Client → Cloudflare CDN → nginx → Docker (console :3000 / api :8080)
Client ──────────────────→ nginx → Docker (gateway :8081)

Step 1 — Install nginx

# Debian / Ubuntu
apt update && apt install -y nginx curl

# RHEL / Rocky / AlmaLinux
dnf install -y nginx curl
systemctl enable --now nginx

Step 2 — Issue a certificate for the gateway

The gateway sub-domain bypasses Cloudflare (grey-cloud), so clients connect directly to your server and expect a publicly trusted certificate. Issue one with acme.sh:

# Install acme.sh
curl https://get.acme.sh | sh -s [email protected]
source ~/.bashrc   # or open a new terminal if you use zsh

# Bootstrap nginx so the HTTP-01 challenge can complete
mkdir -p /var/www/acme
cat > /etc/nginx/conf.d/acme.conf <<'EOF'
server {
    listen 80;
    server_name gateway.yourdomain.com;
    location /.well-known/acme-challenge/ { root /var/www/acme; }
    location / { return 444; }
}
EOF
nginx -s reload

# Issue the certificate
~/.acme.sh/acme.sh --issue \
  -d gateway.yourdomain.com \
  --webroot /var/www/acme

# Install it so nginx auto-reloads on renewal
mkdir -p /etc/nginx/ssl
~/.acme.sh/acme.sh --install-cert -d gateway.yourdomain.com \
  --fullchain-file /etc/nginx/ssl/gateway-fullchain.crt \
  --key-file       /etc/nginx/ssl/gateway.key \
  --reloadcmd      "nginx -s reload"

# Verify renewal cron is installed
crontab -l | grep acme
# Expected: 0 0 * * * "/root/.acme.sh"/acme.sh --cron ...

# Test the renewal pipeline end-to-end (dry-run, no real renewal)
~/.acme.sh/acme.sh --renew -d gateway.yourdomain.com --force --dry-run

Alternative — DNS-01 challenge: If port 80 is not reachable, use the Cloudflare DNS API instead:

export CF_Token="your-cloudflare-api-token"
export CF_Zone_ID="your-zone-id"
~/.acme.sh/acme.sh --issue --dns dns_cf -d gateway.yourdomain.com

See acme.sh DNS API docs for other providers.

Step 3 — Create a Cloudflare Origin Certificate

For the orange-cloud sub-domains (console and api), use a Cloudflare Origin Certificate. This is a free certificate issued by Cloudflare's CA and trusted by Cloudflare's CDN edge — no Let's Encrypt needed for these two domains.

  1. In Cloudflare dashboard go to SSL/TLS → Origin Server → Create Certificate
  2. Leave the defaults (RSA 2048, 15-year validity, covers *.yourdomain.com and yourdomain.com)
  3. Copy the certificate and key into files on your server:
# Paste the certificate content when prompted
cat > /etc/nginx/ssl/cf-origin.crt   # paste Cloudflare cert here, Ctrl-D
cat > /etc/nginx/ssl/cf-origin.key   # paste Cloudflare key here, Ctrl-D
chmod 600 /etc/nginx/ssl/cf-origin.key

Step 4 — Configure nginx

Remove the temporary acme config and create the full nginx configuration:

rm /etc/nginx/conf.d/acme.conf

Create /etc/nginx/conf.d/soxai.conf:

map $http_upgrade $connection_upgrade {
    default upgrade;
    ''      close;
}

# HTTP → HTTPS redirect for all sub-domains
server {
    listen 80;
    server_name console.yourdomain.com api.yourdomain.com gateway.yourdomain.com;
    location /.well-known/acme-challenge/ { root /var/www/acme; }
    location / { return 301 https://$host$request_uri; }
}

# Console (Next.js) — orange-cloud, Cloudflare Origin Cert
server {
    listen 443 ssl http2;
    server_name console.yourdomain.com;

    ssl_certificate     /etc/nginx/ssl/cf-origin.crt;
    ssl_certificate_key /etc/nginx/ssl/cf-origin.key;
    ssl_protocols       TLSv1.2 TLSv1.3;

    location / {
        proxy_pass         http://127.0.0.1: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;
        proxy_read_timeout 60s;
    }
}

# API server — orange-cloud, Cloudflare Origin Cert
server {
    listen 443 ssl http2;
    server_name api.yourdomain.com;

    ssl_certificate     /etc/nginx/ssl/cf-origin.crt;
    ssl_certificate_key /etc/nginx/ssl/cf-origin.key;
    ssl_protocols       TLSv1.2 TLSv1.3;

    client_max_body_size 50m;

    location / {
        proxy_pass         http://127.0.0.1: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;
        proxy_read_timeout 60s;
    }
}

# Gateway — grey-cloud (bypasses CDN), Let's Encrypt cert
# Streaming settings: buffering off, long timeout.
server {
    listen 443 ssl http2;
    server_name gateway.yourdomain.com;

    ssl_certificate     /etc/nginx/ssl/gateway-fullchain.crt;
    ssl_certificate_key /etc/nginx/ssl/gateway.key;
    ssl_protocols       TLSv1.2 TLSv1.3;

    client_max_body_size 50m;

    location / {
        proxy_pass            http://127.0.0.1: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;

        proxy_buffering       off;
        proxy_cache           off;
        proxy_read_timeout    600s;
        proxy_send_timeout    600s;
        proxy_connect_timeout 10s;

        proxy_http_version    1.1;
        proxy_set_header      Upgrade    $http_upgrade;
        proxy_set_header      Connection $connection_upgrade;
    }
}

Test and reload:

nginx -t && nginx -s reload

Step 5 — Configure Cloudflare DNS

In Cloudflare DNS → Records, create three A records pointing to your server's public IP:

NameTypeContentProxy status
consoleAYOUR_SERVER_IPProxied (orange cloud)
apiAYOUR_SERVER_IPProxied (orange cloud)
gatewayAYOUR_SERVER_IPDNS only (grey cloud)

Why grey-cloud the gateway? Cloudflare's CDN proxy has a 100-second proxy timeout on free, Pro, and Business plans. AI streaming responses (SSE) routinely exceed this — the request gets cut off mid-stream. Grey-cloud sends traffic directly to your server where nginx has a 600s timeout.

Cloudflare planProxy timeout
Free / Pro / Business100 seconds
Enterprise6000 seconds

Even with the gateway on grey-cloud, console and API still get CDN acceleration and DDoS protection.

Step 6 — Set Cloudflare SSL/TLS mode

Go to SSL/TLS → Overview and select Full (strict). This makes Cloudflare verify the Origin Certificate on your server before completing the connection.

Step 7 — Disable Cloudflare caching for API routes

Go to Rules → Cache Rules and add a rule:

  • Match: hostname contains api.yourdomain.com
  • Action: Bypass cache

This prevents Cloudflare from caching JSON API responses.

Step 8 — Update SoxAI and verify

cd /opt/soxai
# Update BASE_URL to your console domain
nano .env
BASE_URL=https://console.yourdomain.com
docker compose up -d

Verify all three endpoints:

curl -I https://console.yourdomain.com   # should return 200
curl -I https://api.yourdomain.com/healthz
curl -I https://gateway.yourdomain.com/healthz

Option B — acme.sh + nginx (no Cloudflare CDN)

Use this option if you are not using a CDN proxy — your DNS records point directly to your server (any DNS provider, records without CDN proxy). TLS terminates on your server with Let's Encrypt certificates.

Architecture:

Client → nginx (TLS termination) → Docker (console :3000 / api :8080 / gateway :8081)

Step 1 — Install nginx

# Debian / Ubuntu
apt update && apt install -y nginx curl

# RHEL / Rocky / AlmaLinux
dnf install -y nginx curl
systemctl enable --now nginx

Step 2 — Point DNS to your server

At your DNS provider, create three A records pointing to your server's public IP:

NameTypeContent
consoleAYOUR_SERVER_IP
apiAYOUR_SERVER_IP
gatewayAYOUR_SERVER_IP

Wait a few minutes for DNS to propagate before proceeding.

Step 3 — Install acme.sh

curl https://get.acme.sh | sh -s [email protected]
source ~/.bashrc   # or open a new terminal if you use zsh

acme.sh installs to ~/.acme.sh/ and automatically adds a cron job for certificate renewal.

Step 4 — Issue certificates

HTTP-01 challenge (recommended — no API key required)

Port 80 must be reachable from the internet.

mkdir -p /var/www/acme

# Temporary nginx config to serve the ACME challenge
cat > /etc/nginx/conf.d/acme.conf <<'EOF'
server {
    listen 80;
    server_name console.yourdomain.com api.yourdomain.com gateway.yourdomain.com;
    location /.well-known/acme-challenge/ { root /var/www/acme; }
    location / { return 444; }
}
EOF
nginx -s reload

# Issue a single certificate covering all three sub-domains
~/.acme.sh/acme.sh --issue \
  -d console.yourdomain.com \
  -d api.yourdomain.com \
  -d gateway.yourdomain.com \
  --webroot /var/www/acme

DNS-01 challenge (use when port 80 is blocked, or for a wildcard cert)

Example with Cloudflare DNS API:

export CF_Token="your-cloudflare-api-token"
export CF_Zone_ID="your-zone-id"

~/.acme.sh/acme.sh --issue \
  --dns dns_cf \
  -d "*.yourdomain.com" \
  -d yourdomain.com

See acme.sh DNS API docs for other providers (Route53, Aliyun, DNSPod, Namecheap, etc.).

Step 5 — Install certificates to nginx

mkdir -p /etc/nginx/ssl

# If you used HTTP-01 (three separate -d flags):
~/.acme.sh/acme.sh --install-cert \
  -d console.yourdomain.com \
  --fullchain-file /etc/nginx/ssl/soxai-fullchain.crt \
  --key-file       /etc/nginx/ssl/soxai.key \
  --reloadcmd      "nginx -s reload"

# If you used DNS-01 with a wildcard cert (*.yourdomain.com), the -d must
# match the primary domain used at --issue time:
# ~/.acme.sh/acme.sh --install-cert \
#   -d "*.yourdomain.com" \
#   --fullchain-file /etc/nginx/ssl/soxai-fullchain.crt \
#   --key-file       /etc/nginx/ssl/soxai.key \
#   --reloadcmd      "nginx -s reload"

The --reloadcmd flag tells acme.sh to reload nginx automatically after every renewal, so the new certificate takes effect without manual intervention.

Step 6 — Configure nginx

Remove the temporary acme config and create the full configuration:

rm /etc/nginx/conf.d/acme.conf

Create /etc/nginx/conf.d/soxai.conf:

map $http_upgrade $connection_upgrade {
    default upgrade;
    ''      close;
}

# HTTP → HTTPS redirect
server {
    listen 80;
    server_name console.yourdomain.com api.yourdomain.com gateway.yourdomain.com;
    location /.well-known/acme-challenge/ { root /var/www/acme; }
    location / { return 301 https://$host$request_uri; }
}

# Console (Next.js)
server {
    listen 443 ssl http2;
    server_name console.yourdomain.com;

    ssl_certificate     /etc/nginx/ssl/soxai-fullchain.crt;
    ssl_certificate_key /etc/nginx/ssl/soxai.key;
    ssl_protocols       TLSv1.2 TLSv1.3;
    ssl_ciphers         HIGH:!aNULL:!MD5;

    location / {
        proxy_pass         http://127.0.0.1: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;
        proxy_read_timeout 60s;
    }
}

# API server
server {
    listen 443 ssl http2;
    server_name api.yourdomain.com;

    ssl_certificate     /etc/nginx/ssl/soxai-fullchain.crt;
    ssl_certificate_key /etc/nginx/ssl/soxai.key;
    ssl_protocols       TLSv1.2 TLSv1.3;
    ssl_ciphers         HIGH:!aNULL:!MD5;

    client_max_body_size 50m;

    location / {
        proxy_pass         http://127.0.0.1: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;
        proxy_read_timeout 60s;
    }
}

# Gateway — AI streaming endpoint
server {
    listen 443 ssl http2;
    server_name gateway.yourdomain.com;

    ssl_certificate     /etc/nginx/ssl/soxai-fullchain.crt;
    ssl_certificate_key /etc/nginx/ssl/soxai.key;
    ssl_protocols       TLSv1.2 TLSv1.3;
    ssl_ciphers         HIGH:!aNULL:!MD5;

    client_max_body_size 50m;

    location / {
        proxy_pass            http://127.0.0.1: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;

        # Streaming (SSE) — tokens reach the client immediately, no buffering
        proxy_buffering       off;
        proxy_cache           off;

        # Long-running AI responses can exceed minutes
        proxy_read_timeout    600s;
        proxy_send_timeout    600s;
        proxy_connect_timeout 10s;

        # WebSocket support
        proxy_http_version    1.1;
        proxy_set_header      Upgrade    $http_upgrade;
        proxy_set_header      Connection $connection_upgrade;
    }
}

Test and reload:

nginx -t && nginx -s reload

Step 7 — Certificate auto-renewal

acme.sh installs a cron job automatically on installation. Verify it:

crontab -l | grep acme

Expected output:

0 0 * * * "/root/.acme.sh"/acme.sh --cron --home "/root/.acme.sh" > /dev/null

Certificates renew automatically 30 days before expiry. Test a dry-run to confirm the renewal pipeline works end-to-end:

~/.acme.sh/acme.sh --renew -d console.yourdomain.com --force --dry-run

Step 8 — Update SoxAI and verify

cd /opt/soxai
nano .env
BASE_URL=https://console.yourdomain.com
docker compose up -d

Verify all three endpoints:

curl -I https://console.yourdomain.com
curl -I https://api.yourdomain.com/healthz
curl -I https://gateway.yourdomain.com/healthz

Troubleshooting

502 Bad Gateway — nginx can't reach the Docker container. Check containers are running:

cd /opt/soxai && docker compose ps

Streaming cuts off at exactly 100 seconds (Option A) — the CDN proxy timeout is firing. Confirm that gateway.yourdomain.com is set to DNS only (grey cloud), not proxied.

ERR_SSL_PROTOCOL_ERROR (Option A) — Cloudflare SSL/TLS mode is set to Flexible (origin receives plain HTTP). Change it to Full (strict).

Certificate not renewing (Option B) — ensure port 80 is reachable and run a forced renewal:

~/.acme.sh/acme.sh --renew -d console.yourdomain.com --force

SSL_ERROR_RX_RECORD_TOO_LONG — nginx is serving plain HTTP on port 443. Run nginx -t to check for config errors.