跳至主要内容

Docker Compose 生產部署

本頁深入講解 Aivory 的生產 compose 檔案 deploy/docker-compose.prod.yml:每個服務的職責與健康檢查、資料卷與備份策略、預構建映象與本地構建的切換、資源規劃、日常運維命令、升級流程,以及常見故障的排查表。如果你只想儘快把服務拉起來,請先看快速部署,本頁假設你已經完成了首次部署。

架構總覽

整套棧由 5 個服務組成,全部掛在一個私有 bridge 網路 internal 上。只有 app 釋出宿主埠(預設 80:8787),其餘服務(包括 Qdrant 和沙箱)不對外暴露任何埠,只能在私有網路內被訪問。

服務映象宿主埠職責
appghcr.io/<IMAGE_OWNER>/aivory-app80(可改)單容器同源伺服構建後的 SPA 與 /api 後端
postgrespostgres:16-alpine關係型儲存:使用者、對話、知識庫、用量等
redisredis:7-alpine快取、限流計數器、跨程序停止流式輸出的 pub/sub
qdrantqdrant/qdrant:v1.12.4RAG 向量檢索
sandboxghcr.io/<IMAGE_OWNER>/aivory-sandbox-sidecar內建程式碼執行沙箱控制面,僅內網可達
同源架構,零域名配置

app 容器內的 Go 程序同時伺服 SPA 靜態檔案和 /api,前後端天然同源。因此不存在跨域問題,也不需要配置 PUBLIC_ORIGINALLOWED_ORIGINS 或任何域名相關變數:代理把請求轉發到容器,哪個域名進來哪個域名就能用,多域名同時指向也沒問題。公網部署時只需在前面放一層 TLS 終止(見反向代理與 HTTPS)。

服務詳解

app:應用主容器

  • 容器內監聽 8787(AIVORY_LISTEN: ":8787"),同時伺服 SPA 與 /api。SPA 目錄在構建映象時已內建(STATIC_DIR 指向映象內的構建產物),無需單獨的 nginx/web 層。
  • 埠對映寫死在 compose 檔案裡:"80:8787"。宿主 80 被佔用時,直接改左邊的數字(例如 "8080:8787"),不需要任何環境變數。
  • 啟動依賴:postgresredis 必須通過健康檢查(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_ENVproduction觸發部署級安全校驗(如 JWT_SECRET 強制)
DATABASE_URLpostgres://<user>:<password>@postgres:5432/<db>?sslmode=disable.env 中的 Postgres 變數拼接而成
REDIS_URLredis://:<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_URLhttp://sandbox:8000走私有網路訪問內建沙箱
SANDBOX_API_KEY預設 aivory-bundled-sandbox與沙箱服務共享的內部預設值
ENABLE_MOCK_PROVIDER預設 falsetrue 可啟用內建演示模型,無需真實 API key
模型 API key 不在 .env 裡

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 官方映象只在首次初始化時應用密碼

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)。棧內 qdrantapp 讀取同一個 .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: 1gpids_limit: 512。會話容器的資源由環境變數控制:
變數預設值含義
SANDBOX_NETWORKnone會話容器無網路;僅當沙箱內程式碼需要聯網時改為 bridge
SANDBOX_MEMORY2g單個會話容器記憶體上限
SANDBOX_CPUS1單個會話容器 CPU 配額
SANDBOX_MAX_SESSIONS16併發會話容器數上限
SANDBOX_EXEC_TIMEOUT_CAP_MS600000單次執行時長硬上限(10 分鐘),管理後臺的超時設定會被鉗制到此值以內
SANDBOX_IDLE_TTL_CAP_SECONDS86400空閒回收視窗硬上限(24 小時),同樣鉗制管理後臺設定
SANDBOX_WORKSPACE_SIZE512m會話工作區容量
SANDBOX_READ_ONLY_ROOTFS1會話容器根檔案系統只讀
  • 工作區持久化:會話被回收時,/workspace 會打包歸檔到命名卷 sandbox-archives(按對話 ID 鍵控),同一對話再次執行程式碼時自動恢復。這套本地歸檔零配置可用,但僅限單機;多副本部署必須改用 S3/OSS 類物件儲存。
  • 健康檢查:每 30s 用 Python urllib 探測容器內 http://localhost:8000/healthz(映象裡除 Python 外沒有其它探測工具),超時 10s,重試 3 次,start_period120s。這個寬限期覆蓋的是冷啟動時拉取會話執行時映象的耗時,拉取完成前服務不開始受理請求。

資料持久化與備份

資料都在哪裡

卷 / 掛載容器路徑內容丟失後果
pgdata(命名卷)/var/lib/postgresql/data全部關係型資料:使用者、對話、知識庫、配置災難性,必須備份
qdrantdata(命名卷)/qdrant/storageRAG 向量可從資料庫中的 chunk 文本重建,但要重新消耗嵌入 API 呼叫
redisdata(命名卷)/data快取、限流計數器(AOF)影響很小,重啟後自然重建
sandbox-archives(命名卷)/var/lib/aivory/sandbox-archives各對話的沙箱工作區歸檔對應對話的沙箱檔案丟失,不影響對話本身
DATA_DIR(宿主目錄繫結掛載,預設 ./data)/app/data上傳檔案、生成產物、備份歸檔(backups/ 子目錄)使用者上傳與產物丟失,必須備份
不要執行 docker compose down -v

-v 會連同命名卷一起刪除,pgdataqdrantdata 等全部資料當場清空且不可恢復。日常停止服務用 docker compose -f docker-compose.prod.yml down(不帶 -v)或 stop

備份策略

推薦兩條路線並行:

  1. 應用級備份(首選):管理後臺的 Backup & Migration 頁面可非同步生成全量遷移 ZIP,內含引擎中立的資料庫邏輯備份(每表一個 JSONL)、可選的 uploads/artifacts 檔案,以及可選的 Qdrant 向量資料。生成的歸檔存放在 BACKUP_DIR(容器內預設 /app/data/backups,宿主機上對應 DATA_DIR/backups),把它拷走異地儲存即可。這種備份可以跨引擎恢復(比如恢復到 SQLite 部署),詳見備份與遷移
  2. 基礎設施級冷備:停止整個棧後,把所有命名卷和 DATA_DIR 目錄一起快照/複製。必須一起備份,才能保證資料庫行、向量和磁碟檔案三者一致。

備份匯入的大小上限由 MAX_BACKUP_BYTES 控制,預設 20 GiB;含向量的全量歸檔可能很大,必要時上調。

預構建映象與本地構建

appsandbox 兩個服務同時聲明瞭 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_REGISTRYghcr.io映象倉庫地址;GitHub 訪問受限時可整體切換到 ghcr 映象代理或自建/私有倉庫,見下方「受限網路部署」
IMAGE_OWNERhjxwz123ghcr.io 名稱空間;fork 到自己賬號後改成自己的使用者名稱
IMAGE_TAGlatest映象標籤;生產環境建議固定到具體版本標籤,升級和回滾都更可控

本地構建時,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-appaivory-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 不需要再拉執行時映象,可在 .envSANDBOX_PULL_ON_START=0 跳過啟動時的拉取嘗試。

方式 C:推到自己的私有倉庫(阿里雲 ACR、Harbor 等)——docker tag 後推上去,.envIMAGE_REGISTRY=registry.cn-xxx.aliyuncs.comIMAGE_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)。

磁碟方面,pgdataqdrantdataDATA_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=productionJWT_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_periodlogs -f sandbox 確認是否仍在拉取;可預先 docker pull ghcr.io/hjxwz123/aivory-sandbox:latest;app 不受影響,只有程式碼執行暫不可用
app 日誌出現 Qdrant 401 / unauthorizedappQDRANT_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 日誌,恢復後自動回到向量檢索

下一步