Ghostfolio 3.90.0 Docker Compose 部署指南
Docker Compose 部署 Ghostfolio 3.90.0 三服务栈(app+PostgreSQL 15+Redis):环境变量、数据导入导出、持久化与备份恢复闭环实测
Ghostfolio 3.90.0 Docker Compose 部署指南
目标:用 Docker Compose 一键部署 Ghostfolio 3.90.0 三服务栈(应用 + PostgreSQL 15 + Redis),覆盖镜像选型、环境变量、数据导入导出、持久化与「备份 → 删库 → 恢复 → API 验证」完整闭环。已在 Ubuntu 22.04(x86_64,Docker 28 / Compose v5.5.1)实测通过。
一、环境信息
- 操作系统:Ubuntu 22.04 LTS(x86_64)
- Docker:28.x(含 Compose v5.5.1 插件,
docker compose version验证) - Ghostfolio:3.90.0(官方镜像
ghostfolio/ghostfolio:3.90.0,压缩后 1.46GB;查询日期 2026-10-23) - PostgreSQL:15.19(镜像
postgres:15-alpine,实测postgres --version) - Redis:8.10.2(镜像
redis:alpine,实测redis-server --version) - 应用端口:3333(容器内),宿主机映射 3333
- 部署目录:
/opt/ghostfolio(docker-compose.yml+.env) - 数据目录:PostgreSQL 数据持久化目录(本篇以 bind mount 落
/opt/ghostfolio/data/postgres为例) - Compose 项目名:
ghostfolio-3.90.0-stack(name:显式声明,避免与其他项目同名互扰)
二、镜像选型说明
Ghostfolio 官方在 Docker Hub 发布多架构镜像(linux/amd64、linux/arm64、linux/arm/v7),仓库 ghostfolio/ghostfolio。三服务角色与镜像选择:
| 服务 | 镜像 | 选型理由 |
|---|---|---|
| 应用 | ghostfolio/ghostfolio:3.90.0 | 官方唯一镜像,tag 与 GitHub Release 一致;不建议用 latest(随发版漂移) |
| 数据库 | postgres:15-alpine | 与官方 compose 一致;14/16 亦可,Prisma 迁移负责 schema |
| 缓存 | redis:alpine | 仅作缓存与队列,无持久化诉求,alpine 体积最小 |
解读:三个镜像全部来自官方源(Docker Hub library 与 Ghostfolio 官方账号),生产固定版本 tag。拉取确认:
docker pull ghostfolio/ghostfolio:3.90.0
docker images ghostfolio/ghostfolio --format '{{.Repository}}:{{.Tag}} {{.Size}}'
docker.io/ghostfolio/ghostfolio:3.90.0
ghostfolio/ghostfolio:3.90.0 1.46GB
三、前置条件
- 已安装 Docker Engine 与 Compose 插件(
docker compose version有输出即可); - 服务器可访问 Docker Hub(拉镜像);
- 端口 3333 未被占用(
ss -ltn | grep 3333应无输出,占用则改映射左端口); - 规划目录
/opt/ghostfolio用于 compose 文件与数据落盘。
四、部署步骤
4.1 准备目录与环境变量文件
mkdir -p /opt/ghostfolio/data/postgres
cd /opt/ghostfolio
创建 .env(JWT_SECRET_KEY 与 ACCESS_TOKEN_SALT 用 openssl rand -hex 32 各自生成,勿照抄演示值):
cat > /opt/ghostfolio/.env <<'EOF'
NODE_ENV=production
PORT=3333
HOST=0.0.0.0
ROOT_URL=http://<服务器IP或域名>:3333
POSTGRES_USER=ghostfolio
POSTGRES_PASSWORD=q1w2e3r4
POSTGRES_DB=ghostfolio_db
DATABASE_URL=postgresql://ghostfolio:q1w2e3r4@postgres:5432/ghostfolio_db
REDIS_HOST=redis
REDIS_PORT=6379
REDIS_PASSWORD=q1w2e3r4
REDIS_DB=0
JWT_SECRET_KEY=8f4c1a9e7b2d5f3a6c0e9d1b4a7f2c5e8d0a3b6f9c1e4d7a0b3f6c9e2d5a8f1b
ACCESS_TOKEN_SALT=a3f6c9e2d5b8f1a4c7e0d3b6a9f2c5e8d1a4b7f0c3e6d9a2b5f8c1e4d7a0b3f6
EOF
逐项说明:
DATABASE_URL:应用连 PostgreSQL 的连接串,主机名写服务名postgres(Compose 内置 DNS 按服务名解析);POSTGRES_USER/PASSWORD/DB:postgres 镜像初始化三要素,须与DATABASE_URL一致;REDIS_*:Redis 分项连接(Ghostfolio 无REDIS_URL整串变量);REDIS_PASSWORD在本栈同时驱动 redis 的--requirepass;ROOT_URL:对外访问地址,用于生成回调类链接,按实际域名/IP 修改;- 两个密钥为必填,缺失或改盐的后果见 12.2 / 12.3。
4.2 编写 docker-compose.yml
/opt/ghostfolio/docker-compose.yml(与官方 compose 同构,数据落盘改为 bind mount 并显式项目名):
name: ghostfolio-3.90.0-stack
services:
ghostfolio:
image: docker.io/ghostfolio/ghostfolio:3.90.0
container_name: ghostfolio-app
restart: unless-stopped
init: true
stop_grace_period: 1m
cap_drop:
- ALL
security_opt:
- no-new-privileges:true
env_file:
- .env
ports:
- "3333:3333"
depends_on:
postgres:
condition: service_healthy
redis:
condition: service_healthy
healthcheck:
test: ['CMD-SHELL', 'curl -f http://localhost:3333/api/v1/health']
interval: 10s
timeout: 5s
retries: 5
postgres:
image: docker.io/library/postgres:15-alpine
container_name: ghostfolio-postgres
restart: unless-stopped
cap_drop:
- ALL
cap_add:
- CHOWN
- DAC_READ_SEARCH
- FOWNER
- SETGID
- SETUID
security_opt:
- no-new-privileges:true
env_file:
- .env
healthcheck:
test:
['CMD-SHELL', 'pg_isready -d "$${POSTGRES_DB}" -U $${POSTGRES_USER}']
interval: 10s
timeout: 5s
retries: 5
volumes:
- /opt/ghostfolio/data/postgres:/var/lib/postgresql/data
redis:
image: docker.io/library/redis:alpine
container_name: ghostfolio-redis
restart: unless-stopped
user: '999:1000'
cap_drop:
- ALL
security_opt:
- no-new-privileges:true
env_file:
- .env
command:
- /bin/sh
- -c
- redis-server --requirepass "$${REDIS_PASSWORD:?REDIS_PASSWORD variable is not set}"
healthcheck:
test:
['CMD-SHELL', 'redis-cli --pass "$${REDIS_PASSWORD}" ping | grep PONG']
interval: 10s
timeout: 5s
retries: 5
逐项说明:
name:固定项目名,docker compose down只作用于本项目;- 应用容器
cap_drop: ALL+no-new-privileges最小权限化;init: true让 tini 正确回收子进程;stop_grace_period: 1m给优雅停机留时间; depends_on带service_healthy:应用等 PostgreSQL 与 Redis 健康后才启动,pg_isready/redis-cli ping各自守护数据库可用性;- postgres 保留
CHOWN等五个能力是 alpine postgres 入口脚本修正数据目录属主所必需; - redis 以
999:1000低权限用户运行,--requirepass从.env注入(:?语法在变量缺失时直接拒绝启动); - PostgreSQL 数据经 bind mount 落
/opt/ghostfolio/data/postgres,重建容器数据不丢。
4.3 启动栈
cd /opt/ghostfolio
docker compose up -d
实测回显(节选):
Container ghostfolio-redis Healthy
Container ghostfolio-postgres Healthy
Container ghostfolio-app Started
首次启动应用容器会自动执行数据库迁移与种子(
npx prisma migrate deploy→npx prisma db seed→node main,官方 entrypoint 行为),等待 10~30 秒。
确认三服务健康:
docker compose ps --format '{{.Name}}\t{{.Status}}'
ghostfolio-app Up 25 seconds (healthy)
ghostfolio-postgres Up 35 seconds (healthy)
ghostfolio-redis Up 35 seconds (healthy)
4.4 验证服务
curl -s http://127.0.0.1:3333/api/v1/health
{"status":"OK"}
curl -s http://127.0.0.1:3333/api/v1/info
{"benchmarks":[],"globalPermissions":["enableAuthToken","createUserAccount"],"baseCurrency":"USD","currencies":["USD","USX"]}
curl -sI http://127.0.0.1:3333/ | head -3
HTTP/1.1 301 Moved Permanently
X-Powered-By: Express
Access-Control-Allow-Origin: *
Location: /en
浏览器打开 http://<服务器IP>:3333/(跳转 /en 注册页)创建首个账号,首个注册用户自动获得管理员角色。
4.5 注册首个账号(API 实测)
自托管 Ghostfolio 的注册即「创建用户并领取访问令牌」(无邮箱注册流程):
curl -s -X POST http://127.0.0.1:3333/api/v1/user -H 'Content-Type: application/json' -d '{"languageCode":"en"}'
实测回显(节选):
{"accessToken":"da5e8b1e580837b49ddb7e98ddb9e32104d2862da46b5fd4320df9e2e4dab851c099bfc71eb9a3fa026e016a1bdb089e29b24b2ddf73f70864aded4759e4bb0d","role":"ADMIN","authToken":"eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpZCI6IjI1OTk0ODEzLTMwNGMtNGNmMy04ZGM2LWNjMjRkYjA3OTAzNSIsImlhdCI6MTc5MjczOTk3MCwiZXhwIjoxNzk1MzMxOTcwfQ.TINiCPrIvIgzpnvUFWOMVguKpKOtvs8ANvrwxyVCGn8"}
accessToken:128 位访问令牌,等价登录凭据,妥善保存(Web 端「登录」即输入它);role: ADMIN:首个注册用户即管理员;authToken:30 天有效期的 JWT,调 API 时放Authorization: Bearer <authToken>。
五、数据导入与导出
5.1 导入交易活动(自建资产实测闭环)
Ghostfolio 支持从 CSV 导入(Web 界面)与 JSON 导入(POST /api/v1/import)。导入 JSON 的字段:type(BUY/SELL/DIVIDEND…)、symbol、dataSource、date、quantity、unitPrice、fee、currency。
默认数据源 YAHOO 需要外网访问 Yahoo Finance 行情接口做符号校验;网络受限的服务器(对端限流/防火墙)会报 symbol ("AAPL") cannot be resolved by the data source ("YAHOO")。此时改走「自建资产 + MANUAL 数据源」,实测全流程:
第一步,创建自建资产档案(符号必须 GF_ 前缀):
curl -s -X POST "http://127.0.0.1:3333/api/v1/admin/profile-data/MANUAL/GF_QZFUND1" -H "Authorization: Bearer $JWT" -H 'Content-Type: application/json' -d '{"currency":"USD"}'
实测回显(节选):
{"currency":"USD","dataGatheringFrequency":"DAILY","dataSource":"MANUAL","symbol":"GF_QZFUND1",...}
⚠️ 符号不带
GF_前缀会报must start with the prefix "GF_" for the data source ("MANUAL")。
第二步,修正资产显示名(PATCH):
curl -s -X PATCH "http://127.0.0.1:3333/api/v1/admin/profile-data/MANUAL/GF_QZFUND1" -H "Authorization: Bearer $JWT" -H 'Content-Type: application/json' -d '{"name":"青竹全球ETF","assetClass":"EQUITY","assetSubClass":"ETF"}' -o /dev/null -w '%{http_code}\n'
200
第三步,导入两笔活动(买入 + 分红):
curl -s -X POST http://127.0.0.1:3333/api/v1/import -H "Authorization: Bearer $JWT" -H 'Content-Type: application/json' -d '{
"activities": [
{"type":"BUY","symbol":"GF_QZFUND1","dataSource":"MANUAL","date":"2025-01-15T00:00:00.000Z","quantity":100,"unitPrice":10.5,"fee":2,"currency":"USD","comment":"建仓"},
{"type":"DIVIDEND","symbol":"GF_QZFUND1","dataSource":"MANUAL","date":"2025-06-30T00:00:00.000Z","quantity":100,"unitPrice":0.15,"fee":0,"currency":"USD"}
]
}'
实测回显(节选,返回入库后的活动明细):
{"activities":[{"comment":"建仓","currency":"USD","date":"2025-01-15T00:00:00.000Z","fee":2,"quantity":100,"type":"BUY","value":1050,"valueInBaseCurrency":1050},...]}
第四步,验证持仓与绩效:
curl -s "http://127.0.0.1:3333/api/v2/portfolio/performance?range=max" -H "Authorization: Bearer $JWT" | python3 -m json.tool | grep -E 'currentNetWorth|totalInvestment"'
"currentNetWorth": 1050,
"totalInvestment": 1050,
5.2 导出全量数据
curl -s "http://127.0.0.1:3333/api/v1/export" -H "Authorization: Bearer $JWT" | python3 -c "import json,sys; d=json.load(sys.stdin); print('meta:',d['meta']['version'],'activities:',len(d['activities']),'accounts:',len(d['accounts']))"
实测回显:
meta: 3.90.0 activities: 2 accounts: 1
导出 JSON 即官方导入格式(Web 界面「导出」按钮同源),可作为跨实例迁移与备份的补充手段。
六、数据持久化说明
- PostgreSQL:唯一持久层。本栈 bind mount 到
/opt/ghostfolio/data/postgres;官方原版 compose 用命名卷postgres:/var/lib/postgresql/data,两者二选一(bind mount 便于直接观察与物理备份,命名卷便于 compose 自管理)。实测数据目录生成:
ls /opt/ghostfolio/data/postgres | head -5
PG_VERSION
base
global
pg_wal
- Redis:纯缓存,不挂卷,容器重建即空,无数据损失风险;
- 应用容器:无本地状态(全在 PostgreSQL/Redis),可随时重建;
- 重建验证:
docker compose down && docker compose up -d后登录令牌与持仓数据均保留(数据在 PostgreSQL)。注意down不删 bind mount 数据;若要彻底清空需另删/opt/ghostfolio/data/postgres。
七、环境变量表
本栈 .env 全量变量(docker compose exec ghostfolio env 可复核注入结果):
| 变量 | 必填 | 示例值 | 作用 |
|---|---|---|---|
NODE_ENV | 是 | production | 运行模式,官方镜像内置 production |
PORT | 否 | 3333 | 应用监听端口(默认 3333) |
HOST | 否 | 0.0.0.0 | 监听地址 |
ROOT_URL | 否 | http://<IP>:3333 | 对外地址,用于回调链接 |
POSTGRES_USER | 是 | ghostfolio | postgres 初始化用户 |
POSTGRES_PASSWORD | 是 | q1w2e3r4 | postgres 初始化密码 |
POSTGRES_DB | 是 | ghostfolio_db | postgres 初始化库名 |
DATABASE_URL | 是 | postgresql://ghostfolio:q1w2e3r4@postgres:5432/ghostfolio_db | 应用数据库连接串(主机写服务名) |
REDIS_HOST | 是 | redis | Redis 主机(服务名) |
REDIS_PORT | 是 | 6379 | Redis 端口 |
REDIS_PASSWORD | 是 | q1w2e3r4 | Redis 密码(同时驱动 requirepass) |
REDIS_DB | 否 | 0 | Redis 逻辑库索引 |
JWT_SECRET_KEY | 是 | 32 字节 hex | JWT 签名密钥 |
ACCESS_TOKEN_SALT | 是 | 32 字节 hex | 访问令牌加盐,变更即全部旧令牌失效 |
更多可选变量(数据源 API Key、TRUST_PROXY、LOG_LEVELS、安全头开关、OIDC 等)的逐项实测说明见 《Ghostfolio 3.90.0 配置参数详解》。
八、配置文件挂载
Ghostfolio 无独立应用配置文件,全部配置经环境变量注入(env_file: .env),因此无需挂载任何配置卷。变量修改后的生效方式:
cd /opt/ghostfolio
vim .env
docker compose up -d ghostfolio
Compose 检测到 env 变更会重建应用容器(数据在 PostgreSQL,重建无损)。
九、等价 docker run
理解 compose 行为的单容器等价命令(应用容器单独运行时需接入与 postgres/redis 相同的网络):
docker network create ghostfolio-net
docker run -d --name ghostfolio-postgres --network ghostfolio-net \
-e POSTGRES_USER=ghostfolio -e POSTGRES_PASSWORD=q1w2e3r4 -e POSTGRES_DB=ghostfolio_db \
-v /opt/ghostfolio/data/postgres:/var/lib/postgresql/data \
docker.io/library/postgres:15-alpine
docker run -d --name ghostfolio-redis --network ghostfolio-net \
docker.io/library/redis:alpine redis-server --requirepass q1w2e3r4
docker run -d --name ghostfolio-app --network ghostfolio-net --env-file /opt/ghostfolio/.env \
-p 3333:3333 --init --restart unless-stopped \
docker.io/ghostfolio/ghostfolio:3.90.0
生产仍建议 compose 管理(健康检查与启动顺序编排是手写 docker run 做不到的)。
十、常用运维命令
docker compose ps # 栈内服务状态与健康
docker compose logs -f ghostfolio # 跟踪应用日志(迁移、行情抓取都在这)
docker compose restart ghostfolio # 重启应用
docker compose stop / start # 停止 / 启动整栈
docker compose down # 删除容器与网络(保留数据目录)
docker compose pull && docker compose up -d # 升级镜像并滚动重建
升级完整流程(固定新版本 tag):
cd /opt/ghostfolio
sed -i 's/ghostfolio:3.90.0/ghostfolio:3.91.0/' docker-compose.yml
docker compose pull
docker compose up -d
应用启动时自动执行 prisma migrate deploy,数据库 schema 随新版本自动演进;升级前务必先做第十一章的数据库备份。
十一、备份与恢复闭环(实测)
11.1 备份
cd /opt/ghostfolio
docker compose stop ghostfolio
docker exec ghostfolio-postgres pg_dump -U ghostfolio ghostfolio_db > /opt/ghostfolio/backup-ghostfolio-20261023.sql
wc -l /opt/ghostfolio/backup-ghostfolio-20261023.sql
docker compose start ghostfolio
实测回显:
1605 /opt/ghostfolio/backup-ghostfolio-20261023.sql
pg_dump的-U ghostfolio指定数据库角色;导出为纯 SQL 文本,可跨 PostgreSQL 14/15/16 恢复。生产建议再加定时任务每日导出并异地保存。
11.2 模拟数据丢失并恢复
先停应用释放连接,删库重建后回灌备份:
docker compose stop ghostfolio
docker exec ghostfolio-postgres psql -U ghostfolio -d postgres -c 'DROP DATABASE ghostfolio_db;'
docker exec ghostfolio-postgres psql -U ghostfolio -d postgres -c 'CREATE DATABASE ghostfolio_db OWNER ghostfolio;'
实测回显:
DROP DATABASE
CREATE DATABASE
⚠️ 管理类 psql 命令必须带
-d postgres,否则 psql 默认连「与用户同名库」而报FATAL: database "ghostfolio" does not exist(实测踩坑)。
回灌并重启:
docker exec -i ghostfolio-postgres psql -U ghostfolio -d ghostfolio_db < /opt/ghostfolio/backup-ghostfolio-20261023.sql
docker compose start ghostfolio
实测回显(节选):
ALTER TABLE
ALTER TABLE
11.3 恢复后验证
curl -s http://127.0.0.1:3333/api/v1/health
{"status":"OK"}
用恢复前签发的 JWT(同一用户)查绩效——净值 1050 与 5.1 导入的数据完全一致,证明用户、令牌哈希、活动数据全量还原:
curl -s "http://127.0.0.1:3333/api/v2/portfolio/performance?range=max" -H "Authorization: Bearer $JWT" | python3 -m json.tool | grep currentNetWorth
"currentNetWorth": 1050,
十二、常见问题(FAQ)
12.1 改了 PORT 后容器一直 unhealthy
现象:.env 中 PORT=4444 后应用实际监听 4444(容器内 curl localhost:4444/api/v1/health 返回 OK),但 docker compose ps 健康状态为 starting/unhealthy。原因:compose 健康检查硬编码 http://localhost:3333/api/v1/health。解决:改端口时同步修改 healthcheck 的 test 端口,或保持 3333 仅改宿主映射左端口("8150:3333")。
12.2 缺 JWT_SECRET_KEY 启动即退出
实测日志:
Missing environment variables:
JWT_SECRET_KEY: undefined
================================
Exiting with error code 1
解决:.env 补齐后 docker compose up -d ghostfolio 重建。
12.3 改了 ACCESS_TOKEN_SALT 后无法登录
实测:改盐后 POST /api/v1/auth/anonymous 返回 {"statusCode":403,"message":"Forbidden"},还原盐值后旧令牌立即恢复可用。盐参与访问令牌哈希,已有数据的生产实例禁止更换。
12.4 导入活动报 symbol cannot be resolved
原因:默认数据源 YAHOO 需外网可达 Yahoo Finance;受限网络实测报 activities.0.symbol ("AAPL") cannot be resolved by the data source ("YAHOO")。解决:按 5.1 改用 MANUAL 自建资产(GF_ 前缀),或配置 CoinGecko API Key。
12.5 Redis 密码不一致
现象:应用日志刷 RedisCacheService Error ... 鉴权/连接错误。原因:.env 的 REDIS_PASSWORD 是 redis 服务 --requirepass 与应用认证的共同来源,若曾单独改过某一侧会失配。解决:统一改 .env 后 docker compose up -d(postgres/redis/app 三个服务都会以新 env 重建)。
12.6 重建后数据消失
排查顺序:确认用的是 docker compose down(保留数据目录)而非删除 /opt/ghostfolio/data/postgres;确认 volumes: 路径与首次部署一致(路径换了等于换了新库)。
十三、参考链接
- 官方 compose 与镜像:
https://github.com/ghostfolio/ghostfolio/blob/main/docker/docker-compose.yml、https://hub.docker.com/r/ghostfolio/ghostfolio - 官方镜像发布说明(README Self-hosting 节):查询日期 2026-10-23,三架构 amd64/arm64/arm-v7
- 源码构建部署(非 Docker 路线):《Ubuntu 22.04 安装 Ghostfolio 3.90.0 指南》
- REST API 导入/导出/绩效接口逐条实测:《Ghostfolio 3.90.0 API 使用详解》