轻量部署(SQLite)
Aivory 的后端在启动时按环境变量自动选型:DATABASE_URL 以 postgres:// 开头就用 PostgreSQL,否则(例如一个 .db 文件路径)使用内嵌 SQLite;REDIS_URL 留空则用进程内缓存/队列;QDRANT_URL 留空则禁用向量检索,RAG 回退为注入全文上下文。生产二进制和开发用的是同一个,SQLite 驱动已经编译在镜像里。
这意味着你可以把完整生产栈的 5 个服务砍到 1 个:只跑 app 容器,数据落在一个 SQLite 文件里。本页讲清楚这种部署方式适合谁、compose 怎么改、有什么限制,以及日后如何无痛迁移到 Postgres。
何时选择 SQLite 模式
| 场景 | 推荐 |
|---|---|
| 个人使用 / 试用评估 | SQLite,一个容器一分钟拉起 |
| 小团队(个位数到十几人),单机部署 | SQLite 通常够用 |
| 资源受限的小 VPS(1 核 1 GB 级别) | SQLite,完整栈跑不动 |
| 需要多副本 / 水平扩展 | 必须 Postgres,SQLite 是单写入者 |
| 写入密集(大量并发对话、频繁知识库写入) | 建议 Postgres |
| 需要外部工具直接并发读写数据库 | 建议 Postgres |
选 SQLite 不是一锤子买卖:备份格式是引擎中立的,之后随时可以整库迁到 Postgres(见下文)。
最小 compose 文件
只保留 app 一个服务:去掉 postgres、redis、qdrant 三个服务及其 depends_on,把 DATABASE_URL 改成 SQLite 文件路径,并且不设置 REDIS_URL 和 QDRANT_URL。
name: aivory
services:
app:
image: ghcr.io/${IMAGE_OWNER:-hjxwz123}/aivory-app:${IMAGE_TAG:-latest}
restart: unless-stopped
ports:
- "80:8787"
environment:
AIVORY_ENV: production
# SQLite:非 postgres:// 的值即选中内嵌 SQLite。
# 两个 pragma 分别开启 WAL 日志模式和 5 秒写锁等待。
DATABASE_URL: "/app/data/aivory.db?_pragma=journal_mode(WAL)&_pragma=busy_timeout(5000)"
# 不设置 REDIS_URL:使用进程内缓存/队列
# 不设置 QDRANT_URL:禁用向量检索,RAG 走全文注入回退
JWT_SECRET: ${JWT_SECRET:?set JWT_SECRET in .env}
# 试用时可置 true,启用内置演示模型,无需任何真实 API key
ENABLE_MOCK_PROVIDER: ${ENABLE_MOCK_PROVIDER:-false}
volumes:
# 一个目录装下所有持久化数据:aivory.db + uploads/ + artifacts/ + backups/
- ${DATA_DIR:-./data}:/app/data
配套的 .env 只需要一行必填项:
# openssl rand -hex 32
JWT_SECRET=<至少 32 字符的强随机值>
启动与验证:
docker compose up -d
curl -fsS http://localhost/api/health
首次启动时系统没有任何用户,打开站点后第一个注册的账号自动成为管理员,详见首次运行配置。
为什么数据库文件放在 /app/data 下
镜像内置的上传目录、产物目录和备份目录都在 /app/data 下(UPLOAD_DIR=/app/data/uploads、ARTIFACT_DIR=/app/data/artifacts、BACKUP_DIR=/app/data/backups)。把 SQLite 文件也放进 /app/data,一个绑定挂载就覆盖了全部持久化数据:
./data/ # 宿主机目录(DATA_DIR)
├── aivory.db # SQLite 主库
├── aivory.db-wal # WAL 日志(运行期存在,属于数据库的一部分)
├── aivory.db-shm # WAL 共享内存文件
├── uploads/ # 用户上传的文件
├── artifacts/ # 生成的产物
└── backups/ # 管理后台导出的备份归档
如果 DATABASE_URL 指向挂载卷之外的容器内路径,容器一旦重建,整个数据库就没了。服务端不设 DATABASE_URL 时的默认值是 ./data/aivory.db?_pragma=journal_mode(WAL)&_pragma=busy_timeout(5000)(相对进程工作目录,在容器内即 /app/data/ 下),恰好落在挂载点内;显式写成绝对路径是为了杜绝歧义。
两个 pragma 参数的含义
| 参数 | 值 | 作用 |
|---|---|---|
_pragma=journal_mode(WAL) | WAL | 写前日志模式:读写不互斥,多个读者可与单个写者并发 |
_pragma=busy_timeout(5000) | 5000 ms | 遇到写锁时等待最多 5 秒再报错,而不是立即失败 |
这两个参数就是服务端的默认配置,照抄即可,不建议删减。
砍掉 Redis 和 Qdrant 的代价
| 去掉的组件 | 替代行为 | 实际影响 |
|---|---|---|
postgres | 内嵌 SQLite | 单写入者限制(见下节);功能无差异 |
redis | 进程内缓存与队列 | 单实例下功能可用;流恢复等依赖 Redis 的能力不启用;缓存和限流计数随进程重启清零 |
qdrant | RAG 全文注入回退 | 知识库仍可用,但不做向量相似度检索,而是把范围内的文档全文注入上下文;文档量大时消耗更多 token、检索精度下降 |
选型是各自独立的:完全可以只把 Postgres 换成 SQLite,而保留 redis 和 qdrant 服务(保留对应的 REDIS_URL / QDRANT_URL / QDRANT_API_KEY 环境变量和 depends_on 条目即可)。知识库是重度使用场景的话,建议至少保留 Qdrant。
代码沙箱怎么办
沙箱与数据库选型无关。需要代码执行功能时,把完整栈里的 sandbox 服务原样搬进你的 compose 文件,并给 app 加上两个环境变量:
environment:
# ...上面的变量保持不变...
SANDBOX_BASE_URL: "http://sandbox:8000"
SANDBOX_API_KEY: ${SANDBOX_API_KEY:-aivory-bundled-sandbox}
沙箱服务的完整定义和安全注意事项(Docker socket 挂载、不发布端口等)见代码沙箱部署。不需要代码执行就什么都不加,应用其余功能不受影响。
单写入者限制
SQLite 是单文件嵌入式数据库,WAL 模式下允许多个读者与一个写者并发。这带来几条硬性约束:
- 只能有一个
app实例打开这个数据库文件。不要把服务扩展为多副本,不要让两个容器挂同一个数据目录,也不要在应用运行时用其它工具对该文件做写操作。 - 数据库文件必须在本地文件系统上。不要把
DATA_DIR放在 NFS 或其它网络文件系统上,文件锁语义不可靠,可能导致数据库损坏。 - 写入吞吐有上限。
busy_timeout(5000)意味着并发写高峰时,后到的写请求最多排队 5 秒;持续的写入压力堆积会表现为请求变慢甚至超时。到了这个阶段就该迁 Postgres 了。
JWT_SECRET 在 SQLite 下同样强制
不要以为"轻量部署"就能省掉这一项。判定是否执行部署级安全校验看的是:AIVORY_ENV 是否为非开发值,或 DATABASE_URL 是否为 Postgres。上面的最小 compose 设置了 AIVORY_ENV: production,所以即使数据库是 SQLite,JWT_SECRET 也必须设置且至少 32 字符,否则应用拒绝启动。
开发环境下不设 JWT_SECRET 时,应用会自动生成一个随机临时密钥,每次重启所有登录会话全部失效,且这是为本机开发准备的行为,不是部署选项。任何对外提供服务的实例都应显式设置 AIVORY_ENV: production 和强随机 JWT_SECRET。
备份
SQLite 模式的一大好处是备份面极小:所有持久化数据都在 DATA_DIR 一个目录里。
- 首选:管理后台导出。Backup & Migration 页面生成的全量备份 ZIP 是引擎中立的(manifest + 每表一个 JSONL + 可选文件),不受"复制运行中的数据库文件"问题的影响,而且可以直接导入到 Postgres 部署。
- 冷备:先
docker compose stop,再整体拷贝DATA_DIR目录。注意aivory.db-wal和aivory.db-shm是数据库的一部分,不要在应用运行中只拷aivory.db单个文件,WAL 中未合并的写入会丢失,拷出来的文件也可能不一致。
备份导入的大小上限由 MAX_BACKUP_BYTES 控制(默认 20 GiB)。
与 Postgres 互迁
备份格式是引擎中立的:SQLite 备份可以导入 Postgres 部署,反之亦然,序列和外键由导入过程自动处理。典型的"业务长大了,从 SQLite 升到完整栈"流程:
- 旧实例导出:在管理后台 Backup & Migration 生成全量备份(勾选文件与向量,如有),下载 ZIP。
- 拉起新实例:按Docker Compose 生产部署启动完整栈。首次启动后,用与旧实例管理员相同的邮箱注册首个账号(它将成为管理员)。
- 导入:在新实例的 Backup & Migration 页面导入 ZIP,输入确认词
REPLACE。导入是整库替换:在一个事务里清空并重载所有表、恢复文件与向量,完成后当前会话失效,需用备份里的账号密码重新登录。 - 向量处理:如果旧实例没有 Qdrant(本页的最小部署就没有),备份里没有向量数据。新实例可在管理后台的向量维护功能里从数据库已存的 chunk 文本重建向量,无需原始文件,但会消耗嵌入 API 调用。
导入完成后有一道防提权机制:除了"执行导入操作的管理员邮箱"之外,备份中携带的其它 admin 账号会被降级为普通用户。新旧实例管理员邮箱一致,导入后你的管理员身份才能原样保留。
反方向(Postgres 迁到 SQLite,例如把小型部署收缩回单容器)流程完全相同,只是新实例换成本页的最小 compose 文件。
完整的导出选项、异步导出任务、配置导出与导入等细节见备份与迁移。