Skip to content

Reverse Proxy Setup

A reverse proxy terminates TLS and routes traffic to the ai.doo services. Caddy is recommended for its automatic HTTPS, but an nginx configuration is also provided.

Service Ports

Service Internal Port Suggested Public Path
Hub 2000 hub.example.com
PIKA 8000 pika.example.com
VERA (Next.js) 3000 vera.example.com
VERA (API) 4000 vera.example.com/api/*

Warning

Never expose Ollama (port 11434) to the public internet. It has no authentication. Only the Docker bridge network (ollama_network) should be able to reach it.

Caddy obtains and renews TLS certificates automatically via Let's Encrypt.

Caddyfile

{
    email admin@example.com
}

hub.example.com {
    reverse_proxy hub:2000

    header {
        Strict-Transport-Security "max-age=63072000; includeSubDomains; preload"
        X-Content-Type-Options "nosniff"
        X-Frame-Options "DENY"
        Content-Security-Policy "default-src 'self'; script-src 'self'; style-src 'self' 'unsafe-inline'"
        Referrer-Policy "strict-origin-when-cross-origin"
    }
}

pika.example.com {
    reverse_proxy pika:8000

    header {
        Strict-Transport-Security "max-age=63072000; includeSubDomains; preload"
        X-Content-Type-Options "nosniff"
        X-Frame-Options "DENY"
        Content-Security-Policy "default-src 'self'; script-src 'self'; style-src 'self' 'unsafe-inline'"
        Referrer-Policy "strict-origin-when-cross-origin"
    }
}

vera.example.com {
    handle /api/* {
        reverse_proxy vera-backend:4000
    }

    handle {
        reverse_proxy vera-frontend:3000
    }

    header {
        Strict-Transport-Security "max-age=63072000; includeSubDomains; preload"
        X-Content-Type-Options "nosniff"
        X-Frame-Options "SAMEORIGIN"
        Content-Security-Policy "default-src 'self'; script-src 'self' 'unsafe-eval'; style-src 'self' 'unsafe-inline'; img-src 'self' data: blob:"
        Referrer-Policy "strict-origin-when-cross-origin"
    }
}

Docker Compose Overlay

Create a docker-compose.caddy.yml alongside your main compose files:

services:
  caddy:
    image: caddy:2-alpine
    restart: unless-stopped
    ports:
      - "80:80"
      - "443:443"
    volumes:
      - ./Caddyfile:/etc/caddy/Caddyfile:ro
      - caddy_data:/data
      - caddy_config:/config
    networks:
      - ollama_network

networks:
  ollama_network:
    external: true
    name: ollama_network

volumes:
  caddy_data:
  caddy_config:

Start it with:

docker compose -f docker-compose.caddy.yml up -d

Note

Caddy must be on the same Docker network as the services it proxies. The ollama_network bridge is shared by all ai.doo services.

nginx

If you prefer nginx, here is an equivalent configuration.

upstream hub {
    server hub:2000;
}

upstream pika {
    server pika:8000;
}

upstream vera_api {
    server vera-backend:4000;
}

upstream vera_frontend {
    server vera-frontend:3000;
}

server {
    listen 443 ssl http2;
    server_name hub.example.com;

    ssl_certificate     /etc/letsencrypt/live/hub.example.com/fullchain.pem;
    ssl_certificate_key /etc/letsencrypt/live/hub.example.com/privkey.pem;

    add_header Strict-Transport-Security "max-age=63072000; includeSubDomains; preload" always;
    add_header X-Content-Type-Options "nosniff" always;
    add_header X-Frame-Options "DENY" always;

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

server {
    listen 443 ssl http2;
    server_name pika.example.com;

    ssl_certificate     /etc/letsencrypt/live/pika.example.com/fullchain.pem;
    ssl_certificate_key /etc/letsencrypt/live/pika.example.com/privkey.pem;

    add_header Strict-Transport-Security "max-age=63072000; includeSubDomains; preload" always;
    add_header X-Content-Type-Options "nosniff" always;
    add_header X-Frame-Options "DENY" always;

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

server {
    listen 443 ssl http2;
    server_name vera.example.com;

    ssl_certificate     /etc/letsencrypt/live/vera.example.com/fullchain.pem;
    ssl_certificate_key /etc/letsencrypt/live/vera.example.com/privkey.pem;

    add_header Strict-Transport-Security "max-age=63072000; includeSubDomains; preload" always;
    add_header X-Content-Type-Options "nosniff" always;
    add_header X-Frame-Options "SAMEORIGIN" always;

    location /api/ {
        proxy_pass http://vera_api;
        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;
    }

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

# Redirect HTTP → HTTPS
server {
    listen 80;
    server_name hub.example.com pika.example.com vera.example.com;
    return 301 https://$host$request_uri;
}

Tip

With nginx you must manage TLS certificates yourself. Consider certbot for automated Let's Encrypt renewals.

Security Headers

Both configurations above include these recommended headers:

Header Value Purpose
Strict-Transport-Security max-age=63072000; includeSubDomains; preload Enforce HTTPS for 2 years
X-Content-Type-Options nosniff Prevent MIME-type sniffing
X-Frame-Options DENY / SAMEORIGIN Prevent clickjacking
Content-Security-Policy App-specific Restrict script/style sources
Referrer-Policy strict-origin-when-cross-origin Limit referrer information

Note

VERA uses SAMEORIGIN for X-Frame-Options and a more permissive CSP because the Next.js frontend requires unsafe-eval in development mode. Tighten these in production if you build the frontend for production.