Deploying Caddy + 9Router with Docker Compose
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
80and443open 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
exposerather thanportsconfines port20128to 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.sqlitesurvives 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.