Standard Notes Docker Compose 部署指南
Standard Notes 自托管五服务 Docker Compose 部署实录:web index.html 改写、localstack 消息总线初始化、首次迁移、验证清单与五个部署坑
Standard Notes Docker Compose 部署指南
目标:用 Docker Compose 把 Standard Notes 3.3.2 版线自托管全家桶(server + web + MySQL + Redis + localstack 五服务)完整拉起,覆盖官方 compose 的加固点、web 前端 index.html 改写、localstack 消息总线初始化、首次数据库迁移与验证清单,并沉淀部署过程中的五个实测坑。
本文全部配置与命令均为 2026-11-02 实测照抄,宿主端口规划:Web 8139、API 3000、files 3125。组件定位见本专栏《Standard Notes 介绍与选型指南》一篇,环境变量逐项解释见《Standard Notes 3.3.2 配置参数详解》一篇,API 验证见《Standard Notes 3.3.2 API 使用详解》一篇。
1. 部署拓扑
官方帮助文档明确:自托管服务器只是后端,Web 前端需另行部署。因此完整可用栈是五个服务:
| 服务 | 镜像 | 容器名 | 宿主端口 | 角色 |
|---|---|---|---|---|
| server | standardnotes/server:3125ba9212fe7c1f8b635017babdb726778ff26e | stdnotes-3125ba92-server | 3000、3125→3104 | 微服务全家桶(网关/auth/syncing/files/revisions/workers) |
| web | standardnotes/web:latest | stdnotes-3125ba92-web | 8139→80 | 前端 SPA(nginx 静态托管) |
| db | mysql:8 | stdnotes-3125ba92-db | 不暴露 | 持久化(37 张表) |
| cache | redis:6.0-alpine | stdnotes-3125ba92-cache | 不暴露 | 缓存 + 服务间队列 + 登录限流 |
| localstack | localstack/localstack:3.0 | stdnotes-3125ba92-localstack | 不暴露 | 模拟 AWS SNS/SQS 事件总线 |
server 镜像 tag 是 commit sha 而非语义版本(见第 8 节坑 3);容器内部监听 3000(API 网关)、3101(auth)、3103(syncing)、3104(files)、3105(revisions)、50051/50052(gRPC),对外只映射 3000 和 3125 两个口。
2. 官方 compose 基线与本方案加固点
官方 standardnotes/server 仓库自带的 docker-compose.yml 只有 server/localstack/db/cache 四服务,网络名固定 standardnotes_self_hosted,容器名固定 server_self_hosted 等,数据库密码明文 changeme123。本方案在其基础上做了四点加固(多栈共存环境必须):
- 命名前缀隔离:所有容器名、网络名加
stdnotes-3125ba92-前缀,避免与其他自托管栈冲突; - 固定子网:网络显式声明
10.89.58.0/24网槽,防止 Docker 自动分配漂移; - 补 web 服务:官方 compose 不含前端,这里用
standardnotes/web镜像 + 挂载改写后的index.html; - 密码与密钥走
.env:DB 密码、4 个必配密钥全部环境变量化,不进 compose 正文。
3. 目录与密钥准备
mkdir -p stdnotes/{data/mysql,data/redis,data/import,logs,uploads}
cd stdnotes
# 4 个必配密钥,全部 32 字节 hex
openssl rand -hex 32 # AUTH_JWT_SECRET
openssl rand -hex 32 # AUTH_SERVER_ENCRYPTION_SERVER_KEY
openssl rand -hex 32 # AUTH_SERVER_ADMIN_KEY
openssl rand -hex 32 # VALET_TOKEN_SECRET
目录用途:data/mysql、data/redis 为持久化数据;data/import 挂到 MySQL 的 /docker-entrypoint-initdb.d(首次初始化 SQL);logs 收集 supervisord 下各微服务日志;uploads 是 files 服务本地存储模式(FILE_UPLOAD_PATH)的落盘目录。
4. .env 最小必需配置
server 容器实测暴露 162 个环境变量名,但跑起来只需要下面 16 个(其余走镜像内默认值),这也是官方 env.sample 的口径:
######
# DB #
######
DB_HOST=db
DB_PORT=3306
DB_USERNAME=std_notes_user
DB_PASSWORD=q1w2e3r4
DB_DATABASE=standard_notes_db
DB_TYPE=mysql
#########
# CACHE #
#########
REDIS_PORT=6379
REDIS_HOST=cache
CACHE_TYPE=redis
########
# KEYS #
########
AUTH_JWT_SECRET=d772cdbc8dd1ae0fc0b00ef9d58a3eac0a81b305705a412f1b7ec0c8cc07885d
AUTH_SERVER_ENCRYPTION_SERVER_KEY=8fd9ced144be5cc40e46c1cf2be2742391f4c67e6a7a81b9d0b41dee8a2d8b06
AUTH_SERVER_ADMIN_KEY=36f42c310a5b6962c9b38f3932622e4892ef964ec940bd51a94b8d51530152f7
VALET_TOKEN_SECRET=7c24a0b8518f45b51c554258348beb4e8a27e75362bcd059260065af191fcfb2
注意示例中的密钥是本次测试栈实际使用的值,生产环境务必重新生成。官方 env.sample 注释明确:AUTH_SERVER_ADMIN_KEY 不设置时 /admin 与 /internal 接口处于无保护状态,自托管务必设置;U2F 五个 AUTH_SERVER_U2F_* 变量为可选项。
5. docker-compose.yml 全文
name: stdnotes-3125ba92-stack
services:
server:
image: standardnotes/server:3125ba9212fe7c1f8b635017babdb726778ff26e
container_name: stdnotes-3125ba92-server
env_file: .env
restart: unless-stopped
ports:
- 3000:3000
- 3125:3104
volumes:
- ./logs:/var/lib/server/logs
- ./uploads:/opt/server/packages/files/dist/uploads
networks:
- stdnotes-net
localstack:
image: localstack/localstack:3.0
container_name: stdnotes-3125ba92-localstack
expose:
- 4566
restart: unless-stopped
environment:
- SERVICES=sns,sqs
- HOSTNAME_EXTERNAL=localstack
- LS_LOG=warn
volumes:
- ./localstack_bootstrap.sh:/etc/localstack/init/ready.d/localstack_bootstrap.sh
networks:
- stdnotes-net
db:
image: mysql:8
container_name: stdnotes-3125ba92-db
environment:
- MYSQL_DATABASE=standard_notes_db
- MYSQL_USER=std_notes_user
- MYSQL_ROOT_PASSWORD=q1w2e3r4
- MYSQL_PASSWORD=q1w2e3r4
expose:
- 3306
restart: unless-stopped
volumes:
- ./data/mysql:/var/lib/mysql
- ./data/import:/docker-entrypoint-initdb.d
networks:
- stdnotes-net
cache:
image: redis:6.0-alpine
container_name: stdnotes-3125ba92-cache
volumes:
- ./data/redis/:/data
expose:
- 6379
restart: unless-stopped
networks:
- stdnotes-net
web:
image: standardnotes/web:latest
container_name: stdnotes-3125ba92-web
restart: unless-stopped
ports:
- 8139:80
volumes:
- ./index.html:/usr/share/nginx/html/index.html:ro
networks:
- stdnotes-net
networks:
stdnotes-net:
name: stdnotes-3125ba92-net
ipam:
config:
- subnet: 10.89.58.0/24
6. web 前端:index.html 必须改写(坑 1)
standardnotes/web 镜像里的 /usr/share/nginx/html/index.html 默认把前端指向官方云:window.defaultSyncServer = "https://api.standardnotes.com"。直接起容器,登录注册会打到官方服务器。必须先提取、改写、再回挂。
提取(等价 docker run 一次性容器):
docker run --rm --entrypoint cat standardnotes/web:latest \
/usr/share/nginx/html/index.html > index.html.orig
cp index.html.orig index.html
改写三行(实测 diff,本地测试环境用 127.0.0.1,生产换成你的域名并配 HTTPS 反代):
- window.defaultSyncServer = "https://api.standardnotes.com";
- window.defaultFilesHost = "https://files.standardnotes.com";
+ window.defaultSyncServer = "http://127.0.0.1:3000";
+ window.defaultFilesHost = "http://127.0.0.1:3000";
- window.websocketUrl = "wss://sockets.standardnotes.com";
+ window.websocketUrl = "ws://127.0.0.1:3125";
compose 中用 ./index.html:/usr/share/nginx/html/index.html:ro 只读挂载覆盖。前端其余静态资源(app.js/app.css/favicon)仍来自镜像。
7. localstack:消息总线必须初始化(坑 2)
server 镜像内各微服务之间不直连,事件全部走 SNS topic → SQS queue。localstack 起来后是空壳,必须由初始化脚本建好主题和队列,否则服务能启动、API 能通,但后台异步链路(版本历史归档、邮件备份、worker 消费)静默失败。
官方 server 仓库提供了 localstack_bootstrap.sh,compose 把它挂到 /etc/localstack/init/ready.d/(localstack 就绪钩子自动执行)。脚本逻辑:
# 核心函数(节选)
create_topic() { awslocal sqs/sns create-topic --name $1; }
create_queue() { awslocal sqs create-queue --queue-name $1; }
link_queue_and_topic() {
awslocal sns subscribe --topic-arn $1 --protocol sqs --notification-endpoint $2
}
# 创建 7 个 topic:
# payments / syncing-server / auth / files / analytics /
# revisions-server / scheduler (均为 *-local-topic 命名)
# 创建 3 个 queue:
# analytics-local-queue / auth-local-queue / files-local-queue /
# syncing-server-local-queue
# 再按服务拓扑把 topic 逐个 subscribe 到 queue
脚本用 awslocal(localstack 内置 AWS CLI 封装)在容器内执行,endpoint 统一 http://localhost:4566,region us-east-1。验证:docker exec <localstack容器> awslocal sns list-topics 应列出 7 个 topic。
8. 启动与首次迁移
docker compose up -d
docker compose ps # 等 5 个容器全部 Up(localstack 会先显示 healthy)
server 容器的 entrypoint 自带 wait-for.sh 依次探测 db/cache 就绪后才拉起 supervisord;首次启动自动执行迁移(knex migrations,DB_MIGRATIONS_PATH 默认镜像内路径),完成后 MySQL 里出现 37 张表:
users sessions revoked_sessions session_traces roles role_permissions
permissions user_roles offline_user_roles items revisions_revisions
shared_vaults shared_vault_users shared_vault_invites
emergency_access_invitations subscription_settings user_subscriptions ...
migrations
实测 docker compose ps 输出(起栈后):
stdnotes-3125ba92-web Up 0.0.0.0:8139->80/tcp
stdnotes-3125ba92-server Up 0.0.0.0:3000->3000/tcp, 0.0.0.0:3125->3104/tcp
stdnotes-3125ba92-localstack Up (healthy)
stdnotes-3125ba92-db Up 3306/tcp
stdnotes-3125ba92-cache Up 6379/tcp
9. 验证清单
按顺序核对,全绿即部署成功:
# 1. files 服务健康检查(宿主 3125 → 容器 3104)
curl -s http://127.0.0.1:3125/healthcheck
# OK
# 2. files 鉴权确实在岗(无 token 必须 401)
curl -s http://127.0.0.1:3125/v1/files
# {"error":{"tag":"invalid-auth","message":"Invalid valet token."}}
# 3. web 前端可打开
curl -s http://127.0.0.1:8139/ | head -5
# <!DOCTYPE html> ... 且 window.defaultSyncServer 已指向 127.0.0.1:3000
curl -s -o /dev/null -w '%{http_code} %{size_download}\n' http://127.0.0.1:8139/app.js
# 200 3986323
# 4. API 注册闭环(协议细节见本专栏 API 使用详解一篇)
curl -s -X POST http://127.0.0.1:3000/v1/users -H 'Content-Type: application/json' \
-d '{"email":"admin@quzuo.net","password":"q1w2e3r4","api":"20200115","version":"004","pw_nonce":"94ec8360ae7c472b","origination":"registration","created":1793596081689}'
# HTTP 200,返回 data.session.access_token
浏览器打开 http://<宿主>:8139,走注册 → 新建笔记 → 换浏览器登录 → 看到同步笔记,即端到端闭环。
10. 排障实录
坑 3:server 镜像 tag 是 commit sha。 官方仓库 README 的 compose 样例直接用 standardnotes/server(latest),而实际可用的发布 tag 是 40 位 commit sha(如 3125ba9212fe7c1f8b635017babdb726778ff26e)。生产务必 pin 到这个 sha,latest 与 3.3.2 版线行为可能不一致。直连 Docker Hub 拉取超时时可走国内镜像加速(如 1ms.run 前缀)拉取后再打回原 tag。
坑 4:files 端口映射方向。 容器内 files 监听 3104,不是 3125;3125:3104 是宿主侧映射。前端 window.websocketUrl 和 meta 里返回的 filesServerUrl 都按宿主 3125 配置,若把映射写成 3125:3125 会得到连接拒绝。
坑 5:失败登录限流落在 Redis。 连续登录失败后,auth 服务在 Redis 写 lock:<userUuid> 计数,触发后登录接口开始拒绝。测试脚本反复失败密码后想恢复,直接清计数:
docker exec stdnotes-3125ba92-cache redis-cli --scan --pattern 'lock:*'
docker exec stdnotes-3125ba92-cache redis-cli del lock:9785c10f-d1fb-4284-8792-ac306efc3aa5
# 1
坑 6:web 镜像没有 env 注入口。 前端指向哪个服务器只认 index.html 里的 window.* 全局变量,镜像不提供环境变量覆盖(DEFAULT_SYNC_SERVER 是构建期变量,运行时改不了),所以第 6 节的"提取-改写-回挂"三步是必选项,不是优化项。
坑 7:AUTH_SERVER_ADMIN_KEY 留空。 官方 env.sample 注释写明不设置时 /admin、/internal 无保护。公网部署要么设置该密钥,要么在反代上封掉这两个路径前缀。
11. 运维要点
- 日志:
./logs/下按进程分文件(syncing-server.log、auth-worker.log 等 9 组,supervisord 管理),排障先看对应服务; - 备份:
mysqldump(items 全是密文,备份即密文快照)+uploads/目录 +.env(密钥丢了密文永久不可解); - 升级:改 compose 里的 server 镜像 sha →
docker compose pull && docker compose up -d server,迁移自动执行;先做 DB 备份; - 重启:db/cache/localstack 与 server 无状态耦合,整体
docker compose restart安全;localstack 的 topic/queue 不持久化,重建后需重跑 bootstrap(挂 ready.d 即自动); - 拆除:
docker compose down后按需删./data、./uploads、./logs。
12. 专栏导航
13. 跨栏相关阅读
- 《Joplin 3.7.2 Docker Compose 部署指南》——更轻的笔记自托管栈对照
- 《Papermerge 3.5.3 Docker Compose 部署指南》——六服务栈的同类加固手法(命名前缀/固定网槽)
14. 参考来源
- github.com/standardnotes/server(官方 compose 样例、env.sample、localstack_bootstrap.sh)
- standardnotes.com/help/self-hosting(官方自托管帮助:Docker 为唯一受支持方式、架构与微服务职责)
- github.com/standardnotes/app(web 前端构建与
DEFAULT_SYNC_SERVER说明) - 本平台 2026-11-02 五服务栈部署实测原始记录