Actual 常用命令与运维实战

Actual sync-server(v26.10.0)运维实战:启停与日志监控、版本查询与升级流程、多预算文件管理(fileId/groupId/Sync ID 辨析)、双客户端端到端同步实测闭环、目录级备份与恢复演练、exportBudget 导出、API/CLI/HTTP 接口速查与 6 条排障实录。

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

Actual 常用命令与运维实战

目标:给出 Actual sync-server(v26.10.0)日常运维的完整命令集——服务启停与日志、版本升级、多预算文件管理、双客户端端到端同步实测、备份与恢复演练,以及 API/CLI 命令速查与排障实录。全部命令均在实测环境逐条验证(实测日期 2026-10-28);部署与配置细节见 《Actual Docker Compose 部署实战》、《Actual 配置参数详解》。

一、环境信息

  • 组件版本:Actual sync-server v26.10.0(latest 标签,服务器与 API 客户端库版本严格一致)
  • 实测环境:Ubuntu 24.04(x86_64),Node.js 24(API 客户端运行时)
  • 服务地址:http://<服务器IP>:8129(默认端口 5006,本环境映射为 8129)
  • 示例预算:「家庭预算2026」(含账户"现金钱包"、分类"日常支出/餐饮、交通")
  • 示例密码:q1w2e3r4(演示口令)
  • API 客户端:@actual-app/api@26.10.0(npm 安装,版本须与服务器一致)

💡 说明:运维 Actual 的抓手有两层——服务进程运维(日志/升级/数据目录)与预算数据运维(多预算管理/备份/同步验证)。数据层的黄金法则是:user-files 目录(预算文件 blob + 同步组库)加 server-files/account.sqlite(账号密码库)就是全部状态,备份即拷贝目录。

二、服务启停与状态命令

2.1 CLI 部署形态(systemd / 裸进程)

sync-server 以 NPM 包 @actual-app/sync-server 分发(要求 Node.js ≥ 22),进程级运维命令如下:

# 启动(前台验证配置时)
actual-server --config /srv/actual/config.json

# systemd 部署时的标准生命周期
systemctl start actual
systemctl stop actual
systemctl restart actual      # 改配置后重启生效
systemctl status actual       # 运行状态(active/running 即健康)

# 版本查询
actual-server --version

服务就绪的判定信号是启动日志监听行与健康端点(两章命令均实测可用):

curl -sS http://127.0.0.1:8129/health
{"status":"UP"}

2.2 Compose 部署形态

按 《Actual Docker Compose 部署实战》 部署时,启停与状态命令为:

cd /opt/actual
docker compose ps              # STATUS 列 healthy 即健康
docker compose restart         # 重启服务
docker compose stop && docker compose start

docker compose ps 实测回显形态:

NAME                     IMAGE                               STATUS
actual-v26.10.0-server   actualbudget/actual-server:latest   Up 22 minutes (healthy)

三、日志与监控

3.1 日志观测点

sync-server 日志行与含义(实测归纳):

日志行含义运维关注点
Checking if there are any migrations to run for direction "up"...启动时检查数据库迁移每次升级后必然出现
Migrations: DONE迁移完成卡在此行之前说明迁移失败,查数据目录可写性
Running in production mode - Serving static React app生产模式 + 前端静态资源出现即为正常
Listening on 0.0.0.0:5006...监听就绪核对端口/地址是否符合预期
Syncing since <时间戳>(客户端侧)客户端与服务器增量同步同步排障时观察游标推进

运行期日志的核心健康信号是安静——正常同步流量几乎不产生服务端日志;大量重复的同步错误日志通常意味着客户端与服务端版本不匹配。

3.2 健康监控命令

适合放监控定时任务的三个探针(全部实测回显):

# 1. 存活与健康
curl -sf http://127.0.0.1:8129/health
# {"status":"UP"}

# 2. 引导状态(可顺带验证登录方式配置)
curl -sf http://127.0.0.1:8129/account/needs-bootstrap
# {"status":"ok","data":{"bootstrapped":true,...}}

# 3. 数据目录增长监控(预算文件 blob 大小变化即数据在增长)
du -sb /srv/actual/user-files | awk '{print $1}'

四、版本查询与升级

4.1 版本核对(服务端 + 客户端双侧)

升级前后都要核对两侧版本一致:

# 服务端:CLI 形态
actual-server --version
# Compose 形态:探活后用 API 核对(见下)

# 客户端库版本
npm ls @actual-app/api

API 实测核对服务端版本(脚本片段):

await actual.init({ serverURL: 'http://127.0.0.1:8129', password: 'q1w2e3r4', dataDir: '...' });
console.log(JSON.stringify(await actual.getServerVersion()));

实测回显:

{"version":"26.10.0"}

4.2 升级流程

CLI 形态(官方文档口径):

npm update -g @actual-app/sync-server
systemctl restart actual

Compose 形态(官方升级姿势,实测执行):

docker compose pull && docker compose up -d

实测回显(已是最新版时的形态):

Image actualbudget/actual-server:latest Pulled
Container actual-v26.10.0-server Running
{"status":"UP"}

升级后验证三步(实测清单):

  1. curl http://127.0.0.1:8129/health → {"status":"UP"};
  2. 启动日志出现 Migrations: DONE(新版本迁移自动执行);
  3. API getServerVersion() 返回新版本号,且 @actual-app/api 同步升级到相同版本。

⚠️ 注意:升级前必做备份(第六章目录打包一条命令即可);@actual-app/api 与服务器版本错配是升级后最常见的故障(同步协议错误),两侧一起升。

五、多预算文件管理

Actual 支持一个服务器上管理多个预算文件(如"家庭预算2026""2027 年度预算"并存)。实测环境中有两个同名预算(运维演练产物),正好用于管理演示。

5.1 查看预算清单

API 实测(getBudgets,输出节选):

console.log(JSON.stringify(await actual.getBudgets(), null, 1));
[
 {
  "cloudFileId": "29b70409-25fa-47ba-b410-6c553ed0a32a",
  "groupId": "caa22e62-9d2e-48d9-a781-b2b1455e38a1",
  "name": "家庭预算2026",
  ...
 },
 {
  "cloudFileId": "29ee16e1-2e38-41b6-9138-edcb9db4920e",
  "name": "家庭预算2026",
  "state": "remote",
  ...
 }
]

服务端等价 HTTP 接口实测(GET + 两个头):

curl -sS http://127.0.0.1:8129/sync/list-user-files -H 'X-ACTUAL-TOKEN: <token>'
{"status":"ok","data":[{"deleted":0,"fileId":"29ee16e1-...","groupId":"c74f8121-...","name":"家庭预算2026",...},{"deleted":0,"fileId":"29b70409-...","groupId":"caa22e62-...","name":"家庭预算2026",...}]}

三个 ID 的角色辨析(实测结论,重要):

ID作用用在哪
fileId(cloudFileId)预算文件在服务器侧的文件 ID服务器文件管理(如 get-user-file-info)
groupId(同步组 ID)多端同步的组标识,即 Web 界面设置里显示的 Sync ID客户端 downloadBudget 的参数
本地 id客户端本地预算目录名仅本地

⚠️ 注意:downloadBudget 的参数名是 syncId,匹配的是 groupId 而非 cloudFileId——传 cloudFileId 会报 not found(实测踩中,见第十章排障实录)。这是文档级易错点。

5.2 新建与切换预算

  • 新建:Web 界面「创建新预算文件」或 API runImport(name, fn)(每次调用都新建,重复执行会产生多个同名预算);
  • 切换:客户端 downloadBudget(groupId) 拉取目标预算;Web 界面右上角文件菜单切换;
  • 删除:v26.10.0 的 @actual-app/api 没有删除预算的方法(65 个导出方法实测核对),删除走 Web 界面「设置 → 高级 → 预算管理」。删除只摘除服务器记录,user-files 下的 blob 物理文件建议运维定期清点。

5.3 数据目录视角的预算管理

# 实查预算文件与同步组库(-h 人类可读)
ls -lh /srv/actual/user-files/

实测回显:

-rw-r--r-- 1 root root 12K Oct 28 11:05 file-29b70409-25fa-47ba-b410-6c553ed0a32a.blob
-rw-r--r-- 1 root root 12K Oct 28 11:04 file-29ee16e1-2e38-41b6-9138-edcb9db4920e.blob
-rw-r--r-- 1 root root 24K Oct 28 11:07 group-caa22e62-9d2e-48d9-a781-b2b1455e38a1.sqlite

file-*.blob 与预算一一对应;group-*.sqlite 每个同步组一个。孤儿 blob(服务器清单里没有对应 fileId)可用脚本比对清理。

六、端到端同步实测(第二客户端接入)

local-first 架构的核心运维验证:第二个客户端能否完整接入并双向同步。实测按"下载 → 一致性 → 反向变更 → 回流验证"四步闭环。

6.1 第二客户端下载同一预算

全新客户端(独立本地缓存目录)接入实测脚本 client2-sync.js:

const actual = require('@actual-app/api');
const SYNC_ID = 'caa22e62-9d2e-48d9-a781-b2b1455e38a1'; // 预算的 groupId(Web 设置页可见)

(async () => {
  await actual.init({
    serverURL: 'http://127.0.0.1:8129',
    password: 'q1w2e3r4',
    dataDir: '/srv/actual/client2-data',   // 全新目录,需预先创建
  });
  await actual.downloadBudget(SYNC_ID);
  const accs = await actual.getAccounts();
  console.log('accounts:', accs.map(a => a.name));
  const txns = await actual.getTransactions(accs[0].id, '2026-10-01', '2026-10-31');
  console.log('txn count:', txns.length, '| balance:', await actual.getAccountBalance(accs[0].id));
  await actual.shutdown();
})().catch(e => { console.error('FATAL:', e.message); process.exit(1); });

实测回显:

accounts: [ '现金钱包' ]
txn count: 2 | balance: 7150

第二客户端下载到的数据与第一客户端逐字节一致(账户、交易数、余额 7150 全对上)——同步基础设施正常。

6.2 第二客户端记账 → 第一客户端验证回流

在第二客户端追加一笔交易(-5.00 元地铁):

await actual.addTransactions(accs[0].id, [
  { date: '2026-10-28', payee_name: '地铁', amount: -500, notes: 'client2 记一笔' },
]);
console.log('new balance:', await actual.getAccountBalance(accs[0].id));

实测回显:

new balance: 6650

第一客户端(原目录)重新连接并拉取增量,实测回显(交易列表节选):

client1 txn count: 3
[
 { "payee": "地铁", "amount": -500, "notes": "client2 记一笔" },
 { "payee": "楼下超市", "amount": -2850, "notes": "买菜" },
 ...
]
client1 balance: 6650

闭环结论:client-2 记的「地铁 -500」完整回流到 client-1(金额、收款人、备注原样传递),余额两端一致(6650)。这就是"本地优先 + 消息式同步"的运维验证形态——Web 界面与桌面 App 接入自建服务器的行为与此等价(填服务器地址 + 密码 → 下载预算 → 自动增量同步)。

💡 说明:日常运维中验证同步是否健康,用同样思路即可:两端各改一笔,看另一端余额是否一致。同步游标异常时优先核对两端版本号。

七、备份策略与恢复演练

7.1 备份对象与粒度

粒度内容命令适用
目录级(推荐主备份)server-files + user-files 全部状态tar czf ... 一条命令服务器整体备份/迁移/升级前
单预算导出单个预算的 zip(db.sqlite + metadata.json)API exportBudget()单预算版本化、跨服务器迁移、给人肉管报

目录级备份实测:

cd /srv/actual && tar czf data-backup-$(date +%Y%m%d).tar.gz server-files user-files

实测回显(产物):

-rw-r--r-- 1 root root 27870 Oct 28 11:27 data-backup-20261028.tar.gz

💡 说明:备份前先 stop 或确保无写入(或接受文件级快照的微小不一致风险——SQLite blob 在无写入的瞬间是自洽的)。生产建议加 --warning=no-file-changed 并在低峰执行;有条件用文件系统快照更稳。

单预算导出实测(exportBudget 返回 zip 字节流):

const fs = require('fs');
await actual.downloadBudget(SYNC_ID);
const data = await actual.exportBudget();
fs.writeFileSync('/srv/actual/backup-家庭预算2026.zip', Buffer.from(data));

实测回显(产物核验):

exported bytes: 18152
zip entries: ['db.sqlite', 'metadata.json']

导出的 zip 与 Web 界面「导出备份」产物同构,可在任何 Actual 实例(含官方演示站)导入恢复。

7.2 恢复演练(实测)

演练设计(零风险方式):把 7.1 的备份包解压到独立目录,以独立实例挂载启动,验证账号、密码与预算完整可用——效果等价于"原目录丢失后从备份重建",且不触碰生产数据。

mkdir -p /srv/restore-drill && tar xzf data-backup-20261028.tar.gz -C /srv/restore-drill
find /srv/restore-drill -maxdepth 2 | sort

实测回显:

/srv/restore-drill
/srv/restore-drill/.migrate
/srv/restore-drill/server-files
/srv/restore-drill/server-files/account.sqlite
/srv/restore-drill/user-files
/srv/restore-drill/user-files/file-29b70409-25fa-47ba-b410-6c553ed0a32a.blob
/srv/restore-drill/user-files/file-29ee16e1-2e38-41b6-9138-edcb9db4920e.blob
/srv/restore-drill/user-files/group-caa22e62-9d2e-48d9-a781-b2b1455e38a1.sqlite

以恢复目录启动验证实例后,逐项验证(实测回显):

# 1. 引导状态 —— 账号库随备份回来了
curl -sS http://127.0.0.1:8135/account/needs-bootstrap
{"status":"ok","data":{"bootstrapped":true,...}}
# 2. 原密码登录 —— 密码哈希完好,token 与原实例一致
curl -sS -X POST http://127.0.0.1:8135/account/login \
  -H 'Content-Type: application/json' -d '{"password":"q1w2e3r4"}'
{"status":"ok","data":{"token":"d43b7717-e7a7-409e-a74b-9df059ee78ae"}}
# 3. 预算清单完整
curl -sS http://127.0.0.1:8135/sync/list-user-files -H 'X-ACTUAL-TOKEN: <token>'
[('家庭预算2026', '29ee16e1-...'), ('家庭预算2026', '29b70409-...')]

演练结论:目录级备份完整覆盖"账号 + 密码 + 预算 + 同步组"全部状态;恢复 = 解包 + 启动,无需重新 bootstrap、无需重建预算。生产恢复流程:停服务 → 备份目录解压回数据目录(属主对齐)→ 启动 → 走 7.2 三步验证。

八、API / CLI 命令速查

8.1 API 客户端常用命令(@actual-app/api,实测可用)

const actual = require('@actual-app/api');
// 连接(dataDir 须预先存在)
await actual.init({ serverURL, password, dataDir });
// 版本/清单
await actual.getServerVersion();   // {"version":"26.10.0"}
await actual.getBudgets();         // 预算清单(本地+remote)
await actual.downloadBudget(groupId);           // 按 Sync ID 下载/切预算
await actual.runImport(name, fn);               // 新建预算
await actual.exportBudget();       // 导出当前预算 zip
// 数据操作(金额单位:分)
await actual.getAccounts();
await actual.getAccountBalance(accountId);
await actual.addTransactions(accountId, [ { date, payee_name, category, amount, notes } ]);
await actual.getTransactions(accountId, 'YYYY-MM-DD', 'YYYY-MM-DD');
await actual.getBudgetMonth('YYYY-MM');        // 月度预算视图
await actual.createCategoryGroup({ name });
await actual.createCategory({ name, group_id, is_income: false, hidden: false });
await actual.shutdown();

8.2 服务端 HTTP 接口速查(实测核对)

接口方法用途
/health、/api/healthGET存活探针
/account/needs-bootstrapGET引导状态/登录方式
/account/bootstrapPOST首次设密码 {password}
/account/loginPOST登录换 token {password}
/sync/list-user-filesGET(头 X-ACTUAL-TOKEN)预算文件清单
/sync/get-user-file-infoGET(头 X-ACTUAL-FILE-ID)单文件元信息
/sync/download-user-fileGET下载预算 blob
/sync/upload-user-filePOST上传/新建预算

⚠️ 注意:文件类接口在 /sync 前缀下(/sync/list-user-files),直接打根路径会命中前端 SPA fallback 返回 HTML(实测踩中,见第十章)。

8.3 CLI 工具命令(actual-server)

actual-server                     # 默认配置启动
actual-server --config <path>     # 指定 config.json
actual-server --reset-password    # 交互式重置服务器密码
actual-server --version           # 版本号
npm update -g @actual-app/sync-server   # 升级
npm uninstall -g @actual-app/sync-server  # 卸载

九、排障实录

9.1 downloadBudget 报 Budget not found,但预算明明存在

现象(实测原文):

Budget "29b70409-25fa-47ba-b410-6c553ed0a32a" not found. Check the sync id of your budget in the Advanced section of the settings page.

定位:用 /sync/list-user-files 确认预算在服务器上完好;核对 API 源码发现 downloadBudget 内部用 find(f => f.groupId === syncId) 匹配——参数要的是 groupId(同步组 ID),传入的却是 cloudFileId。

解法:改传 groupId(即 Web 界面设置页 Advanced 区显示的 Sync ID)后下载成功(第六章 6.1 实测)。记忆口诀:文件 ID 管服务器文件,Sync ID 管客户端接入。

9.2 手动 curl 文件接口返回 HTML 而非 JSON

现象:curl http://127.0.0.1:8129/list-user-files 返回 <title>Actual</title> 的完整前端页面;POST /get-user-file-info 返回 Cannot POST。

原因:v26 起同步文件接口挂在 /sync 前缀下,根路径同名路径被前端 SPA fallback 兜底成 HTML。

解法:统一走 /sync/list-user-files、/sync/get-user-file-info、/sync/download-user-file(第八章 8.2 已核对全表)。

9.3 API 客户端 init 报 ENOENT scandir

现象:init({dataDir}) 报 ENOENT ... scandir <dataDir>。

原因:API 库要求本地缓存目录预先存在,不自动创建。

解法:mkdir -p <dataDir> 后重跑(服务端数据目录会自动建,客户端缓存目录不会——两者行为不同)。

9.4 API 库加载报 GLIBCXX_3.4.29 not found

现象:

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

原因:@actual-app/api 依赖 better-sqlite3 原生模块,运行环境 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 脚本.js;系统库确实过旧则升级 libstdc++6 或 npm rebuild better-sqlite3 源码编译。

9.5 升级后客户端同步报协议错误

现象:服务器升级新版本后,API 脚本或桌面端同步报错。

原因:客户端(@actual-app/api / 桌面 App)与服务器版本错配,同步消息协议不兼容。

解法:客户端库升级到与服务器相同版本(npm install @actual-app/api@<服务器版本>);桌面 App 用其内置更新。getServerVersion() 与 npm ls @actual-app/api 双侧核对是标准排查动作。

9.6 服务器上出现多个同名预算

原因:runImport/界面"创建新预算"每次都会新建预算文件,重跑初始化脚本必然重复。

解法:getBudgets() 比对各预算 groupId,确认保留项后在 Web 界面删除多余预算;初始化脚本只在空服务器跑一次,后续一律 downloadBudget 拉取。

十、参考链接