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 Service | The mirror | The Host Port. | duty |
|---|---|---|---|
app | ghcr.io/<IMAGE_OWNER>/aivory-app | 80 can be changed) | Single-containers servo after construction SPA and /api The back |
postgres | postgres:16-alpine | 无 | Relational storage: user, conversation, knowledge base, usage and so on |
redis | redis:7-alpine | 无 | Caching, limit flow counters, cross-process stop streaming output pub/sub |
qdrant | qdrant/qdrant:v1.12.4 | 无 | RAG Vector Recovery |
sandbox | ghcr.io/<IMAGE_OWNER>/aivory-sandbox-sidecar | 无 | The built-in code executes the sandbox control, accessible only on the internal network. |
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_ORIGIN、ALLOWED_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/apiThe SPA directory was built when the mirror image was built (STATIC_DIRPointing 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:
postgres和redisHealth care needs to be checked (condition: service_healthy),qdrantIt is only requested to be launched (condition: service_startedThere is no definition of health check. - **
appnot dependentsandbox**: 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.appMid-definition: used every 15 swgetDetection ofhttp://127.0.0.1:8787/api/healthOvertime 3s, launch width 20s, failure 5 consecutive times judged unhealthy. - Directory of data:
${DATA_DIR:-./data}attached to the container./app/dataStorage 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):
| varied | Values in Composite | Explained |
|---|---|---|
AIVORY_ENV | production | Deployment level security screening (such as JWT_SECRET and compulsory) |
DATABASE_URL | postgres://<user>:<password>@postgres:5432/<db>?sslmode=disable | 由 .env The Postgres variables are stacked. |
REDIS_URL | redis://:<REDIS_PASSWORD>@redis:6379/0 | Enable Redis cache, cohort and stream recovery |
QDRANT_URL | admittedly http://qdrant:6333 | Point to Qdrant in the stack; can be covered as an external cluster |
QDRANT_API_KEY | admittedly aivory-internal-qdrant | must with qdrant Key of service consistency (see below) |
JWT_SECRET | must be in .env set up | When missing compose direct error report refuses to start |
SANDBOX_BASE_URL | http://sandbox:8000 | Smuggling with network access with built-in sandbox |
SANDBOX_API_KEY | admittedly aivory-bundled-sandbox | Internal default values shared with the sandbox service |
ENABLE_MOCK_PROVIDER | admittedly false | 置 true Enable built-in demo model without a real API key |
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-alpineThe data falls in the name volume.pgdataWithin the container./var/lib/postgresql/data)。 POSTGRES_PASSWORDUse of Composite: Composite${POSTGRES_PASSWORD:?...}Syntax, no direct error reporting set.User name and library name are both defaultaivory。- Health inspection: every 10 s
pg_isreadyOvertime 5s, try up to 10 times.appWait until it becomes healthy. - Database Schema
appIt is created and migrated automatically at start without the need to execute any SQL manually.
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-alpineand by--appendonly yes --requirepass <REDIS_PASSWORD>Start: Open AOF Permanence and compel password authentication.REDIS_PASSWORDalso is.envmust 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.
redisdataWithin the container./data)。 - Health inspection: every 10s
redis-cli -a "$REDIS_PASSWORD" pingand check back.PONGOvertime 5s, repeat 10 times.
Qdrant: Vector Database
- The mirror
qdrant/qdrant:v1.12.4The data falls in the name volume.qdrantdataWithin the container./qdrant/storage)。 - Qdrant requires an API key for each request (
QDRANT__SERVICE__API_KEYIn the stack.qdrant与appRead the same..envvariedQDRANT_API_KEYBoth sides share the value.aivory-internal-qdrantBecause 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..envCovered with strong random values. - There is no definition of health check.
appJust wait for it.service_startedQdrant’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 correspondsaivory_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.sockA limited container derived from each session in the host Docker Guardianship process, a session runtime mirror forghcr.io/<IMAGE_OWNER>/aivory-sandboxAt the start of the process (SANDBOX_PULL_ON_START: "1"The first call can be used. - ** Not publishing any host port. ** Only only
appthrough private networks.http://sandbox:8000Visit it and hang it.docker.sockEquivalent to host root permissions, “Do not expose ports” is the key to controlling this risk aspect, don’t add it.portsThe Mapping. - The control container itself has a resource limit:
mem_limit: 1g、pids_limit: 512Session container resources are controlled by environmental variables:
| varied | The default value | Meaning of |
|---|---|---|
SANDBOX_NETWORK | none | Session containers do not have a network; only when code within the sandbox requires networking bridge |
SANDBOX_MEMORY | 2g | Upper limit for single session containers |
SANDBOX_CPUS | 1 | Single session container CPU quota |
SANDBOX_MAX_SESSIONS | 16 | Maximum number of session containers. |
SANDBOX_EXEC_TIMEOUT_CAP_MS | 600000 | Once 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_SECONDS | 86400 | Free recovery window hard upper limit (24 hours), the same with the admin console settings |
SANDBOX_WORKSPACE_SIZE | 512m | Session work area capacity |
SANDBOX_READ_ONLY_ROOTFS | 1 | Session container root file system only read |
- Work area persistence: When the session is recovered,
/workspaceFiled 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
urllibDetection of containers.http://localhost:8000/healthz(There is no other detection tool in the mirror except Python), overtime 10s, try again 3 times,start_period为 120s 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 / Hanging | The container path. | The contents | Lost consequences |
|---|---|---|---|
pgdata Name of Volume | /var/lib/postgresql/data | All relational data: user, conversation, knowledge base, configuration | Disastrous and must be repaired. |
qdrantdata Name of Volume | /qdrant/storage | The RAG vector | You can reconstruct chunk text from a database, but you need to re-consume embedded API calls |
redisdata Name of Volume | /data | Caching and Limit Flow Counter (AOF) | The impact is small, and natural reconstruction after restart. |
sandbox-archives Name of Volume | /var/lib/aivory/sandbox-archives | Sandbox 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/data | Upload documents, generated products, backup archives (backups/ List of children) | User uploads and products are lost, must be backed up |
-v It will be deleted together with the name volume. pgdata、qdrantdata 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:
- ** 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_DIRInside the container./app/data/backupscorresponding to the host.DATA_DIR/backupsThis backup can be recovered across engines (such as recovering to SQLite deployment). Backup and Migration。 - ** Cold infrastructure **: after stopping the entire stack, put all the named volumes and
DATA_DIRPhotocopy / 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
app 和 sandbox 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:
| varied | The default value | Explained |
|---|---|---|
IMAGE_REGISTRY | ghcr.io | 镜像仓库地址;GitHub 访问受限时可整体切换到 ghcr 镜像代理或自建/私有仓库,见下方「受限网络部署」 |
IMAGE_OWNER | hjxwz123 | ghcr.io name space; fork to your own account after changing to your own username |
IMAGE_TAG | latest | Mirror 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-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
离线导入后 sandbox 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 即可。
Recommendations for Resources
| The Scene | CPU | The Memory | Explained |
|---|---|---|---|
| Minimum available (trial, single-digit user) | 2 nuclear | 4 GB | The entire stack can run, but the sandbox must be tightened. |
| Used by regular teams | 4 Nuclear | 8 GB | Cover daily conversation + RAG + small number of simultaneous code execution |
| Code Execution / Deep Research | 4 Nuclear | More than 16 GB | Add 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, pgdata、qdrantdata 和 DATA_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
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.
| Symptoms | Possibly 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 examination | cp .env.example .env subsequently filled in; openssl rand -hex 32 generated JWT_SECRET |
app Start and exit, log reminds about JWT_SECRET problem | AIVORY_ENV=production 下 JWT_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 fails | POSTGRES_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_period | 看 logs -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 / Unauthorized | app 的 QDRANT_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 come | The 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 text | checked qdrant Containers and app Log, automatically return to vector recovery after recovery |
The next step.
- Reverse Proxy and HTTPS: Add the TLS termination layer in front of the stack.
- Cloudflare access Attention to CDN.
- Deployment of Sandbox Sandbox security model and depth shift.
- Lightweight Deployment (SQLite) A very simple alternative to a small scale single-machine scene.
- First operating configuration Initialize administrators and add model channels.