Skip to main content

Deployment of Docker Compose

This page provides an in-depth description of Aivory's production compose documentation deploy/docker-compose.prod.yml: Responsibilities for each service with health checks, data volumes and backup strategies, pre-built mirrors and local build switches, resource planning, daily operating commands, upgrade processes, and common failure checklists. Rapid deployment This page assumes that you have completed the first deployment.

Structural Overview

The entire stack consists of 5 services, all hanging on a private bridge network internal Only up. app Publish host port** (default 80:8787 The rest of the services (including Qdrant and sandbox) do not expose any ports to the outside, and can only be accessed within a private network.

The ServiceThe mirrorThe Host Port.duty
appghcr.io/<IMAGE_OWNER>/aivory-app80 can be changed)Single-containers servo after construction SPA and /api The back
postgrespostgres:16-alpineRelational storage: user, conversation, knowledge base, usage and so on
redisredis:7-alpineCaching, limit flow counters, cross-process stop streaming output pub/sub
qdrantqdrant/qdrant:v1.12.4RAG Vector Recovery
sandboxghcr.io/<IMAGE_OWNER>/aivory-sandbox-sidecarThe built-in code executes the sandbox control, accessible only on the internal network.
Same source structure, zero domain name configuration

app The Go process in the container simultaneously serves SPA static files and /api Therefore, there is no cross-domain problem, either. ** not need ** Configured PUBLIC_ORIGINALLOWED_ORIGINS Or any domain name related variable: the agent forwardes the request to the container, which domain name enters which domain name can be used, multiple domain names pointing at the same time is no problem. When the public network is deployed, just put a layer of TLS termination in front (see Reverse Proxy and HTTPS)。

Detailed Services

App: Application of main containers

  • Listening in the container. 8787 (AIVORY_LISTEN: ":8787" At the same time, the SPA and /api The SPA directory was built when the mirror image was built (STATIC_DIR Pointing to the building product within the mirror image) without the need for a separate nginx/web layer.
  • Port mapping is written dead in the compose document: "80:8787" When host 80 is occupied, change the number directly to the left (e.g. "8080:8787" No environmental variables are required.
  • Start of Dependency: postgresredis Health care needs to be checked (condition: service_healthy), qdrant It is only requested to be launched (condition: service_started There is no definition of health check.
  • ** app not dependent sandbox **: The sandbox is a soft dependence on request, only the code execution functionality is used for it. If it is made into a startup dependence, the sandbox mirror can not pull down or will drag down the entire application when it is unhealthy, so compose does not intend to declare this dependence.
  • Medical examination by mirror (Dockerfile.app Mid-definition: used every 15 s wget Detection of http://127.0.0.1:8787/api/health Overtime 3s, launch width 20s, failure 5 consecutive times judged unhealthy.
  • Directory of data: ${DATA_DIR:-./data} attached to the container. /app/data Storage upload files, generate products, backup archives (and SQLite files, if used).Binding loads to host directories rather than Docker volumes is to make these files directly visible and backupable on the host.

compose for app Key environmental variables injected (see the complete list) Core environmental variables):

variedValues in CompositeExplained
AIVORY_ENVproductionDeployment level security screening (such as JWT_SECRET and compulsory)
DATABASE_URLpostgres://<user>:<password>@postgres:5432/<db>?sslmode=disable.env The Postgres variables are stacked.
REDIS_URLredis://:<REDIS_PASSWORD>@redis:6379/0Enable Redis cache, cohort and stream recovery
QDRANT_URLadmittedly http://qdrant:6333Point to Qdrant in the stack; can be covered as an external cluster
QDRANT_API_KEYadmittedly aivory-internal-qdrantmust with qdrant Key of service consistency (see below)
JWT_SECRETmust be in .env set upWhen missing compose direct error report refuses to start
SANDBOX_BASE_URLhttp://sandbox:8000Smuggling with network access with built-in sandbox
SANDBOX_API_KEYadmittedly aivory-bundled-sandboxInternal default values shared with the sandbox service
ENABLE_MOCK_PROVIDERadmittedly falsetrue Enable built-in demo model without a real API key
the model API key is not in .env

API key for providers such as Anthropic/OpenAI ** Not by environmental variables. ** They are stored in the channels table of the database and deployed in the admin console. Channel and Model Page is added.

Postgres: Relational Databases

  • The mirror postgres:16-alpine The data falls in the name volume. pgdata Within the container. /var/lib/postgresql/data)。
  • POSTGRES_PASSWORD Use of Composite: Composite ${POSTGRES_PASSWORD:?...} Syntax, no direct error reporting set.User name and library name are both default aivory
  • Health inspection: every 10 s pg_isready Overtime 5s, try up to 10 times. app Wait until it becomes healthy.
  • Database Schema app It is created and migrated automatically at start without the need to execute any SQL manually.
Postgres official mirror applies passwords only when initialized for the first time

POSTGRES_PASSWORD only in pgdata The volume is empty and comes into effect when the database is initialized for the first time. .env that value. ** No is ** Updating the actual password in the database will only result in app Authentication fails when connecting the old library with a new password. Change the password or use it in the database ALTER USER Execute, or delete (only environment where data can be discarded) pgdata Volume is restarted.

Redis: Caching and Message

  • The mirror redis:7-alpine and by --appendonly yes --requirepass <REDIS_PASSWORD> Start: Open AOF Permanence and compel password authentication. REDIS_PASSWORD also is .env must be filled.
  • Caching, limit flow counters, and pub/subs with cross-process "stop generating" signals; when Redis is configured, the app also enables Redis cohort and stream recovery.
  • Data falls in name volumes. redisdata Within the container. /data)。
  • Health inspection: every 10s redis-cli -a "$REDIS_PASSWORD" ping and check back. PONG Overtime 5s, repeat 10 times.

Qdrant: Vector Database

  • The mirror qdrant/qdrant:v1.12.4 The data falls in the name volume. qdrantdata Within the container. /qdrant/storage)。
  • Qdrant requires an API key for each request (QDRANT__SERVICE__API_KEY In the stack. qdrantapp Read the same. .env varied QDRANT_API_KEY Both sides share the value. aivory-internal-qdrant Because Qdrant does not publish any host ports and is only available within a private network, this shared default is acceptable; it is still recommended that you use it. .env Covered with strong random values.
  • There is no definition of health check. app Just wait for it. service_started Qdrant’s short-term unavailability does not cause functional disruption: When vector retrieval fails, the RAG returns to the text injected into the full text.
  • Collection is named by embedding dimension aivory_c<维度> For example, the 1536-dimensional model corresponds aivory_c1536)。

Sandbox: Code execution of the sandbox control

Sandbox with stack, one docker compose up With it, there is no need for a separate project or additional configuration of the API key. Deployment of Sandbox Here is the compose level:

  • The service is a sidecar control: through hanging hosts. /var/run/docker.sock A limited container derived from each session in the host Docker Guardianship process, a session runtime mirror for ghcr.io/<IMAGE_OWNER>/aivory-sandbox At the start of the process (SANDBOX_PULL_ON_START: "1" The first call can be used.
  • ** Not publishing any host port. ** Only only app through private networks. http://sandbox:8000 Visit it and hang it. docker.sock Equivalent to host root permissions, “Do not expose ports” is the key to controlling this risk aspect, don’t add it. ports The Mapping.
  • The control container itself has a resource limit: mem_limit: 1gpids_limit: 512 Session container resources are controlled by environmental variables:
variedThe default valueMeaning of
SANDBOX_NETWORKnoneSession containers do not have a network; only when code within the sandbox requires networking bridge
SANDBOX_MEMORY2gUpper limit for single session containers
SANDBOX_CPUS1Single session container CPU quota
SANDBOX_MAX_SESSIONS16Maximum number of session containers.
SANDBOX_EXEC_TIMEOUT_CAP_MS600000Once the hard time upper limit is carried out (10 minutes), the overtime settings of the admin console will be limited to this value.
SANDBOX_IDLE_TTL_CAP_SECONDS86400Free recovery window hard upper limit (24 hours), the same with the admin console settings
SANDBOX_WORKSPACE_SIZE512mSession work area capacity
SANDBOX_READ_ONLY_ROOTFS1Session container root file system only read
  • Work area persistence: When the session is recovered, /workspace Filed to Name Volume. sandbox-archives (Conversation ID key) automatically recovers when the same conversation runs the code again. This local archive zero configuration is available, but ** Only single machine. ** Multi-copy deployments must be switched to S3/OSS class object storage.
  • Health Check: Every 30s with Python urllib Detection of containers. http://localhost:8000/healthz (There is no other detection tool in the mirror except Python), overtime 10s, try again 3 times, start_period120s This grace period covers the time it takes to pull the session runtime mirror at the cold start, and the pull before the service is completed does not start accepting requests.

Data Persistence and Backup

Where are the data?

Volume / HangingThe container path.The contentsLost consequences
pgdata Name of Volume/var/lib/postgresql/dataAll relational data: user, conversation, knowledge base, configurationDisastrous and must be repaired.
qdrantdata Name of Volume/qdrant/storageThe RAG vectorYou can reconstruct chunk text from a database, but you need to re-consume embedded API calls
redisdata Name of Volume/dataCaching and Limit Flow Counter (AOF)The impact is small, and natural reconstruction after restart.
sandbox-archives Name of Volume/var/lib/aivory/sandbox-archivesSandbox workspace for conversations.The sandbox file that corresponds to conversation is lost and does not affect the conversation itself
DATA_DIR (The host directory is bound, attached by default ./data)/app/dataUpload documents, generated products, backup archives (backups/ List of children)User uploads and products are lost, must be backed up
Do not execute docker compose down -v

-v It will be deleted together with the name volume. pgdataqdrantdata When all data is empty and unrecoverable, service stops daily. docker compose -f docker-compose.prod.yml down without taking -v) or stop

Backup tactics

Two parallel routes are recommended:

  1. ** Application level backup (preferred) ** The Backup & Migration page in admin console can generate full-volume migration ZIP asynchronously, with engine-neutral database logic backups (one JSONL per table), optional uploads/artifacts files, and optional Qdrant vector data. BACKUP_DIR Inside the container. /app/data/backups corresponding to the host. DATA_DIR/backups This backup can be recovered across engines (such as recovering to SQLite deployment). Backup and Migration
  2. ** Cold infrastructure **: after stopping the entire stack, put all the named volumes and DATA_DIR Photocopy / copy of the catalogue. ** Must be backed up together. ** This ensures that the database lines, vectors and disk files are consistent.

Upper limit of imported size MAX_BACKUP_BYTES Control is 20 GiB by default; the total amount of vector archives can be large and upgraded if necessary.

Pre-constructing mirrors and local construction

appsandbox Both services were announced at the same time. image:build: Compose behavior: when the mirror is present (or can be pulled) priority is given to the mirror, plus --build The same compose file thus serves two processes, and topology does not need to be written twice.

** Method 1: Draw pre-constructed mirrors (recommended for production) **

cd deploy
docker compose -f docker-compose.prod.yml pull
docker compose -f docker-compose.prod.yml up -d

** Method 2: Local build from source code (for secondary development or for architectures that are not covered by official mirrors) **

cd deploy
docker compose -f docker-compose.prod.yml up -d --build

Source of Mirror .env Control of two variables:

variedThe default valueExplained
IMAGE_REGISTRYghcr.io镜像仓库地址;GitHub 访问受限时可整体切换到 ghcr 镜像代理或自建/私有仓库,见下方「受限网络部署」
IMAGE_OWNERhjxwz123ghcr.io name space; fork to your own account after changing to your own username
IMAGE_TAGlatestMirror labels; production environment recommends fixing to specific version labels, upgrading and rolling back are more controllable

At the time of local construction, app The structure below is the warehouse root directory: Dockerfile.app It is a three-stage construction, first with Node 20 to build Vite SPA (executed on the built-in architecture, the product is a static file unrelated to the architecture), then with Go 1.24 compilation API binary (required CGO, because the binary embedded SQLite driver as the development/return backend), and finally to enter debian:bookworm-slim mirror in operation.

受限网络部署(无法访问 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

离线导入后 sandbox 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 即可。

Recommendations for Resources

The SceneCPUThe MemoryExplained
Minimum available (trial, single-digit user)2 nuclear4 GBThe entire stack can run, but the sandbox must be tightened.
Used by regular teams4 Nuclear8 GBCover daily conversation + RAG + small number of simultaneous code execution
Code Execution / Deep Research4 NuclearMore than 16 GBAdd up to the sandbox.

Estimate memory expenditure for the sandbox: a ceiling for each session container SANDBOX_MEMORY (Standard 2g) and simultaneous upper limit SANDBOX_MAX_SESSIONS (Default 16), in extreme cases, only the sandbox can consume 32 GB. .env Reduce these values (e.g. SANDBOX_MAX_SESSIONS=4, the sandbox controller itself has a maximum of 1 GB (mem_limit)。

of the disc, pgdataqdrantdataDATA_DIR As usage grows, it is recommended to put it on SSDs and reserve monitoring; Qdrant vectors and upload files are usually the two fastest growing pieces.

Check the journal.

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

Ordinary operating orders.

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 Several commonly used self-checks:

\dt -- 表清单
SELECT count(*) FROM users; -- 用户数
\q -- 退出

upgrading process

The database structure is app Automatic migration at boot, upgrading itself is "Draw New Mirror + Reconstruct Container", but ** Please update before upgrading. **:

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
Fixed version labels

IMAGE_TAG=latest Means every time. pull It is possible to get a new version, and the upgrade time is uncontrollable. IMAGE_TAG Fix to specific version tags, explicitly change the value when upgrading; roll back to IMAGE_TAG Replace the old version. up -d Yes (database structure migration is forward, please confirm that backup is available before scrolling back across the large version).

Common failure inspection.

SymptomsPossibly the cause.Treated
docker compose up direct error set JWT_SECRET in .env (or similar type of POSTGRES_PASSWORD / REDIS_PASSWORD error).env Lack of compulsory variables, composite. :? Intercepted examinationcp .env.example .env subsequently filled in; openssl rand -hex 32 generated JWT_SECRET
app Start and exit, log reminds about JWT_SECRET problemAIVORY_ENV=productionJWT_SECRET It must be at least 32 characters and the position value or short will be denied to start.in exchange openssl rand -hex 32 After the export. up -d
app Repeated restart, log database authentication/connection failsPOSTGRES_PASSWORD with special characters (@ : / # % & (iii) the link URL that has been stacked is corrupted; or $ is swallowed by the compose variable plug; or changed the password but pgdata The old code.All code is used. openssl rand -hex 24 (Pure sixteen, no special characters); the initialized library password needs to be in psql ALTER USER
sandbox Show unhealthy (especially at first launch)Cold start to pull the session running mirror. aivory-sandbox Lower than 120 seconds. start_periodlogs -f sandbox Confirm whether it is still being pulled; can be taken in advance docker pull ghcr.io/hjxwz123/aivory-sandbox:latest; app Not affected, only code execution is temporarily unavailable
app Qdrant 401 / UnauthorizedappQDRANT_API_KEY Discrepancy with the key on the Qdrant side (usually occurring in an external Qdrant cluster, or just changing the key on one side)Both sides are set to the same value; both sides read the same value inside the stack. .env Changes are finished. up -d Reconstruction is possible.
Port 80 is occupied. app not comeThe host has 80 other services.Modify the compose document. "80:8787" The left port.
Conversation is normal but the code executes an error.Sandbox is soft dependency. app Not because it hangs.checked sandbox state of health, /var/run/docker.sock exist and available.
The RAG retrieval effect suddenly deteriorated (degraded to the full injection)Qdrant is unavailable or empty, the application automatically goes back to the full textchecked qdrant Containers and app Log, automatically return to vector recovery after recovery

The next step.