AFFiNE自托管实战:用Docker部署本地优先的开源Notion替代品

发布时间:2026/9/13 8:21:04
AFFiNE自托管实战:用Docker部署本地优先的开源Notion替代品 我手机上现在还留着Notion但电脑上的客户端已经卸干净了换成了一款叫 AFFiNE 的开源工具。促使我动手的契机其实很朴素我对 Notion 的在线依赖越来越不耐烦了数据放在别人服务器上这件事本身我倒是能接受真正让我难受的是离线时连看个笔记都要转圈以及数据库表格一旦复杂起来编辑体验能把我拖到崩溃。所以当我看到 AFFiNE 这个“笔记白板数据库三合一”的开源项目时第一反应是怀疑——这类全能型工具十有八九是样样通样样松。但抱着 Docker 部署成本也不高的心态试了一周之后我直接把主力笔记迁过去了。这篇文章就聊三件事AFFiNE 到底有什么底气说自己是 Notion 的替代品我实际部署时踩了哪些坑以及怎么填的还有那些网上很少直接说的真实体感问题。1. 为什么我用得越久越觉得 Notion 不对劲寻找自托管笔记的内在逻辑很多人换笔记工具的第一驱动是“功能不够”但我是反过来——Notion 的功能太多了多到它的运行机制开始反过来压住我的使用习惯。1.1 在线优先的架构让我在离线时变成一个废人Notion 的渲染和编辑都重度依赖云端同步本地其实只有一层薄缓存。这个架构的好处是多人协作非常顺Web Clipper、全平台客户端都能做到状态一致坏处是当你的网络状况不稳定或者你人在高铁上、电梯里、地下车库时打开一个稍微大一点的页面你会看到无限转圈的加载图标。我自己的笔记本里存了很多带表格的读书笔记单页内容大概有几千字每次离线打开都要等 10 秒以上运气不好直接白屏。这种体验用一次两次还能忍长期用真的会磨掉你记录的热情。相比之下AFFiNE 的底层逻辑是“本地优先”。它把数据落在本地 SQLite 或者你自建的数据库里渲染和编辑操作都不需要经过远端服务器。你打开文档就是打开一个本地文件内容秒出同步变成后台动作。这种体验在你习惯了之后回去再用 Notion 会非常难受——你会觉得为什么我自己的笔记还要等网络。1.2 数据库能力很强但强到普通用户根本用不动Notion 的 Database 功能是很多人的心头好我也曾经用得很开心。但用多了你会发现它的数据库看似灵活实际有一种“半吊子”的感觉。比如数据库的视图切换很慢当你有几百条数据时筛选和排序经常要等上一小会。对于本地单机用户来说Database 和普通页面之间的联动让你被迫学习一套 Notion 专属的“关系”概念。真正复杂的统计、透视、公式处理它的能力又远不如真正的表格工具。我需要的“数据库”其实是结构化的笔记索引和任务清单而不是一个在线版的 Airtable。AFFiNE 的 Database 做得更克制它保留了核心的表格、看板、筛选、排序功能但又没有把复杂度全部堆到用户脸上。特别是它和文档、白板之间的联动让我可以把一个数据库视图直接嵌入到白板或者文档里这是 Notion 里绕半天才能实现的交互。1.3 白板在 Notion 里是一个“补充”在 AFFiNE 里是一个“核心”Notion 很晚才加入白板功能实际上是收购了 Skiff 之后带过来的但用过的朋友都懂那个白板就是一个便利贴墙你很难拿它做真正的内容创作。而 AFFiNE 脱胎于开源社区的 Blocksuite 项目它的白板并不是一个独立功能而是和文档编辑融在同一套块编辑器里。你可以把任意文字块直接拖到白板画布上也可以在画布上画一个框再去填充内容文本块和绘图元素之间是可以互相转化的。这个交互我做产品原型、画架构图、梳理项目流程时非常受用基本可以替代 Figma 或者 miro 的轻度使用场景。基于这三点我判断 AFFiNE 不是那种“缝合怪”工具而是和 Notion 同赛道但出发点完全不同的产品。它更符合我对“本地优先、模块化、数据可控”这三个关键词的期待。不过判断归判断真正决定我是否迁移的还是部署体验。2. Docker 部署 AFFiNE我用的方案、资源规划与每一步操作记录AFFiNE 官方其实提供了很多部署方式有桌面客户端、Web 版托管服务也有一键 Docker 镜像。既然我的目标是自托管那 Docker 部署就是最顺的一条路。2.1 部署之前先确定你要用哪种部署模式AFFiNE 的 Docker 部署实际上有两种常见模式搞混了后面全是坑。第一种是local模式数据都写在容器文件系统或者挂载卷里不依赖外部数据库适合个人试用、单机轻量使用。启动之后AFFiNE 内部会自己管理数据库文件。这种模式最大的好处就是“一条命令跑起来”连 PostgreSQL 都不用装。但问题也很明显——将来数据量变大后迁移和备份相对麻烦因为你需要处理的是文件和 PostgreSQL/Redis 两套数据实际 local 模式也内置了 PostgreSQL 和 Redis但都封装在容器内部。第二种是self-hosted模式也就是把 AFFiNE 的前端和后端服务跑起来同时自己创建 PostgreSQL 数据库和 Redis 实例。这种模式适合正式长期使用数据可控性更强部署起来也更接近生产环境的配置习惯。我最终选择的是第二种。理由有两个首先我本来就有一套跑在 NAS 上的 PostgreSQL 和 Redis直接复用比再开一套容器更省资源其次我后期大概率会做定期备份和多端同步独立的数据库实例在备份和还原上会方便得多。2.2 拉取镜像和编写 docker-compose 文件我用的服务器是 Ubuntu 22.04装好了 Docker 和 docker compose 插件。如果你还没有 Docker先去完成安装这里不再展开基础安装步骤。AFFiNE 官方的 Docker 镜像目前统一发布在 Docker Hub 上搜索ghcr.io/toeverything/affine或者直接拉取affine主仓库里的self-hosted示例。我建议不要只拉镜像而是直接把官方提供的 docker-compose.yml 拿下来改。我最后使用的 compose 文件关键部分如下精简过环境变量version: 3.8 services: affine: image: ghcr.io/toeverything/affine-graphql:stable container_name: affine restart: unless-stopped ports: - 3010:3000 environment: - AFFINE_ENVproduction - DATABASE_URLpostgres://affine:affine_passwordpostgres:5432/affine - REDIS_SERVER_URLredis://redis:6379 - AFFINE_SERVER_PORT3000 depends_on: - postgres - redis volumes: - affine_data:/app/data postgres: image: postgres:16 container_name: affine-postgres restart: unless-stopped environment: - POSTGRES_USERaffine - POSTGRES_PASSWORDaffine_password - POSTGRES_DBaffine volumes: - pg_data:/var/lib/postgresql/data redis: image: redis:7 container_name: affine-redis restart: unless-stopped volumes: - redis_data:/data volumes: affine_data: pg_data: redis_data:注意ports左边是宿主机端口右边是容器内 AFFiNE 服务默认的 3000 端口。我这里改用 3010 是为了避免和宿主机上其他服务冲突。实际拉取镜像时由于涉及到 ghcr.io 源国内服务器可能需要配置镜像加速否则镜像拉取时间可能很长甚至直接超时。2.3 启动后必须做的三件收尾优化第一步配置 Nginx 反向代理和 HTTPS。AFFiNE 的 Web 端是基于 WebSocket 实时协作的所以 Nginx 配置里除了常规的proxy_pass还必须要单独处理 WebSocket 升级否则多人协作时会出现“连接已断开”的提示。我贴一段核心配置参考location / { proxy_pass http://127.0.0.1:3010; proxy_http_version 1.1; proxy_set_header Upgrade $http_upgrade; proxy_set_header Connection upgrade; proxy_set_header Host $host; proxy_set_header X-Real-IP $remote_addr; proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; proxy_set_header X-Forwarded-Proto $scheme; proxy_read_timeout 3600s; proxy_send_timeout 3600s; }proxy_read_timeout和proxy_send_timeout务必要设置得大一点否则走 WebSocket 协作时稍微空闲一会儿就被 Nginx 掐断这种坑排查起来非常隐蔽。第二步修改默认管理员密码。如果你是用官方 compose 模板启动的AFFiNE 初始化后创建的默认管理员邮箱和密码是公开写在文档里的。启动成功后尽快登录后台进入设置页面把账号密码改掉然后关掉任何非必要的注册接口。AFFiNE 后端支持通过环境变量控制是否允许注册我是直接把AFFINE_ADMIN_EMAIL和AFFINE_ADMIN_PASSWORD换成自己的强密码并在启动前就设置好。第三步做数据定期备份任务。AFFiNE 的数据主要在 PostgreSQL 和 Redis 里前端界面操作产生的块数据都会落到数据库。所以我的定时任务很简单每天凌晨用一个脚本pg_dump导出数据库再用scp推到另一个机器上保留 7 天。Redis 里主要存的是缓存和会话数据不需要太重量的备份但建议跟服务器快照绑定防止机器故障后连缓存带数据一起丢。3. 部署后的第一个硬仗数据库与会话问题的完整排查链路任何自托管项目都不可能一帆风顺AFFiNE 也不例外。我部署那一晚遇到的几个问题非常典型值得单独开一节来讲排查思路而不是只报一个“最终答案”。3.1 症状一容器起来了但浏览器打开是“502 Bad Gateway”这是最糟糕的“半成功”状态——Docker 容器确实在运行但 Web 服务没有真正响应。最开始我以为只是服务启动慢多等两分钟就好结果等了五分钟还是 502。排查链路是这样的第一看容器日志看有没有明显的报错。命令是docker logs affine --tail 100。日志里如果全是INFO没有ERROR那基本排除应用崩溃。第二确认端口映射和容器内服务监听地址。AFFiNE 默认的监听地址是0.0.0.0:3000如果你在 compose 里配置了AFFINE_SERVER_PORT但是和容器内实际端口不一致就可能导致流量进得来但找不到服务。第三检查依赖服务是否就绪。502 很多时候是后端依赖了 PostgreSQL 或 Redis但这两个容器还没初始化完成导致后端应用启动失败或者一直处于重试状态。我用docker logs看到过类似cant connect to postgres的报错。解决方式很简单给后端容器加一个健康检查或者干脆在启动脚本里先 sleep 几秒让数据库先跑起来。最终我用了一个更稳妥的方案——在 compose 文件里给 postgres 定义healthcheck然后 affine 服务用depends_on里的condition: service_healthy来确保数据库先就绪再启动后端。healthcheck: test: [CMD-SHELL, pg_isready -U affine -d affine] interval: 10s timeout: 5s retries: 53.2 症状二登录后会话频繁失效刷新一下就被登出这个问题更恶心因为它不影响你打开页面但严重影响你使用。每次登录之后过几分钟再操作就提示登录过期刷新又要重新登录。定位过程分成两步先看 Redis 是否正常工作。AFFiNE 的会话和 token 相关数据是存在 Redis 里的如果 Redis 没起来或者网络不通服务端就无法保存会话状态表现就是“假登录”。我查了一下 Redis 容器的状态发现它虽然起来了但因为我之前单独跑过另一个 Redis 实例端口冲突导致 AFFiNE 所连的 Redis 压根不是 compose 里面定义的那个。也就是说后端连到了一个“幽灵 Redis”自然存不进会话数据。解决方法是在 compose 文件里给 Redis 服务单独映射一个非默认端口并确保环境变量里的REDIS_SERVER_URL指向的是这个内部服务名和端口而不是宿主机地址。其次再看 AFFiNE 的SERVER_FLAVOR配置。AFFiNE 区分了selfhosted和local两种部署口味如果你用的是local模式但想用外部 Redis某些版本会导致 token 存储异常。我现在统一用selfhosted情况就稳定了。3.3 症状三多人同时在线编辑时文档内容互相覆盖这是我在新鲜劲头过去后拉朋友一起测试时发现的。两个账号同时编辑同一个文档改完后发现一方的内容被另一方覆盖了不是实时合并而是“后写覆盖先写”。通常来说AFFiNE 使用 CRDT 数据结构天然支持多人实时协作理论上不会出现这种问题。但出现覆盖最大嫌疑是 WebSocket 没有生效客户端和后端变成了“请求-响应”式通信而不是实时通道。我又看了一遍 Nginx 的配置发现问题出在我最初没有配置proxy_set_header Connection upgrade导致 WebSocket 握手失败客户端自动降级为轮询模式。这个模式下 CRDT 的实时合并效果大打折扣尤其在编辑密集的情况下很容易出现覆盖。补上了 WebSocket 升级头之后我又用浏览器的开发者工具确认了 Network 面板里ws://连接的状态为101 Switching Protocols多人协作的问题才算真正解决。提示如果你用了 Cloudflare 之类的 CDN还要注意 CDN 是否支持 WebSocket很多免费 CDN 默认不转发升级头需要单独开启。3.4 关于镜像拉取慢的补充说明国内服务器拉取 Docker Hub 和 ghcr.io 镜像都可能有网络问题这个属于环境问题不是 AFFiNE 本身的问题。我的做法是给 Docker daemon 配置了镜像加速地址并指定了registry-mirrors。改完配置重启 Docker 服务后再重新拉取镜像速度稳定了很多。4. 笔记、白板、数据库三个场景的日常实操记录部署稳定之后我开始正经把 AFFiNE 当日常主力工具用。这一节聊聊这三个核心模块在真实使用中的表现以及我摸索出的操作习惯。4.1 笔记模块用 Markdown 写文档还行吗AFFiNE 的文档编辑兼容 Markdown 语法但它的底层是块编辑器不是传统的 Markdown 文本编辑。这意味着你敲#加空格会生成一级标题敲-加空格会生成列表项输入/可以唤起指令菜单插入代码块、引用块、待办事项等等。实际体验下来我的结论是日常写作完全够用但如果你习惯“所见即所得”的 Markdown 编辑器需要一点适应时间。适应点在于AFFiNE 把“文本”和“块”的概念放大了。你随手打了一段字那是一个文本块你想给这段字换个背景色也是在块级别操作。这跟 Typora、Obsidian 的纯文本体验有明显差异。我自己的笔记习惯是“卡片式写作”——一篇笔记由若干个小标题和小段落组成。在 AFFiNE 里这种结构天然适配因为每个标题和段落都可以独立拖拽排序。我经常写完一个段落觉得它放的位置不对直接拖到另一个章节下面整个文档的结构就跟着调整了非常顺畅。不过我遇到一个小问题长文档性能不如 Obsidian。当你一篇文档超过了两三万字继续输入时会有轻微延迟。这可能和块编辑器的渲染逻辑有关毕竟每个块都是独立对象。这个问题目前还没有彻底解决官方也在持续优化但我可以通过把一个超长文档拆分成多个子文档来规避。4.2 白板模块画原型和流程图真的能替代 Figma 吗白板是 AFFiNE 最亮眼的功能也是我决定迁移的重要理由。我工作中经常要画两类东西产品功能流程图和系统架构图。以前画这些我可能会打开 Figma 或者 draw.io但有了 AFFiNE 之后我基本直接在项目文档里调出白板画完直接嵌入文档上下文效率提升非常明显。具体操作是这样的在任意文档里输入/whiteboardAFFiNE 会创建一个白板子块然后你可以切到白板模式在画布上自由绘图、连线、加文字。这些白板元素和文本块共存于同一个文档页面里你不需要从文档跳转到另一个工具也不需要导入导出直接就能看到效果。连线功能做得也不错。你可以从任意图形边缘拉出一条线连接到另一个图形线会跟着图形移动而自动避让和重排。画流程图的体验非常接近 miro轻量够用。但如果你需要画特别精细的 UML 时序图或者需要复杂的自动布局功能AFFiNE 还是比不过专业绘图工具。从我的使用频率来看AFFiNE 的白板至少帮我减少了 60% 的 Figma 打开次数。那些只为了“画两张图”而开 Figma 的场景已经完全被 AFFiNE 白板替代了。4.3 数据库模块看板和表格的实际体验AFFiNE 的 Database 支持表格、看板、列表三种视图。我目前主要用它管理两类内容任务清单和资料索引。任务清单我用看板视图按照“待办 / 进行中 / 已完成”分列。每张卡片可以关联到一个详细的文档页面点击卡片就能展开看到完整上下文。这个体验非常接近 Notion 的看板而且由于数据存在本地操作响应速度明显更快。资料索引我用表格视图每一行代表一本书或者一篇论文列字段有标题、作者、阅读状态、笔记链接、标签。这套使用方式和 Notion 数据库几乎一致但 AFFiNE 的表格在刚创建时没有那么多默认属性需要自己添加列稍微熟悉一下就能上手。需要提醒的是AFFiNE Database 毕竟不是 Airtable不要拿它做复杂的交叉关联和分组统计。它的定位是“结构化笔记”不是“无代码数据平台”。我见过一些人一开始就抱着“我要用 AFFiNE 数据库做订单管理系统”的想法最后发现自定义能力和公式支持不足就放弃了。认清边界很重要。4.4 本地优先带来的额外惊喜Web Clipper 与多端体验虽然我主要用 Docker 自托管的 Web 服务但 AFFiNE 也提供了桌面客户端。客户端同样连接到你自托管的服务器地址这样我可以在电脑桌面上获得更接近原生应用的体验包括快捷键、系统通知和文件拖拽。手机端我用 PWA 把网页添加到主屏幕基本够看和简单编辑。剪藏方面官方没有特别好用的浏览器剪藏插件我目前用的是一个变通方案——直接把网页内容复制粘贴到 AFFiNE 里它能自动把大段文本转换为对应块结构基本保留标题、列表、代码块格式。对于我这种以文字为主、很少剪报图文混排网页的人来说这个方式够用了。5. 从 Notion 迁移数据的执行记录哪些能迁、哪些必须重建迁移是很多人换工具时最头疼的一步。我自己的迁移策略可以总结成一句话别追求无损迁移关键是把你真正高频使用的那部分内容搬过来。5.1 文档数据的导出与导入Notion 支持全量导出导出格式是 HTML/Markdown/CSV我选的是 Markdown CSV。导出后你会得到一堆 zip 压缩包里面按页面层级组织了文件夹每个页面对应一个 md 文件。AFFiNE 支持直接导入 Markdown 文件。我在 AFFiNE 里新建一个“迁移导入”文档集然后把导出的 md 文件一个个拖进去它会自动识别标题层级和列表结构。导入后大部分简单文档的格式都能保留包括加粗、斜体、代码块和引用块。但图片是相对路径AFFiNE 无法自动从 Notion 的导出包里关联图片所以图片需要重新上传。实际操作中我的处理方式是重要文档里如果有图我直接在导入后手动拖拽本地图片到对应位置一般性文字笔记不带图直接导入完事。这个过程花了我大概一个下午但换来的是之后几个月用得舒坦。5.2 数据库的困境结构可以重建内容靠 CSV 半自动导入Notion 的 Database 导出为 CSV 后AFFiNE 目前不提供一键导入数据库的功能。这意味着你不能把 Notion 数据库表直接变成 AFFiNE 数据库表。我尝试过一种间接方案用脚本把 CSV 转成 Markdown 表格再通过 AFFiNE 的导入功能生成一个表格页面。但这种转换方式生成的表格是静态的不具备数据库的筛选、分组能力等于只搬了内容没搬结构。所以对于数据库类内容我的建议是只迁移你需要长期维护和频繁使用的少量数据库手动在 AFFiNE 里重建字段再逐条复制粘贴内容。我迁移了“阅读笔记库”和“任务总表”一共几十条数据重敲一遍其实也就一两个小时。但如果你有几千条 Notion 数据库记录那迁移成本会非常高建议先想清楚是不是真的需要全部搬过来。5.3 附件、图片和文件手动归位是没办法的事Notion 的附件是存在它自己的 CDN 上的导出包里只是下载链接。AFFiNE 没有提供自动抓取并重新上传附件的功能。我选择的是“按使用频率分优先级”处理方式高频附件经常要打开看手动下载后传到 AFFiNE 对应页面。低频附件归档类暂时留在 Notion 里等真的需要时再取出来。截图类图片大多数直接不迁移没了反而让旧文档更干净。这种“断舍离”式迁移迁移完你会觉得还挺轻松的至少比面对几千个乱糟糟的附件强。6. AFFiNE 的真实战力评估它到底能不能替代 Notion这个问题如果简单回答“能”或者“不能”都是不负责任的。我只能说它在我这里确实替代了但这是因为我的使用画像和 AFFiNE 的能力高度重合。我把两者做了一张对比表方便你自己判断维度NotionAFFiNE离线体验差重度依赖网络本地优先离线流畅数据可控性不可控数据在官方服务器自托管数据在自己手里白板能力补充功能偏便利贴核心能力可替代 miro 轻度使用数据库能力强且复杂学习成本高足够日常不适合当无代码平台移动端成熟App 体验好较弱建议 PWA 或等客户端完善插件生态非常丰富刚起步扩展有限协作体验成熟稳定团队小场景可用较大团队需测试部署门槛零门槛注册即用需要 Docker 基础我的结论是如果你是重度 Notion 用户依赖它的模板市场、插件生态、多人协作和成熟移动端那 AFFiNE 暂时替代不了 Notion。但如果你像我一样主要把笔记工具当成“个人知识库 轻度项目管理 画图工具”而且对数据自主权有明确要求那 AFFiNE 完全值得一试。尤其是白板与文档、数据库打通的设计逻辑这种“一体化”体验恰恰是 Notion 因为历史包袱而给不了的。最后分享一个个人技巧AFFiNE 部署之后不要一上来就疯狂迁移所有数据。先用一个月每天只把三五个新建的笔记放进去等确认它能承载你的真实工作流再把高频数据逐步搬过去。这样你既不会因为迁移冲动浪费时间也能在最自然的状态下检验这个工具到底适不适合你。