Actual Docker Compose 部署实战

Actual(v26.10.0)Docker Compose 部署实战:官方 actual-server 镜像选型与 digest、落地 Compose 编排、bootstrap 初始化密码、建预算记账端到端验证、/data 数据目录结构与环境变量/配置文件挂载,附 docker run 等价命令与 6 条排障实录。

Actual财务记账自托管Node.jsSQLite2026/10/28

Actual Docker Compose 部署实战

目标:从零开始在一台 Linux 服务器上用 Docker Compose 部署 Actual(Actual Budget)的 sync-server(同步服务器),完成初始化密码设置、第一个预算文件创建与记账验证,并沉淀数据目录结构与常见故障的实录解法。本文全部命令与回显均在 Ubuntu 24.04 实测环境逐条验证通过,实测版本 Actual v26.10.0。

一、环境信息

本次实测环境与关键产物信息如下,所有版本号为实查值:

  • 操作系统:Ubuntu 24.04(x86_64)
  • Docker:28.x 系列(含 Docker Compose v2 插件,docker compose 子命令可用)
  • Actual 版本:v26.10.0(官方 latest 标签镜像,镜像摘要 sha256:24645e971da6...;启动后经 API 实测确认 {"version":"26.10.0"})
  • 部署形态:单节点 sync-server + 内置 Web 客户端
  • 监听端口:宿主机 8129 → 服务端口 5006(5006 为 Actual 默认端口)
  • 数据目录:/opt/actual/data(映射到服务内 /data,该目录即全部持久化数据)
  • 示例密码:预算文件密码 q1w2e3r4(演示环境专用口令)
  • 查询/实测日期:2026-10-28

💡 说明:Actual 的组成是「客户端(浏览器/桌面 App)+ sync-server(同步服务器)」。用 Docker 部署时,一个实例同时提供 sync-server 接口与 Web 客户端静态页面,浏览器访问一个地址即可完成全部记账操作。

二、镜像选型说明

官方文档明确:Actual 服务器镜像同时发布在 Docker Hub(actualbudget/actual-server)与 GitHub Container Registry(ghcr.io/actualbudget/actual),两者功能一致,任选其一。镜像标签矩阵如下(官方文档口径):

标签说明适用场景
latest指向最近一次正式发布(本篇实测即 v26.10.0)推荐,生产自托管首选
latest-alpinelatest 的 Alpine 基础版,体积更小低功耗设备(NAS/树莓派)
nightlymaster 分支每次提交后自动构建尝鲜新特性,可能有缺陷
nightly-alpinenightly 的 Alpine 版同上,低功耗设备

选型建议:生产与长期使用一律 latest(或锁定到具体版本号如 actual-server:v26.10.0,升级时改标签);nightly 仅用于提前验证新特性且必须配好备份。

本篇实测取用 actualbudget/actual-server:latest,拉取后核对镜像摘要与关键元数据:

# 拉取官方镜像(网络不通时可走代理,见参考链接官方文档)
docker pull actualbudget/actual-server:latest

实测回显(输出节选):

latest: Pulling from actualbudget/actual-server
Digest: sha256:24645e971da6bb1a1f953d6859e307a0b29be1e8b4130a35b3220c209b5b860c
Status: Downloaded newer image for actualbudget/actual-server:latest
docker.io/actualbudget/actual-server:latest
# 核对入口进程、启动命令与默认端口
docker inspect actualbudget/actual-server:latest \
  --format '{{json .Config.Entrypoint}} | {{json .Config.Cmd}} | {{json .Config.ExposedPorts}} | {{json .Config.WorkingDir}}'

实测回显:

["/usr/bin/tini","-g","--"] | ["node","app.js"] | {"5006/tcp":{}} | "/app"

💡 说明:入口是 tini -g --(init 进程,负责把停止信号转发给整个进程组后优雅退出),启动命令 node app.js,默认暴露 5006 端口,工作目录 /app。这解释了为什么停止服务时日志会显示正常的退出流程——信号处理由 tini 托管。

三、前置条件

开始部署前确认以下条件全部满足:

  1. 服务器已安装 Docker 与 Compose 插件,且当前用户有 docker 权限:
docker version --format '{{.Server.Version}}' && docker compose version

实测回显:

28.3.3
Docker Compose version v2.38.2
  1. 端口未被占用:本篇用宿主机 8129 端口,确认无占用:
ss -ltn | grep ':8129' || echo 'port 8129 free'

实测回显:

port 8129 free
  1. 数据目录已创建(也可交给 Docker 自动创建,但显式创建便于控制属主):
mkdir -p /opt/actual/data
  1. 服务器时间准确:记账数据强依赖日期(交易、预算月份),NTP 已同步的服务器可避免"交易记到昨天"的问题。

四、部署步骤

本章从官方 Compose 编排出发生成可直接使用的配置文件,逐条执行并验证。

4.1 官方 Compose 编排原文

官方仓库 packages/sync-server/docker-compose.yml 提供了标准编排(以下为官方文件原文,2026-10-28 取自 master 分支):

services:
  actual_server:
    image: docker.io/actualbudget/actual-server:latest
    ports:
      # This line makes Actual available at port 5006 of the device you run the server on,
      # i.e. http://localhost:5006. You can change the first number to change the port, if you want.
      - '5006:5006'
    environment:
      # Uncomment any of the lines below to set configuration options.
      # - ACTUAL_HTTPS_KEY=/data/selfhost.key
      # - ACTUAL_HTTPS_CERT=/data/selfhost.crt
      # - ACTUAL_PORT=5006
      # - ACTUAL_UPLOAD_FILE_SYNC_SIZE_LIMIT_MB=20
      # - ACTUAL_UPLOAD_SYNC_ENCRYPTED_FILE_SYNC_SIZE_LIMIT_MB=50
      # - ACTUAL_UPLOAD_FILE_SIZE_LIMIT_MB=20
      # See all options and more details at https://actualbudget.org/docs/config/
      # !! If you are not using any of these options, remove the 'environment:' tag entirely.
    volumes:
      # Change './actual-data' below to the path to the folder you want Actual to store its data in on your server.
      # '/data' is the path Actual will look for its files in by default, so leave that as-is.
      - ./actual-data:/data
    healthcheck:
      # Enable health check for the instance
      test: ['CMD-SHELL', 'node scripts/health-check.js']
      # health check using self signed certs
      # test: ['CMD-SHELL', 'NODE_EXTRA_CA_CERTS=/data/selfhost.crt node scripts/health-check.js']
      interval: 60s
      timeout: 10s
      retries: 3
      start_period: 20s
    restart: unless-stopped

关键点解读:服务只挂一个卷(宿主数据目录 → /data),官方健康检查命令是 node scripts/health-check.js(镜像内自带该脚本),重启策略 unless-stopped。官方文件默认把 5006:5006 直接暴露,生产环境通常要改宿主端口。

4.2 落地本次实测的 Compose 文件

结合实测环境做三处适配:宿主端口改 8129、数据目录落 /opt/actual/data、显式指定项目名与独立网络(避免与其他服务同名互删)。在工作目录创建 /opt/actual/docker-compose.yml:

name: actual-v26.10.0-stack

services:
  actual-v26.10.0-server:
    image: actualbudget/actual-server:latest
    container_name: actual-v26.10.0-server
    ports:
      - '8129:5006'
    environment:
      - ACTUAL_HOSTNAME=0.0.0.0
      - ACTUAL_PORT=5006
    volumes:
      - /opt/actual/data:/data
    networks:
      - actual-net
    healthcheck:
      test: ['CMD-SHELL', 'node scripts/health-check.js']
      interval: 60s
      timeout: 10s
      retries: 3
      start_period: 20s
    restart: unless-stopped

networks:
  actual-net:
    name: actual-v26.10.0-net
    driver: bridge
    ipam:
      config:
        - subnet: 10.89.56.0/24

逐项说明:

  • name: actual-v26.10.0-stack:Compose 项目名显式固化,避免目录名冲突导致误操作其他项目;
  • ACTUAL_HOSTNAME=0.0.0.0:显式监听所有网卡(默认值 :: 在多数系统同样可达,写显更便于排查);
  • ports: '8129:5006':宿主 8129 → 服务 5006,浏览器访问 http://<服务器IP>:8129;
  • volumes: /opt/actual/data:/data:全部持久化数据的唯一落点,备份只需备份这个目录;
  • subnet: 10.89.56.0/24:显式网段,防止与其他 Docker 网络的地址池冲突(按服务器实际空闲网段调整)。

4.3 启动与状态确认

cd /opt/actual && docker compose up -d

实测回显:

 Network actual-v26.10.0-net Created
 Container actual-v26.10.0-server Creating
 Container actual-v26.10.0-server Created
 Container actual-v26.10.0-server Starting
 Container actual-v26.10.0-server Started

等待约 10 秒后确认状态(官方健康检查 start_period: 20s 内会完成首次探测):

cd /opt/actual && docker compose ps

实测回显:

NAME                     IMAGE                               COMMAND                  SERVICE                  CREATED         STATUS                   PORTS
actual-v26.10.0-server   actualbudget/actual-server:latest   "/usr/bin/tini -g --…"   actual-v26.10.0-server   8 seconds ago   Up 8 seconds (healthy)   0.0.0.0:8129->5006/tcp, [::]:8129->5006/tcp

STATUS 列出现 (healthy) 即官方健康检查已通过。

4.4 启动日志与 HTTP 探活

cd /opt/actual && docker compose logs --no-color

实测回显(首次启动完整日志):

actual-v26.10.0-server  | Checking if there are any migrations to run for direction "up"...
actual-v26.10.0-server  | Migrations: DONE
actual-v26.10.0-server  | Running in production mode - Serving static React app
actual-v26.10.0-server  | Listening on 0.0.0.0:5006...

四行日志分别对应:数据库迁移检查(首次启动自动执行迁移)→ 生产模式 → 静态 Web 页面 → 监听就绪。随后 HTTP 探活:

curl -sS -o /dev/null -w 'GET / -> %{http_code} (%{size_download} bytes)\n' http://127.0.0.1:8129/
curl -sS -w '\nGET /health -> %{http_code}\n' http://127.0.0.1:8129/health
curl -sS -o /dev/null -w 'GET /api/health -> %{http_code}\n' http://127.0.0.1:8129/api/health

实测回显:

GET / -> 200 (4256 bytes)
{"status":"UP"}
GET /health -> 200
GET /api/health -> 200

三个探活点全部 200:/ 是 Web 客户端首页(<title>Actual</title>),/health 与 /api/health 返回 {"status":"UP"}。浏览器打开 http://<服务器IP>:8129/ 应能看到 Actual 的登录/初始化界面。

4.5 初始化:设置服务器密码(bootstrap)

首次访问时服务器处于"未引导"状态,通过官方接口确认并完成初始化(以下步骤与浏览器界面的初始化向导等价,全部为 sync-server 官方 API):

# 查询引导状态
curl -sS http://127.0.0.1:8129/account/needs-bootstrap; echo

实测回显(全新实例):

{"status":"ok","data":{"bootstrapped":false,"loginMethod":"password","availableLoginMethods":[],"multiuser":false}}

bootstrapped:false 表示还没有设置过密码。此时直接登录会失败,正好验证了错误路径:

# 未 bootstrap 时直接登录
curl -sS -X POST http://127.0.0.1:8129/account/login \
  -H 'Content-Type: application/json' -d '{"password":"q1w2e3r4"}'; echo

实测回显:

{"status":"error","reason":"invalid-password"}

⚠️ 注意:看到 invalid-password 不要误以为密码错了——在全新实例上这是"服务器尚未初始化密码"的表现,正确做法是先走 bootstrap。

执行 bootstrap 设置初始密码(本示例用演示口令 q1w2e3r4,生产环境请换强密码):

curl -sS -X POST http://127.0.0.1:8129/account/bootstrap \
  -H 'Content-Type: application/json' -d '{"password":"q1w2e3r4"}'; echo

实测回显:

{"status":"ok","data":{"token":"d43b7717-e7a7-409e-a74b-9df059ee78ae"}}

bootstrap 成功并直接返回会话 token。再次登录可拿到相同 token(登录路径验证):

curl -sS -X POST http://127.0.0.1:8129/account/login \
  -H 'Content-Type: application/json' -d '{"password":"q1w2e3r4"}'; echo

实测回显:

{"status":"ok","data":{"token":"d43b7717-e7a7-409e-a74b-9df059ee78ae"}}
curl -sS http://127.0.0.1:8129/account/needs-bootstrap; echo

实测回显:

{"status":"ok","data":{"bootstrapped":true,"loginMethod":"password","availableLoginMethods":[{"method":"password","active":1,"displayName":"Password"}],"multiuser":false}}

bootstrapped:true 且登录方式列表中出现 password——服务器密码已生效。此后浏览器访问 http://<服务器IP>:8129/,输入该密码即可进入工作区;API/桌面客户端连接时也使用同一密码。

4.6 创建第一个预算文件并验证端到端可用

部署完成的最终验证是"真的能记账"。Actual 官方提供 Node.js API 客户端库 @actual-app/api(版本与服务器严格同步),用它完成"建预算 → 建分类 → 建账户 → 记一笔 → 查询"的闭环。

准备一个工作目录并安装客户端库(版本号与服务器一致,26.10.0):

mkdir -p /opt/actual/api-test && cd /opt/actual/api-test
npm init -y && npm install @actual-app/api@26.10.0

实测回显(输出节选):

added 1 package in 4s

⚠️ 注意:@actual-app/api 的版本必须与 sync-server 版本一致(v26.10.0 对 26.10.0),版本错配会在连接同步时出现协议不兼容报错。同时该库依赖原生模块 better-sqlite3,运行机器的 libstdc++ 需较新(见 11.3 排障实录)。

编写初始化脚本 /opt/actual/api-test/setup-budget.js:

const actual = require('@actual-app/api');

(async () => {
  await actual.init({
    serverURL: 'http://127.0.0.1:8129',
    password: 'q1w2e3r4',
    dataDir: '/opt/actual/api-test/local-data',   // 客户端本地缓存目录,需预先存在
  });

  // 1. 创建预算文件(v26.10.0 中新建预算走 runImport,空回调即空预算)
  await actual.runImport('家庭预算2026', async () => {});
  console.log('budgets:', JSON.stringify(await actual.getBudgets()));

  // 2. 建分类组与分类
  const groupId = await actual.createCategoryGroup({ name: '日常支出' });
  const catFood = await actual.createCategory({ name: '餐饮', group_id: groupId, is_income: false, hidden: false });

  // 3. 建账户(期初余额 100.00 元,金额单位为"分")
  const acc = await actual.createAccount({ name: '现金钱包', offbudget: false, closed: false }, 10000);

  // 4. 记一笔支出(-28.50 元,分类"餐饮")
  const r = await actual.addTransactions(acc, [
    { date: '2026-10-28', payee_name: '楼下超市', category: catFood, amount: -2850, notes: '买菜' },
  ]);
  console.log('addTransactions:', JSON.stringify(r));

  // 5. 查询验证
  const txns = await actual.getTransactions(acc, '2026-10-01', '2026-10-31');
  console.log('txn count:', txns.length);
  console.log('balance(cent):', await actual.getAccountBalance(acc));

  await actual.shutdown();
})().catch(e => { console.error('FATAL:', e.message); process.exit(1); });

执行:

cd /opt/actual/api-test && node setup-budget.js

实测回显(输出节选,已略去库内部加载日志):

budgets: [{"id":"----2026-09e3d87","cloudFileId":"29b70409-25fa-47ba-b410-6c553ed0a32a","groupId":"caa22e62-9d2e-48d9-a781-b2b1455e38a1","name":"家庭预算2026"}, ...]
addTransactions: "ok"
txn count: 2
balance(cent): 7150

逐项解读:预算文件「家庭预算2026」已创建并同步到服务器(cloudFileId 即预算文件在服务器侧的 ID);记账返回 ok;账户里出现 2 笔交易(1 笔期初余额 + 1 笔买菜支出);余额 7150 分 = 期初 100.00 − 支出 28.50 = 71.50 元,数字自洽。至此部署的端到端验证闭环完成。

⚠️ 注意:runImport 每执行一次都会新建一个预算文件,重复运行脚本会出现多个同名预算(见 11.4)。重跑数据初始化前,先在 Web 界面设置里删除旧预算文件或更换预算名。

五、数据持久化说明

Actual 的持久化设计非常简单:一个数据目录就是全部数据。首次启动与建预算后实查 /opt/actual/data:

find /opt/actual/data -maxdepth 2 | sort

实测回显(初始化预算后):

/opt/actual/data
/opt/actual/data/.migrate
/opt/actual/data/server-files
/opt/actual/data/server-files/account.sqlite
/opt/actual/data/user-files
/opt/actual/data/user-files/file-29b70409-25fa-47ba-b410-6c553ed0a32a.blob
/opt/actual/data/user-files/group-caa22e62-9d2e-48d9-a781-b2b1455e38a1.sqlite

逐项说明:

路径作用
.migrate数据库迁移标记文件
server-files/account.sqlite服务器账号库:密码哈希、预算文件清单、会话 token
user-files/file-<cloudFileId>.blob预算文件本体(二进制 blob),每个预算文件一个
user-files/group-<groupId>.sqlite同步组数据库,支撑多端消息式同步

💡 说明:备份策略由此直接得出——打包 /opt/actual/data 整个目录即完整备份(含账号与全部预算文件);恢复时停服、放回目录、重启即可。实际备份演练见本专栏运维篇。

验证"数据真的落在宿主目录":停掉服务再启动,预算仍在。

cd /opt/actual && docker compose restart && sleep 5 \
  && curl -sS http://127.0.0.1:8129/account/needs-bootstrap; echo

实测回显:

 actual-v26.10.0-server Restarting
 actual-v26.10.0-server Started
{"status":"ok","data":{"bootstrapped":true,...}}

重启后 bootstrapped:true 保持,账号与预算数据完好。

六、环境变量表

Actual 服务端的全部配置均可通过环境变量注入(优先级:环境变量 > config.json > 默认值)。以下为常用项(官方配置文档口径,标注默认值):

环境变量默认值说明
ACTUAL_PORT5006监听端口
ACTUAL_HOSTNAME::监听地址(含 IPv4/IPv6)
ACTUAL_DATA_DIR/data(存在时)数据总目录,其下派生 server-files/user-files
ACTUAL_SERVER_FILES/data/server-files服务器账号库目录
ACTUAL_USER_FILES/data/user-files预算文件目录
ACTUAL_CONFIG_PATH/data/config.json(存在时)config.json 路径
ACTUAL_WEB_ROOT包内 @actual-app/web前端静态资源目录(自定义前端用)
ACTUAL_UPLOAD_FILE_SIZE_LIMIT_MB20普通上传文件大小上限(MB)
ACTUAL_UPLOAD_FILE_SYNC_SIZE_LIMIT_MB20同步文件大小上限(MB)
ACTUAL_UPLOAD_SYNC_ENCRYPTED_FILE_SYNC_SIZE_LIMIT_MB50加密同步文件大小上限(MB)
ACTUAL_HTTPS_KEY / ACTUAL_HTTPS_CERT无HTTPS 私钥/证书路径
ACTUAL_LOGIN_METHODpassword登录方式:password/header/openid
ACTUAL_ALLOWED_LOGIN_METHODSpassword,header,openid允许的登录方式(逗号分隔)
ACTUAL_TRUSTED_PROXIES内网段列表信任的反向代理网段(影响限流的客户端 IP 判定)
ACTUAL_OPENID_CLIENT_SECRET_FILE无以文件形式注入 OpenID 密钥(_FILE 后缀优先)
ACTUAL_SIMPLEFIN_ALLOWED_NETWORKS无允许 SimpleFIN 访问的内网桥地址

💡 技术拓展:Actual 的登录认证不止密码一种。loginMethod 三种可选方式对比——

方式原理适用场景
password(默认)按预算文件密码校验个人自托管标准形态
header网关注入 x-actual-password 头自动登录已有统一认证入口,由反代注入凭据(官方提示配错有安全风险)
openid对接 OIDC 身份提供方(预览特性)团队/家庭统一账号体系

本文实测采用默认 password;无论哪种方式,都建议通过 ACTUAL_ALLOWED_LOGIN_METHODS 显式收敛可用方式。

七、配置参数说明

环境变量之外,Actual 支持 config.json 配置文件:启动时按 ACTUAL_CONFIG_PATH(未设置则在 /data 或安装目录)查找。键名与环境变量的对应关系是 camelCase ↔ ACTUAL_ 大写(如 dataDir ↔ ACTUAL_DATA_DIR),但并非一一对应(如 https 是对象键,只能写进 config.json)。三层优先级:环境变量 > config.json > 默认值。

一个最小 config.json 示例(放在 /opt/actual/data/config.json,与数据同目录持久化):

{
  "port": 5006,
  "hostname": "0.0.0.0",
  "upload": {
    "fileSizeLimitMB": 25,
    "fileSyncSizeLimitMB": 20,
    "syncEncryptedFileSizeLimitMB": 50
  }
}

逐项说明:port/hostname 与环境变量等价(二选一即可,同时设置时环境变量生效);upload 组键对应三个上传上限环境变量,调大 fileSizeLimitMB 的典型场景是导入超大的 OFX/CSV 银行导出文件。

验证配置生效最直接的方式是看启动日志中的监听地址变化(例如把端口改成 5007 后,日志会显示 Listening on 0.0.0.0:5007...)。

八、配置文件挂载

把 config.json 与数据目录一起挂载进服务即可生效。在 Compose 文件的 volumes 中追加一行:

    volumes:
      - /opt/actual/data:/data
      - /opt/actual/config.json:/data/config.json:ro

说明:ro(只读)防止服务端改写配置;config.json 属于配置而非数据,挂载为只读更安全。修改配置后执行 docker compose restart 生效,并观察日志确认新值被加载:

cd /opt/actual && docker compose restart && docker compose logs --no-color | tail -5

实测回显形态(端口场景示例):

actual-v26.10.0-server  | Running in production mode - Serving static React app
actual-v26.10.0-server  | Listening on 0.0.0.0:5006...

九、等价 docker run 命令

不用 Compose 时,官方文档给出的等价启动命令为(官方原文,路径占位按本篇环境代入):

docker run --pull=always --restart=unless-stopped -d \
  -p 8129:5006 \
  -v /opt/actual/data:/data \
  --name my_actual_budget \
  actualbudget/actual-server:latest

逐参数说明:

  • --pull=always:每次启动都拉取最新镜像(生产建议去掉,避免升级不可控);
  • --restart=unless-stopped:崩溃自动拉起,人工停止除外;
  • -d:后台运行;
  • -p 8129:5006:宿主端口映射;
  • -v /opt/actual/data:/data:数据目录持久化(必须挂,否则重建后数据全丢);
  • --name my_actual_budget:实例名。

对应的官方升级流程为「停止 → 删除 → 重新 run」三连(数据在宿主目录,删除实例不影响数据)。日常运维更推荐 Compose 方式(docker compose pull && docker compose up -d 一条完成)。

十、常用运维命令

以下命令均在 /opt/actual(Compose 文件所在目录)执行:

# 查看实例状态(healthy 为健康)
docker compose ps
# 查看日志(实时跟踪加 -f)
docker compose logs --no-color | tail -20
# 重启服务(改配置后)
docker compose restart
# 升级到最新版(官方推荐姿势)
docker compose pull && docker compose up -d
# 停止与启动
docker compose stop && docker compose start
# 彻底移除实例(数据目录 /opt/actual/data 保留)
docker compose down

升级后验证版本(两步:探活 + API 查版本):

curl -sS http://127.0.0.1:8129/health; echo
node -e "require('@actual-app/api').init({serverURL:'http://127.0.0.1:8129',password:'q1w2e3r4',dataDir:'/opt/actual/api-test/local-data'}).then(async a=>{console.log(await require('@actual-app/api').getServerVersion());await require('@actual-app/api').shutdown()})"

实测回显:

{"status":"UP"}
{"version":"26.10.0"}

💡 说明:升级后若 @actual-app/api 报同步协议错误,通常是客户端库与新服务器版本不一致——把库升级到与服务器相同的版本号即可(npm install @actual-app/api@<新版本>)。

十一、常见问题(FAQ)

11.1 登录报 invalid-password,但密码明明是对的

现象:全新实例上 POST /account/login 返回 {"status":"error","reason":"invalid-password"}(本篇 4.5 实测)。

原因:服务器从未 bootstrap,账号库里没有任何密码记录。

解法:先执行 bootstrap(4.5 的 POST /account/bootstrap)设置初始密码,之后 login 正常。浏览器界面上这一步对应首次访问的"设置密码"向导。

11.2 启动失败:端口被占用

现象:docker compose up -d 报端口绑定失败(bind: address already in use)。

定位:ss -ltnp | grep ':8129' 找到占用进程。

解法:换宿主端口(改 Compose 中 '8129:5006' 左侧数字)或停掉占用进程后重新 docker compose up -d。

11.3 @actual-app/api 运行报 GLIBCXX_3.4.29 not found

现象:在服务器上运行 API 客户端脚本时报错(实测原文):

[Exception] Error: libstdc++.so.6: version `GLIBCXX_3.4.29' not found (required by node_modules/better-sqlite3/prebuilds/linux-x64.node)
    code: 'ERR_DLOPEN_FAILED'

原因:@actual-app/api 依赖原生模块 better-sqlite3,其预编译产物要求较新的 libstdc++(GLIBCXX_3.4.29+);运行环境的 LD_LIBRARY_PATH 或系统库过旧时动态加载失败。

解法(实测有效):确认系统自带 libstdc++ 含该符号(strings /usr/lib/x86_64-linux-gnu/libstdc++.so.6 | grep GLIBCXX_3.4.29),再以系统库路径运行脚本:

LD_LIBRARY_PATH=/usr/lib/x86_64-linux-gnu node setup-budget.js

若系统库本身过旧,则升级 libstdc++6 软件包,或在 API 工作目录重编译原生模块(npm rebuild better-sqlite3,需 g++/make/python3)。

11.4 服务器上出现多个同名预算文件

现象:getBudgets() 或 Web 界面预算列表里出现多个「家庭预算2026」。

原因:新建预算的操作(API runImport 或界面"创建新预算文件")每次都会生成一个新预算文件,重跑初始化脚本就会多出一个。

解法:确认要保留的预算后,在 Web 界面「设置 → 高级」中删除多余预算文件;或查询各预算的 syncId,备份后只保留一个。预防:初始化脚本只在空服务器上跑一次,之后用 downloadBudget 拉取既有预算。

11.5 API 脚本报 Budget "..." not found

现象(实测原文):

Budget "00000000-0000-0000-0000-000000000000" not found. Check the sync id of your budget in the Advanced section of the settings page.

原因:downloadBudget 传入的 syncId 在服务器上不存在(空服务器或 ID 写错)。

解法:先 getBudgets() 列出服务器上真实预算拿到 ID;空服务器则先用 runImport(或 Web 界面)创建预算。

11.6 健康检查一直 unhealthy

现象:docker compose ps 中状态为 unhealthy 或 starting 久久不变。

定位:docker compose logs --no-color | tail -20 看是否卡在迁移(Migrations 行未出现 DONE)或监听失败;curl http://127.0.0.1:<端口>/health 人工复现健康检查。

解法:多为数据目录属主不对(容器内进程写不了 /data)——chown -R 1000:1000 /opt/actual/data 后重启;或迁移被中断(删掉 .migrate 后重启让其重跑)。

十二、参考链接