Docker Compose 生產部署
本頁深入講解 Aivory 的生產 compose 檔案 deploy/docker-compose.prod.yml:每個服務的職責與健康檢查、資料卷與備份策略、預構建映象與本地構建的切換、資源規劃、日常運維命令、升級流程,以及常見故障的排查表。如果你只想儘快把服務拉起來,請先看快速部署,本頁假設你已經完成了首次部署。
架構總覽
整套棧由 5 個服務組成,全部掛在一個私有 bridge 網路 internal 上。只有 app 釋出宿主埠(預設 80:8787),其餘服務(包括 Qdrant 和沙箱)不對外暴露任何埠,只能在私有網路內被訪問。
| 服務 | 映象 | 宿主埠 | 職責 |
|---|---|---|---|
app | ghcr.io/<IMAGE_OWNER>/aivory-app | 80(可改) | 單容器同源伺服構建後的 SPA 與 /api 後端 |
postgres | postgres:16-alpine | 無 | 關係型儲存:使用者、對話、知識庫、用量等 |
redis | redis:7-alpine | 無 | 快取、限流計數器、跨程序停止流式輸出的 pub/sub |
qdrant | qdrant/qdrant:v1.12.4 | 無 | RAG 向量檢索 |
sandbox | ghcr.io/<IMAGE_OWNER>/aivory-sandbox-sidecar | 無 | 內建程式碼執行沙箱控制面,僅內網可達 |
app 容器內的 Go 程序同時伺服 SPA 靜態檔案和 /api,前後端天然同源。因此不存在跨域問題,也不需要配置 PUBLIC_ORIGIN、ALLOWED_ORIGINS 或任何域名相關變數:代理把請求轉發到容器,哪個域名進來哪個域名就能用,多域名同時指向也沒問題。公網部署時只需在前面放一層 TLS 終止(見反向代理與 HTTPS)。
服務詳解
app:應用主容器
- 容器內監聽
8787(AIVORY_LISTEN: ":8787"),同時伺服 SPA 與/api。SPA 目錄在構建映象時已內建(STATIC_DIR指向映象內的構建產物),無需單獨的 nginx/web 層。 - 埠對映寫死在 compose 檔案裡:
"80:8787"。宿主 80 被佔用時,直接改左邊的數字(例如"8080:8787"),不需要任何環境變數。 - 啟動依賴:
postgres和redis必須通過健康檢查(condition: service_healthy),qdrant只要求已啟動(condition: service_started,因為該服務沒有定義 healthcheck)。 app不依賴sandbox:沙箱是逐請求的軟依賴,只有程式碼執行功能用到它。如果把它做成啟動依賴,沙箱映象拉不下來或不健康時會拖死整個應用,所以 compose 有意不宣告這條依賴。- 健康檢查由映象自帶(
Dockerfile.app中定義):每 15s 用wget探測http://127.0.0.1:8787/api/health,超時 3s,啟動寬限 20s,連續失敗 5 次判定 unhealthy。 - 資料目錄:
${DATA_DIR:-./data}繫結掛載到容器內/app/data,存放上傳檔案、生成產物、備份歸檔(以及 SQLite 檔案,如果用到)。繫結掛載到宿主目錄而不是 Docker 卷,是為了讓這些檔案在宿主機上直接可見、可備份。
compose 為 app 注入的關鍵環境變數(完整清單見核心環境變數):
| 變數 | compose 中的值 | 說明 |
|---|---|---|
AIVORY_ENV | production | 觸發部署級安全校驗(如 JWT_SECRET 強制) |
DATABASE_URL | postgres://<user>:<password>@postgres:5432/<db>?sslmode=disable | 由 .env 中的 Postgres 變數拼接而成 |
REDIS_URL | redis://:<REDIS_PASSWORD>@redis:6379/0 | 啟用 Redis 快取、佇列與流恢復 |
QDRANT_URL | 預設 http://qdrant:6333 | 指向棧內 Qdrant;可覆蓋為外部叢集 |
QDRANT_API_KEY | 預設 aivory-internal-qdrant | 必須與 qdrant 服務的 key 一致(見下) |
JWT_SECRET | 必須在 .env 設定 | 缺失時 compose 直接報錯拒絕啟動 |
SANDBOX_BASE_URL | http://sandbox:8000 | 走私有網路訪問內建沙箱 |
SANDBOX_API_KEY | 預設 aivory-bundled-sandbox | 與沙箱服務共享的內部預設值 |
ENABLE_MOCK_PROVIDER | 預設 false | 置 true 可啟用內建演示模型,無需真實 API key |
Anthropic / OpenAI 等 provider 的 API key 不通過環境變數配置,它們儲存在資料庫的 channels 表中,部署完成後在管理後臺的渠道與模型頁面新增。
postgres:關係型資料庫
- 映象
postgres:16-alpine,資料落在命名卷pgdata(容器內/var/lib/postgresql/data)。 POSTGRES_PASSWORD是必填項:compose 使用${POSTGRES_PASSWORD:?...}語法,不設定直接報錯。使用者名稱和庫名預設都是aivory。- 健康檢查:每 10s 執行一次
pg_isready,超時 5s,最多重試 10 次。app會等它變為 healthy 後才啟動。 - 資料庫 schema 在
app啟動時自動建立和遷移,不需要手動執行任何 SQL。
POSTGRES_PASSWORD 只在 pgdata 卷為空、資料庫首次初始化時生效。之後修改 .env 裡的這個值不會更新資料庫內的實際密碼,只會導致 app 用新密碼連線舊庫而認證失敗。改密碼要麼在資料庫內用 ALTER USER 執行,要麼(僅限可丟棄資料的環境)刪掉 pgdata 卷重新初始化。
redis:快取與訊息
- 映象
redis:7-alpine,以--appendonly yes --requirepass <REDIS_PASSWORD>啟動:開啟 AOF 持久化並強制密碼認證。REDIS_PASSWORD同樣是.env必填項。 - 承擔快取、限流計數器,以及跨程序"停止生成"訊號的 pub/sub;配置 Redis 後,應用還會啟用 Redis 佇列與流恢復。
- 資料落在命名卷
redisdata(容器內/data)。 - 健康檢查:每 10s 執行
redis-cli -a "$REDIS_PASSWORD" ping並檢查返回PONG,超時 5s,重試 10 次。
qdrant:向量資料庫
- 映象
qdrant/qdrant:v1.12.4,資料落在命名卷qdrantdata(容器內/qdrant/storage)。 - Qdrant 對每個請求都要求 API key(
QDRANT__SERVICE__API_KEY)。棧內qdrant與app讀取同一個.env變數QDRANT_API_KEY,雙方共享預設值aivory-internal-qdrant,所以零配置也能工作。由於 Qdrant 不釋出任何宿主埠、只在私有網路內可達,這個共享預設值是可接受的;仍建議在.env裡覆蓋為強隨機值。 - 該服務沒有定義 healthcheck,所以
app對它只等service_started。Qdrant 短暫不可用不會導致功能中斷:向量檢索失敗時,RAG 會回退為注入全文上下文。 - Collection 按 embedding 維度命名為
aivory_c<維度>(例如 1536 維模型對應aivory_c1536)。
sandbox:程式碼執行沙箱控制面
沙箱隨棧內建,一條 docker compose up 就帶起來,無需單獨專案或額外配置金鑰。它的完整安全模型與調參說明見程式碼沙箱部署,這裡只講 compose 層面的行為:
- 該服務是一個 sidecar 控制面:通過掛載的宿主
/var/run/docker.sock在宿主 Docker 守護程序上派生每會話一個的受限容器,會話執行時映象為ghcr.io/<IMAGE_OWNER>/aivory-sandbox,並在啟動時預拉取(SANDBOX_PULL_ON_START: "1"),保證首次呼叫即可用。 - 不釋出任何宿主埠:只有
app通過私有網路的http://sandbox:8000訪問它。掛載docker.sock等價於宿主 root 許可權,"不對外暴露埠"正是控制這一風險面的關鍵,請勿給它加ports對映。 - 控制面容器自身有資源上限:
mem_limit: 1g、pids_limit: 512。會話容器的資源由環境變數控制:
| 變數 | 預設值 | 含義 |
|---|---|---|
SANDBOX_NETWORK | none | 會話容器無網路;僅當沙箱內程式碼需要聯網時改為 bridge |
SANDBOX_MEMORY | 2g | 單個會話容器記憶體上限 |
SANDBOX_CPUS | 1 | 單個會話容器 CPU 配額 |
SANDBOX_MAX_SESSIONS | 16 | 併發會話容器數上限 |
SANDBOX_EXEC_TIMEOUT_CAP_MS | 600000 | 單次執行時長硬上限(10 分鐘),管理後臺的超時設定會被鉗制到此值以內 |
SANDBOX_IDLE_TTL_CAP_SECONDS | 86400 | 空閒回收視窗硬上限(24 小時),同樣鉗制管理後臺設定 |
SANDBOX_WORKSPACE_SIZE | 512m | 會話工作區容量 |
SANDBOX_READ_ONLY_ROOTFS | 1 | 會話容器根檔案系統只讀 |
- 工作區持久化:會話被回收時,
/workspace會打包歸檔到命名卷sandbox-archives(按對話 ID 鍵控),同一對話再次執行程式碼時自動恢復。這套本地歸檔零配置可用,但僅限單機;多副本部署必須改用 S3/OSS 類物件儲存。 - 健康檢查:每 30s 用 Python
urllib探測容器內http://localhost:8000/healthz(映象裡除 Python 外沒有其它探測工具),超時 10s,重試 3 次,start_period為 120s。這個寬限期覆蓋的是冷啟動時拉取會話執行時映象的耗時,拉取完成前服務不開始受理請求。
資料持久化與備份
資料都在哪裡
| 卷 / 掛載 | 容器路徑 | 內容 | 丟失後果 |
|---|---|---|---|
pgdata(命名卷) | /var/lib/postgresql/data | 全部關係型資料:使用者、對話、知識庫、配置 | 災難性,必須備份 |
qdrantdata(命名卷) | /qdrant/storage | RAG 向量 | 可從資料庫中的 chunk 文本重建,但要重新消耗嵌入 API 呼叫 |
redisdata(命名卷) | /data | 快取、限流計數器(AOF) | 影響很小,重啟後自然重建 |
sandbox-archives(命名卷) | /var/lib/aivory/sandbox-archives | 各對話的沙箱工作區歸檔 | 對應對話的沙箱檔案丟失,不影響對話本身 |
DATA_DIR(宿主目錄繫結掛載,預設 ./data) | /app/data | 上傳檔案、生成產物、備份歸檔(backups/ 子目錄) | 使用者上傳與產物丟失,必須備份 |
-v 會連同命名卷一起刪除,pgdata、qdrantdata 等全部資料當場清空且不可恢復。日常停止服務用 docker compose -f docker-compose.prod.yml down(不帶 -v)或 stop。
備份策略
推薦兩條路線並行:
- 應用級備份(首選):管理後臺的 Backup & Migration 頁面可非同步生成全量遷移 ZIP,內含引擎中立的資料庫邏輯備份(每表一個 JSONL)、可選的 uploads/artifacts 檔案,以及可選的 Qdrant 向量資料。生成的歸檔存放在
BACKUP_DIR(容器內預設/app/data/backups,宿主機上對應DATA_DIR/backups),把它拷走異地儲存即可。這種備份可以跨引擎恢復(比如恢復到 SQLite 部署),詳見備份與遷移。 - 基礎設施級冷備:停止整個棧後,把所有命名卷和
DATA_DIR目錄一起快照/複製。必須一起備份,才能保證資料庫行、向量和磁碟檔案三者一致。
備份匯入的大小上限由 MAX_BACKUP_BYTES 控制,預設 20 GiB;含向量的全量歸檔可能很大,必要時上調。
預構建映象與本地構建
app 和 sandbox 兩個服務同時聲明瞭 image: 和 build:。Compose 的行為是:映象存在(或可拉取)時優先用映象,加 --build 引數時本地構建。同一份 compose 檔案因此服務於兩種流程,拓撲不用寫兩遍。
方式一:拉取預構建映象(推薦用於生產)
cd deploy
docker compose -f docker-compose.prod.yml pull
docker compose -f docker-compose.prod.yml up -d
方式二:從原始碼本地構建(用於二次開發或官方映象未覆蓋的架構)
cd deploy
docker compose -f docker-compose.prod.yml up -d --build
映象來源由 .env 中兩個變數控制:
| 變數 | 預設值 | 說明 |
|---|---|---|
IMAGE_REGISTRY | ghcr.io | 映象倉庫地址;GitHub 訪問受限時可整體切換到 ghcr 映象代理或自建/私有倉庫,見下方「受限網路部署」 |
IMAGE_OWNER | hjxwz123 | ghcr.io 名稱空間;fork 到自己賬號後改成自己的使用者名稱 |
IMAGE_TAG | latest | 映象標籤;生產環境建議固定到具體版本標籤,升級和回滾都更可控 |
本地構建時,app 的構建上下文是倉庫根目錄:Dockerfile.app 是一個三階段構建,先用 Node 20 構建 Vite SPA(在構建機原生架構上執行,產物是架構無關的靜態檔案),再用 Go 1.24 編譯 API 二進位制(需要 CGO,因為二進位制內嵌了 SQLite 驅動作為開發/回退後端),最後打進 debian:bookworm-slim 執行時映象。
受限網路部署(無法訪問 GitHub)
伺服器連不上 GitHub / ghcr.io 時,部署不受阻——按下面三步走。
第一步:拿到部署檔案(不需要 git clone)
部署只需要兩個檔案:compose 檔案和 .env。compose 檔案可直接從本文件站下載(文件站部署在 Cloudflare,不依賴 GitHub):
mkdir -p aivory/deploy && cd aivory/deploy
curl -LO https://aivory-docs.pages.dev/deploy/docker-compose.prod.yml
# 然後在同目錄建立 .env(至少包含 POSTGRES_PASSWORD、REDIS_PASSWORD、JWT_SECRET)
第二步:解決 ghcr.io 映象拉取
三個應用映象(aivory-app、aivory-sandbox-sidecar、會話執行時 aivory-sandbox)預設來自 ghcr.io。任選其一:
方式 A:切換映象代理(最省事)——.env 里加一行,三個映象整體換源:
# 任何相容 ghcr 的代理/映象站或你的私有倉庫地址
IMAGE_REGISTRY=ghcr.nju.edu.cn
方式 B:離線搬運(完全不通外網的內網)——在任意能訪問 ghcr.io 的機器上匯出,再傳到伺服器匯入:
# 有網機器
docker pull ghcr.io/hjxwz123/aivory-app:latest
docker pull ghcr.io/hjxwz123/aivory-sandbox-sidecar:latest
docker pull ghcr.io/hjxwz123/aivory-sandbox:latest
docker save ghcr.io/hjxwz123/aivory-app:latest ghcr.io/hjxwz123/aivory-sandbox-sidecar:latest ghcr.io/hjxwz123/aivory-sandbox:latest | gzip > aivory-images.tar.gz
# 目標伺服器
docker load < aivory-images.tar.gz
離線匯入後沙箱 sidecar 不需要再拉執行時映象,可在 .env 加 SANDBOX_PULL_ON_START=0 跳過啟動時的拉取嘗試。
方式 C:推到自己的私有倉庫(阿里雲 ACR、Harbor 等)——docker tag 後推上去,.env 設 IMAGE_REGISTRY=registry.cn-xxx.aliyuncs.com、IMAGE_OWNER=<你的名稱空間>。
第三步:基礎映象加速(可選)
postgres / redis / qdrant 來自 Docker Hub,訪問慢時給 Docker 守護程序配加速器(/etc/docker/daemon.json):
{ "registry-mirrors": ["https://docker.m.daocloud.io"] }
改完 sudo systemctl restart docker,然後正常 docker compose -f docker-compose.prod.yml up -d 即可。
資源建議
| 場景 | CPU | 記憶體 | 說明 |
|---|---|---|---|
| 最小可用(試用、個位數使用者) | 2 核 | 4 GB | 完整棧可以跑,但沙箱併發要收緊 |
| 常規團隊使用 | 4 核 | 8 GB | 覆蓋日常對話 + RAG + 少量併發程式碼執行 |
| 程式碼執行 / 深度研究重度使用 | 4 核以上 | 16 GB 以上 | 按沙箱併發量向上加 |
估算沙箱的記憶體開銷:每個會話容器上限 SANDBOX_MEMORY(預設 2g),併發上限 SANDBOX_MAX_SESSIONS(預設 16),極端情況下僅沙箱就可能吃掉 32 GB。小記憶體機器請務必在 .env 裡調低這兩個值(例如 SANDBOX_MAX_SESSIONS=4),沙箱控制面自身另佔最多 1 GB(mem_limit)。
磁碟方面,pgdata、qdrantdata 和 DATA_DIR 隨使用量增長,建議放在 SSD 上並預留監控;Qdrant 向量和上傳檔案通常是增長最快的兩塊。
日誌檢視
cd deploy
# 跟蹤 app 日誌(最常用)
docker compose -f docker-compose.prod.yml logs -f app
# 只看最近 200 行
docker compose -f docker-compose.prod.yml logs --tail=200 app
# 看最近一小時所有服務的日誌
docker compose -f docker-compose.prod.yml logs --since=1h
# 單看沙箱(排查程式碼執行問題時)
docker compose -f docker-compose.prod.yml logs -f sandbox
# 檢視某個容器健康檢查的探測輸出
docker inspect --format='{{json .State.Health}}' aivory-app-1 | jq
常用運維命令
cd deploy
# 檢視各服務狀態(重點看 STATUS 列的 healthy / unhealthy)
docker compose -f docker-compose.prod.yml ps
# 重啟單個服務
docker compose -f docker-compose.prod.yml restart app
# 進入 Postgres 互動式命令列
docker compose -f docker-compose.prod.yml exec postgres psql -U aivory -d aivory
# 驗證 Redis 連通性(密碼從容器自身的環境變數讀取)
docker compose -f docker-compose.prod.yml exec redis sh -c 'redis-cli -a "$REDIS_PASSWORD" ping'
# 開啟 app 容器 shell
docker compose -f docker-compose.prod.yml exec app sh
# 從宿主機探測健康端點
curl -fsS http://localhost/api/health
# 檢視各命名卷佔用的磁碟
docker system df -v | grep aivory
psql 內常用的幾條自檢查詢:
\dt -- 表清單
SELECT count(*) FROM users; -- 使用者數
\q -- 退出
升級流程
資料庫結構在 app 啟動時自動遷移,升級本身就是"拉新映象 + 重建容器",但升級前請先備份:
cd deploy
# 1. 備份:在管理後臺 Backup & Migration 匯出全量備份,
# 並確認歸檔已生成在 ./data/backups/ 下(複製一份到異地更穩妥)
# 2. 拉取新映象(IMAGE_TAG 固定了版本的話,先在 .env 裡改成目標版本)
docker compose -f docker-compose.prod.yml pull
# 3. 滾動重建(只重建映象有變化的容器)
docker compose -f docker-compose.prod.yml up -d
# 4. 驗證
docker compose -f docker-compose.prod.yml ps
curl -fsS http://localhost/api/health
docker compose -f docker-compose.prod.yml logs --tail=100 app
IMAGE_TAG=latest 意味著每次 pull 都可能拿到新版本,升級時機不可控。生產環境建議把 IMAGE_TAG 固定到具體版本標籤,升級時顯式改值;回滾時把 IMAGE_TAG 改回舊版本再 up -d 即可(資料庫結構遷移是向前的,回滾跨大版本前請先確認備份可用)。
常見故障排查
| 症狀 | 可能原因 | 處理 |
|---|---|---|
docker compose up 直接報錯 set JWT_SECRET in .env(或 POSTGRES_PASSWORD / REDIS_PASSWORD 同類報錯) | .env 缺少必填變數,compose 的 :? 校驗攔截 | cp .env.example .env 後逐項填寫;openssl rand -hex 32 生成 JWT_SECRET |
app 啟動即退出,日誌提示 JWT_SECRET 問題 | AIVORY_ENV=production 下 JWT_SECRET 必須至少 32 字元,佔位值或過短會被拒絕啟動 | 換成 openssl rand -hex 32 的輸出後 up -d |
app 反覆重啟,日誌報資料庫認證/連線失敗 | POSTGRES_PASSWORD 含特殊字元(@ : / # % & 等)破壞了拼接出的連線 URL;或 $ 被 compose 變數插值吞掉;或改過密碼但 pgdata 卷裡還是舊密碼 | 密碼一律用 openssl rand -hex 24(純十六進位制,無特殊字元);已初始化的庫改密碼需在 psql 內 ALTER USER |
sandbox 顯示 unhealthy(尤其是首次啟動) | 冷啟動要拉取會話執行時映象 aivory-sandbox,網路慢時超過 120s 的 start_period | 看 logs -f sandbox 確認是否仍在拉取;可預先 docker pull ghcr.io/hjxwz123/aivory-sandbox:latest;app 不受影響,只有程式碼執行暫不可用 |
app 日誌出現 Qdrant 401 / unauthorized | app 的 QDRANT_API_KEY 與 Qdrant 側的 key 不一致(通常發生在接外部 Qdrant 叢集,或只改了一側的 key) | 兩側設為同一個值;棧內部署時二者讀同一個 .env 變數,改完 up -d 重建即可 |
埠 80 被佔用,app 起不來 | 宿主 80 已有其它服務 | 修改 compose 檔案中的 "80:8787" 左側埠 |
| 對話正常但程式碼執行報錯 | 沙箱是軟依賴,app 不會因它掛掉 | 檢查 sandbox 健康狀態、/var/run/docker.sock 是否存在且可用 |
| RAG 檢索效果突然變差(退化為全文注入) | Qdrant 不可用或為空,應用自動走全文回退 | 檢查 qdrant 容器狀態與 app 日誌,恢復後自動回到向量檢索 |
下一步
- 反向代理與 HTTPS:在棧前面加 TLS 終止層。
- Cloudflare 接入:套 CDN 時的注意事項。
- 程式碼沙箱部署:沙箱的安全模型與深度調參。
- 輕量部署(SQLite):單機小規模場景的極簡替代方案。
- 首次執行配置:初始化管理員與新增模型渠道。