4 min read

Deploying Caddy + 9Router with Docker Compose

#Docker#Caddy#DevOps#Self-Hosting

I wanted a self-hosted, centralized AI routing proxy accessible from anywhere—not just localhost. My choice landed on 9Router, a unified gateway that routes LLM requests from Claude Code, Codex, Cursor, Cline, and Copilot across 40+ AI model providers with automatic failover.

The architecture goal is simple: run 9Router in a container, put it behind Caddy for automated TLS certificates, and manage the entire stack cleanly with Docker Compose. Since 9Router includes native authentication for its web dashboard and API token enforcement, we don't need additional basic auth wrappers in Caddy.

Prerequisites

Before starting, ensure you have:

  • A Linux server (Ubuntu/Debian recommended) with SSH access.
  • A custom domain pointed to your server IP via an DNS A record (e.g., 9router.yourdomain.com).
  • Inbound ports 80 and 443 open in your firewall for Let's Encrypt TLS challenges.

Connecting to Your Server

Log in via SSH:

ssh user@your-server-ip

If Docker isn't installed yet, use the official convenience script, which sets up both Docker Engine and the Compose plugin:

curl -fsSL https://get.docker.com | sh

Verify the installation:

docker --version
docker compose version

Directory Structure

Keep all service assets cleanly grouped inside a dedicated directory:

mkdir -p ~/9router && cd ~/9router

Inside this directory, we will maintain three files: docker-compose.yml, Caddyfile, and .env.

Configuring docker-compose.yml

Our Compose configuration defines two services: 9router for the application logic and caddy as the reverse proxy managing HTTPS:

services:
  9router:
    image: decolua/9router:latest
    container_name: 9router
    restart: unless-stopped
    environment:
      - DATA_DIR=/app/data
      - NODE_ENV=production
      # Enforce secure cookies behind Caddy's HTTPS reverse proxy
      - AUTH_COOKIE_SECURE=true
      # Initial administrator password for the dashboard
      - INITIAL_PASSWORD=${INITIAL_PASSWORD}
      # Require a Bearer API token on /v1 routes since the endpoint is public
      - REQUIRE_API_KEY=true
    volumes:
      - ./data:/app/data
    # Expose port only to the internal Docker network; do NOT bind to host ports
    expose:
      - "20128"

  caddy:
    image: caddy:2-alpine
    container_name: caddy
    restart: unless-stopped
    ports:
      - "80:80"
      - "443:443"
    volumes:
      - ./Caddyfile:/etc/caddy/Caddyfile:ro
      - caddy_data:/data
      - caddy_config:/config
    depends_on:
      - 9router

volumes:
  caddy_data:
    name: 9router_caddy_data
  caddy_config:
    name: 9router_caddy_config

Key design decisions in this setup:

  • No exposed host ports on 9Router: Using expose rather than ports confines port 20128 to the Docker bridge network. The only ingress into 9Router is through Caddy.
  • AUTH_COOKIE_SECURE=true: Required because the dashboard is served over HTTPS. Without this, session cookies can misbehave behind reverse proxies.
  • REQUIRE_API_KEY=true: Mandates Bearer authentication for all /v1/* inference endpoints, preventing unauthorized third-party usage.
  • Persistent SQLite storage in ./data/db/data.sqlite survives container upgrades and restarts.

Minimal Caddy Configuration

Caddy simplifies reverse proxying and automated TLS management down to just three lines:

9router.yourdomain.com {
    reverse_proxy 9router:20128
}

Caddy automatically provisions TLS certificates via Let's Encrypt, handles renewals, and manages HTTP-to-HTTPS redirects out of the box.

Storing Secrets in .env

Store your credentials in a .env file alongside docker-compose.yml:

# .env
INITIAL_PASSWORD=your-secure-strong-password

Docker Compose automatically interpolates variables from .env into ${INITIAL_PASSWORD}. Choose a strong, unique password to safeguard your dashboard.

Launching the Stack

Start both containers in detached mode:

docker compose up -d

Verify that both containers are running healthy:

docker compose ps

Monitor Caddy's logs to verify automated SSL issuance:

docker compose logs -f caddy

Once Let's Encrypt issues the certificate, navigate to https://9router.yourdomain.com in your browser. Log in with the initial password configured in your .env file.

Connecting Developer Tools

Inside the 9Router dashboard, navigate to Providers to configure upstream AI keys. Generate an API token under the API Keys section, then point your developer tools (Claude Code, Cline, Cursor) to your self-hosted endpoint:

Endpoint: https://9router.yourdomain.com/v1
API Key: [copied from dashboard]
Model: kr/claude-sonnet-4.5

Because REQUIRE_API_KEY=true is enabled, any request without a valid Bearer token is immediately rejected at the proxy layer.

Daily Maintenance Commands

Common operations for maintenance:

# Pull the latest container images and recreate
docker compose pull && docker compose up -d

# Restart services
docker compose restart

# Stop the stack
docker compose down

Wrapping Up

With Caddy handling automated HTTPS and 9Router managing authentication internally, we get a private, production-grade AI proxy that is accessible securely anywhere. Docker Compose keeps the deployment fully reproducible and modular across servers.