Standard Notes Docker Compose 部署指南

Standard Notes 自托管五服务 Docker Compose 部署实录:web index.html 改写、localstack 消息总线初始化、首次迁移、验证清单与五个部署坑

Standard NotesStandard Notes2026/11/02

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 前端需另行部署。因此完整可用栈是五个服务:

服务镜像容器名宿主端口角色
serverstandardnotes/server:3125ba9212fe7c1f8b635017babdb726778ff26estdnotes-3125ba92-server3000、3125→3104微服务全家桶(网关/auth/syncing/files/revisions/workers)
webstandardnotes/web:lateststdnotes-3125ba92-web8139→80前端 SPA(nginx 静态托管)
dbmysql:8stdnotes-3125ba92-db不暴露持久化(37 张表)
cacheredis:6.0-alpinestdnotes-3125ba92-cache不暴露缓存 + 服务间队列 + 登录限流
localstacklocalstack/localstack:3.0stdnotes-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。本方案在其基础上做了四点加固(多栈共存环境必须):

  1. 命名前缀隔离:所有容器名、网络名加 stdnotes-3125ba92- 前缀,避免与其他自托管栈冲突;
  2. 固定子网:网络显式声明 10.89.58.0/24 网槽,防止 Docker 自动分配漂移;
  3. 补 web 服务:官方 compose 不含前端,这里用 standardnotes/web 镜像 + 挂载改写后的 index.html;
  4. 密码与密钥走 .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. 跨栏相关阅读

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 五服务栈部署实测原始记录