Lab 08 — Self-Hosting: Docker Compose, Caddy, Cloudflare Tunnel, GPU, TrueNAS — AI Infrastructure for Real Business Problems
Last updated 10/17/2026
Course › Hands-on track › Lab 08

Lab 08 — Self-Hosting: Docker Compose, Caddy, Cloudflare Tunnel, GPU, TrueNAS

自架部署 · Hands-on track: AI Infrastructure for Real Business Problems

Code: labs/08/ · Packages Labs 03–05 into one stack

Business Problem — 商業問題

The prototypes run on a laptop with uvicorn. The business needs them running 24/7 on the office TrueNAS box (or a Mac mini), reachable by staff at home and by the website's chat widget, without:

Requirements:

Architecture — 系統架構

flowchart LR
    U[Staff / website] -->|HTTPS| CF[Cloudflare edge<br/>TLS + Access policy]
    CF -->|outbound-only tunnel| CFD[cloudflared]
    subgraph Host["TrueNAS / Linux / Mac — no inbound ports"]
        subgraph edge[edge network]
            CFD --> CAD[Caddy :80<br/>/rag /agent /listings /healthz]
        end
        subgraph backend[backend network]
            CAD --> RAG[rag-api :8004]
            CAD --> AG[agent-api :8005]
            CAD --> LS[listing-api :8003]
            RAG --> QD[(Qdrant + snapshots)]
            RAG & AG & LS --> OL[Ollama<br/>GPU optional]
            OW[Open WebUI<br/>own hostname]
        end
    end
    CFD -.->|chat.example.com| OW
    DS[(DATA_ROOT on a ZFS dataset<br/>periodic snapshots + replication)] --- QD & OL & AG & LS

Key decisions:

Prerequisites — 先決條件

Step-by-Step Setup — 逐步設定

1. Configure.

cd hands-on/labs/08
cp .env.example .env
# DATA_ROOT=./volumes            (TrueNAS: /mnt/tank/apps/kk-ai — create the datasets first)
# LLM_BASE_URL=http://ollama:11434/v1   (Mac with native Ollama: http://host.docker.internal:11434/v1)

2. Start the stack.

docker compose --profile local-ollama up -d --build      # NAS/Linux
# NVIDIA:  docker compose -f docker-compose.yml -f docker-compose.gpu.yml --profile local-ollama up -d --build
# AMD:     docker compose -f docker-compose.yml -f docker-compose.rocm.yml --profile local-ollama up -d --build
bash scripts/pull-models.sh                              # pulls LLM_MODEL + LLM_EMBED_MODEL into the container
docker compose ps                                        # wait for "healthy"
bash scripts/smoke.sh http://127.0.0.1:8080              # health of every route + one real RAG question

3. Publish with a Cloudflare Tunnel (token mode).

  1. In Cloudflare Zero Trust, go to Networks → Tunnels → Create tunnel (cloudflared), and copy the token into TUNNEL_TOKEN in .env.
  2. Add public hostnames:
    • ai.example.com → http://caddy:80
    • chat.example.com → http://open-webui:8080 (if using the ui profile)
  3. Under Access → Applications, add a self-hosted app for each hostname, with a policy (e.g. emails ending in your domain). For the website widget or n8n, create a service token and send CF-Access-Client-Id/Secret headers.
  4. Start the tunnel and test from outside the LAN:
docker compose --profile tunnel up -d
CF_ACCESS_CLIENT_ID=... CF_ACCESS_CLIENT_SECRET=... bash scripts/smoke.sh https://ai.example.com

Prefer config-as-code? Use cloudflared/config.example.yml (a locally managed tunnel) and validate it with cloudflared tunnel --config cloudflared/config.example.yml ingress validate.

4. Back up and restore once.

5. Validate config without starting anything (what CI and the build box run):

DOCKER_COMPOSE="docker compose" bash tests/validate.sh

How to Test — 測試方法

tests/validate.sh checks:

After deployment, scripts/smoke.sh is the runtime test.

What was run on the build box — 實測紀錄

Cost & Performance Notes — 成本與效能

Common Failures & Fixes — 常見錯誤與修正

Symptom Cause Fix
could not select device driver "nvidia" toolkit missing install nvidia-container-toolkit, restart Docker; on TrueNAS enable NVIDIA driver in Apps settings
Ollama in Docker on Mac is slow Docker on macOS has no Metal GPU run Ollama natively; set LLM_BASE_URL=http://host.docker.internal:11434/v1, skip local-ollama profile
Tunnel up but 502 wrong service URL in the dashboard must be http://caddy:80 (container name, same network as cloudflared)
Open WebUI broken under /chat no sub-path support give it its own hostname
Permission denied on DATA_ROOT dataset ACLs vs container user set dataset permissions for the app user (UID shown by docker compose exec <svc> id)
TUNNEL_TOKEN empty → cloudflared restarts in a loop token not set set it, or don't enable the tunnel profile
Disk fills up container logs already capped: json-file max-size: 10m, max-file: 3

Stretch Exercise — 延伸練習

  1. Add Uptime Kuma (or Cloudflare health checks) that hits /healthz and each /…/health every minute and alerts on failure.
  2. Move Lab 06 (n8n + ai-services) and Lab 07 (copilot) into this stack, with n8n on its own Access-protected hostname. Lab 10 does this; try it first yourself.

Deliverable Checklist — 交付清單

↑