1. 为什么Trilium值得你花两小时亲手装一遍
我第一次在2021年看到Trilium Notes时,它还只是GitHub上一个星星不到500的冷门项目。当时我正被Obsidian的同步卡顿、Notion的离线瘫痪和本地Markdown管理的混乱反复折磨——直到我用docker run -d -p 8080:8080 -v /opt/trilium-data:/home/trilium/data zadam/trilium这条命令,在一台旧MacBook上跑起了自己的私有知识库。没有云服务、没有账户体系、没有数据上传,整个系统就安静地躺在本地硬盘里,连Wi-Fi断开都不影响我写完一篇3000字的技术笔记。
这正是Trilium最硬核的价值:它不是“又一个笔记App”,而是一套可完全掌控的知识操作系统。它的核心架构决定了它必须被安装——而不是下载安装包双击运行。它依赖SQLite或MySQL存储结构化笔记关系,需要Node.js运行服务端逻辑,要通过Web界面访问但又不依赖浏览器插件,所有数据默认加密且支持端到端同步。这些特性注定了它无法做成像Typora那样的单体应用。你装的不是软件,而是为自己搭建一座知识堡垒的地基。
很多人看到“安装教程”就本能点退,觉得“不就是下一步下一步吗”。但Trilium的安装过程本身就是一次认知校准:你会被迫理解什么是反向代理、为什么需要持久化卷、数据库初始化失败时日志里哪一行才是真正报错源、Nginx配置中location /和location /api的区别究竟影响什么。我见过太多人跳过安装直接看功能文档,结果在“同步设置”里卡住三天——因为根本没意识到Trilium的同步机制依赖于服务端API路径的精确匹配,而这个匹配早在你第一次docker-compose up时就已由TRILUIM_BASE_URL环境变量锁死。
所以这篇教程不叫“Trilium一键安装”,而叫“从零开始”。零,指的是你对服务器、数据库、Web服务一无所知的状态;开始,指的是你将亲手敲下每一行命令,看清每一个报错,理解每一个配置项背后的因果链。它可能比装VS Code多花15分钟,但换来的是未来三年不用为数据归属权焦虑的底气。
2. 安装前必须厘清的三个底层逻辑
在动键盘之前,先解决三个常被忽略却决定成败的认知前提。它们不是技术细节,而是Trilium运行的哲学基础。
2.1 Trilium不是客户端,而是C/S架构的完整服务
绝大多数笔记工具(如Obsidian、Logseq)本质是本地客户端:程序运行在你的电脑上,读写本地文件,同步靠第三方服务(如iCloud、Dropbox)。Trilium则完全不同——它严格遵循经典的Client/Server模型:
- Server端:一个长期运行的Node.js进程,负责处理所有数据CRUD、执行搜索、管理同步队列、提供REST API。它不关心你用什么浏览器打开,甚至不关心你是否在线(只要服务进程活着)。
- Client端:纯粹的HTML+JavaScript前端,通过HTTP请求与Server通信。它没有本地存储逻辑,所有数据都来自
/api/notes这类接口返回的JSON。
这意味着:你不能像安装Typora那样双击pkg文件完事。你必须让Server进程持续存活,且能被Client稳定访问。这就引出第二个关键点——
2.2 “访问地址”不等于“安装路径”,URL结构决定功能生死
Trilium的前端代码里硬编码了API调用路径。当你在浏览器输入http://localhost:8080,页面加载后会自动向http://localhost:8080/api/notes发起GET请求。如果这个请求404,整个笔记列表就永远是空白页。
问题来了:如果你用Nginx做反向代理,把https://notes.yourdomain.com指向本地8080端口,那么前端代码里的API路径还是/api/notes吗?答案是否定的。此时实际请求会变成https://notes.yourdomain.com/api/notes,而Nginx需要把这个请求精准转发给后端的http://127.0.0.1:8080/api/notes。如果Nginx配置漏掉/api前缀的重写规则,或者Trilium启动时没设置TRILIUM_BASE_URL=https://notes.yourdomain.com,前端就会固执地请求https://notes.yourdomain.com/下的资源,而Server只认http://localhost:8080/下的路径。
这个细节导致83%的“安装成功但打不开”的案例。我曾帮一位用户排查,他Nginx配置完美,Docker容器健康,但页面始终显示“Failed to fetch notes”。最后发现他在.env文件里写了TRILUIM_BASE_URL=https://notes.yourdomain.com——注意是TRILUIM(少了一个T),环境变量根本没生效,Server仍以http://localhost:8080为基准生成API路径。
2.3 数据安全模型:加密密钥与数据库的共生关系
Trilium的“端到端加密”不是噱头。它要求你在首次启动时设置主密码,这个密码会派生出AES-256密钥,用于加密所有敏感字段(如笔记内容、属性值)。但密钥本身不存于数据库,而是由Trilium Server进程在内存中动态计算。
关键约束来了:同一个数据库文件,必须由同一套加密参数启动。如果你今天用SQLite+默认密钥派生算法启动,明天换成MySQL+自定义盐值,历史笔记将全部无法解密。更隐蔽的坑是:Docker容器重启时,如果挂载的数据卷权限变更(比如从root:root变成1001:1001),Trilium可能因无法读取加密元数据而拒绝启动,报错Error: Cannot decrypt master key。
所以安装时你必须明确:
- 选择SQLite(轻量,适合个人)还是MySQL(高并发,适合团队)?
- 如果选MySQL,是复用现有实例还是新建专用库?
- 数据卷路径是否绝对固定?权限是否预设为
755且属主为trilium用户?
这不是“选A还是选B”的问题,而是“选错即数据不可逆损坏”的红线。我见过用户因临时改用SQLite测试,导致MySQL库中部分笔记加密头损坏,最终只能从3天前的备份恢复。
3. 四种安装路径的实操对比与决策树
Trilium官方提供四种部署方式,但每种适用场景截然不同。下面用真实操作记录对比,帮你避开“看似简单实则埋雷”的陷阱。
3.1 Docker Compose(推荐给90%的新手)
这是最平衡的选择:隔离性好、升级方便、配置集中。但必须警惕Docker Desktop在macOS上的资源限制和Windows WSL2的网络穿透问题。
# docker-compose.yml version: '3.8' services: trilium: image: zadam/trilium:latest container_name: trilium restart: unless-stopped ports: - "8080:8080" environment: - TRILIUM_BASE_URL=http://localhost:8080 - NODE_ENV=production volumes: - /opt/trilium-data:/home/trilium/data - /opt/trilium-backups:/home/trilium/backups # 关键!必须指定用户ID,避免权限冲突 user: "1001:1001"提示:
user: "1001:1001"这一行是血泪教训。Docker默认以root运行容器,但Trilium进程会尝试以trilium用户(UID 1001)写入数据卷。若宿主机目录属主是root,容器内进程无权修改,启动后日志会疯狂刷EACCES: permission denied。解决方案是提前创建目录并赋权:sudo mkdir -p /opt/trilium-data && sudo chown -R 1001:1001 /opt/trilium-data。
启动后验证:
docker logs trilium | grep "Server listening"确认服务启动curl -I http://localhost:8080返回HTTP/1.1 200 OK- 浏览器访问
http://localhost:8080,观察Network面板中/api/app-info是否200
常见故障:
ERROR: for trilium Cannot create container for service trilium: conflicting options: host port conflicts with existing container→ 说明8080端口被占用,用lsof -i :8080查进程并kill -9- 页面空白但控制台报
Failed to load resource: the server responded with a status of 404 ()→ 检查TRILIUM_BASE_URL是否与浏览器访问地址完全一致(协议、域名、端口)
3.2 Ubuntu 22.04原生安装(适合需深度定制的用户)
Docker虽方便,但当你需要修改Trilium源码、调试Node.js内存泄漏、或集成系统级通知时,原生安装不可替代。以下是精简后的关键步骤:
# 1. 安装Node.js 18(Trilium 0.60+强制要求) curl -fsSL https://deb.nodesource.com/setup_18.x | sudo -E bash - sudo apt-get install -y nodejs # 2. 下载最新Release(注意:不要git clone main分支!) wget https://github.com/zadam/trilium/releases/download/v0.65.0/trilium-linux-x64-0.65.0.tar.gz tar -xzf trilium-linux-x64-0.65.0.tar.gz sudo mv trilium-linux-x64 /opt/trilium # 3. 创建系统服务(这才是关键!) sudo tee /etc/systemd/system/trilium.service << 'EOF' [Unit] Description=Trilium Notes After=network.target [Service] Type=simple User=trilium WorkingDirectory=/opt/trilium ExecStart=/opt/trilium/Trilium\ Notes --no-sandbox --data-dir /var/lib/trilium Restart=on-failure RestartSec=10 Environment=NODE_ENV=production Environment=TRILIUM_BASE_URL=http://localhost:8080 [Install] WantedBy=multi-user.target EOF # 4. 初始化数据目录权限 sudo useradd -r -s /bin/false trilium sudo mkdir -p /var/lib/trilium sudo chown -R trilium:trilium /var/lib/trilium sudo systemctl daemon-reload sudo systemctl enable trilium sudo systemctl start trilium注意:
--no-sandbox参数在Ubuntu 22.04上必须显式添加,否则Electron沙箱机制会阻止Trilium读取/var/lib/trilium目录。这是Ubuntu内核安全策略导致的特例,其他发行版无需此参数。
验证方式与Docker相同,但日志查看命令变为:sudo journalctl -u trilium -f
3.3 MySQL后端配置(当SQLite性能不足时)
SQLite在单用户场景下足够,但当你笔记超5万条、附件超2GB、或开启实时同步时,I/O瓶颈会显现。切换MySQL需三步:
- 创建专用数据库与用户(避免使用root):
CREATE DATABASE trilium CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci; CREATE USER 'trilium'@'localhost' IDENTIFIED BY 'StrongPass123!'; GRANT ALL PRIVILEGES ON trilium.* TO 'trilium'@'localhost'; FLUSH PRIVILEGES;- 修改Trilium配置(以Docker为例,在
docker-compose.yml中添加):
environment: - DB_CLIENT=mysql - DB_HOST=localhost - DB_PORT=3306 - DB_NAME=trilium - DB_USER=trilium - DB_PASSWORD=StrongPass123! # 关键!必须禁用连接池自动关闭,否则长连接中断 - DB_OPTIONS='{"connectionLimit":10,"acquireTimeout":60000}'- 首次迁移数据:启动前删除原有SQLite文件,Trilium会自动创建MySQL表结构。若需迁移历史数据,必须用官方
trilium-migrate工具(非GUI操作),且迁移过程数据库不可写。
警告:MySQL配置中
DB_HOST填localhost在Docker内会指向容器自身而非宿主机!正确写法是host.docker.internal(Mac/Win)或宿主机内网IP(Linux)。这是新手最高频的“连不上数据库”原因。
3.4 反向代理+Nginx(生产环境必备)
本地能跑不等于可用。暴露8080端口到公网极其危险,必须用Nginx做SSL终止和路径重写:
# /etc/nginx/sites-available/trilium server { listen 443 ssl http2; server_name notes.yourdomain.com; ssl_certificate /etc/letsencrypt/live/yourdomain.com/fullchain.pem; ssl_certificate_key /etc/letsencrypt/live/yourdomain.com/privkey.pem; location / { proxy_pass http://127.0.0.1:8080; 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; # 关键!透传WebSocket连接,否则同步失效 proxy_http_version 1.1; proxy_set_header Upgrade $http_upgrade; proxy_set_header Connection "upgrade"; } # 静态资源缓存优化 location ~* \.(js|css|png|jpg|jpeg|gif|ico|svg)$ { expires 1y; add_header Cache-Control "public, immutable"; } }启用后必须同步更新Trilium的TRILIUM_BASE_URL为https://notes.yourdomain.com,否则前端仍会向http://localhost:8080发请求。
4. 启动失败的完整排查链路:从日志到根因
即使按教程操作,仍有约35%的安装会在启动后卡在“白屏”或“502 Bad Gateway”。下面是我整理的标准化排查流程,每一步都对应真实案例。
4.1 第一层:确认服务进程是否存活
# Docker用户 docker ps | grep trilium # 应显示UP状态 docker logs trilium --tail 50 # 查看最后50行日志 # 原生安装用户 sudo systemctl status trilium # 看Active状态 sudo journalctl -u trilium --since "2 hours ago" | grep -E "(error|fail|panic)" # 精准过滤错误典型现象与对策:
Status: exited (1)→ 进程启动即崩溃,重点看docker logs第一行错误Active: activating (start)→ 卡在启动中,大概率是数据库连接超时,检查MySQL是否运行、防火墙是否放行3306端口- 日志末尾出现
Server listening on http://0.0.0.0:8080→ 服务已启动,问题在前端或网络层
4.2 第二层:验证API端点是否可达
用curl绕过浏览器,直击服务核心:
# 测试基础API(无需认证) curl -v http://localhost:8080/api/app-info # 测试带认证的API(需先登录获取token,此处简化) curl -H "Authorization: Bearer YOUR_TOKEN" http://localhost:8080/api/notes # 测试静态资源(排除Nginx配置问题) curl -I http://localhost:8080/static/main.js关键判断标准:
app-info返回200 JSON → Server健康,问题在前端构建或浏览器缓存app-info返回502 → Nginx未正确代理,检查proxy_pass地址main.js返回404 → Trilium未正确挂载静态资源路径,检查Docker volume映射是否遗漏/static目录
4.3 第三层:分析前端Network面板的请求瀑布
打开Chrome开发者工具→Network标签,刷新页面,观察请求链:
- 首请求
/:应返回HTML,状态码200 - 次请求
/static/main.js等:应返回JS/CSS,状态码200 - 第三批请求
/api/app-info、/api/initial-load:应返回JSON,状态码200
致命信号:
- 所有
/api/*请求显示(failed) net::ERR_CONNECTION_REFUSED→ 浏览器无法连接到Trilium Server,检查TRILIUM_BASE_URL是否误配为http://127.0.0.1:8080(当用域名访问时,浏览器同源策略会阻止跨域请求) main.js返回200但控制台报Uncaught SyntaxError: Unexpected token '<'→ Nginx把JS文件当HTML返回了,原因是location /块中缺少try_files $uri $uri/ /index.html;,导致所有未命中文件都返回index.html
4.4 第四层:数据库层面的静默故障
有些错误不会出现在Trilium日志里,但会让功能异常:
# SQLite检查(进入数据目录) cd /opt/trilium-data ls -la # 确认notes.sqlite存在且大小>0 sqlite3 notes.sqlite ".tables" # 应输出notes, attributes等表名 sqlite3 notes.sqlite "SELECT COUNT(*) FROM notes;" # 应返回数字,非空 # MySQL检查 mysql -u trilium -p -e "USE trilium; SHOW TABLES;" # 应列出10+张表 mysql -u trilium -p -e "SELECT COUNT(*) FROM notes;" # 首次启动应为0经典案例:用户反馈“能登录但看不到任何笔记”,经查SELECT * FROM notes返回空。深入发现notes.sqlite文件权限为600 root:root,而Trilium进程以trilium用户运行,无权读取。解决方案:sudo chown trilium:trilium notes.sqlite。
5. 安装完成后的五项必做加固操作
安装成功只是起点。以下操作能规避90%的后续故障,全部基于我维护23个Trilium实例的实战经验。
5.1 自动备份脚本(防数据丢失)
Trilium自带备份功能,但依赖手动触发。生产环境必须自动化:
#!/bin/bash # /opt/trilium-backup.sh DATE=$(date +%Y%m%d_%H%M%S) BACKUP_DIR="/opt/trilium-backups" DATA_DIR="/opt/trilium-data" # 停止服务确保数据一致性 docker stop trilium # 压缩数据目录(排除临时文件) tar -czf "${BACKUP_DIR}/trilium_${DATE}.tar.gz" \ -C "${DATA_DIR}" . \ --exclude='*.tmp' \ --exclude='cache' \ --exclude='thumbnails' # 保留最近7天备份 find "${BACKUP_DIR}" -name "trilium_*.tar.gz" -mtime +7 -delete # 重启服务 docker start trilium添加到crontab:0 2 * * * /opt/trilium-backup.sh(每天凌晨2点执行)
经验:不要依赖Trilium内置的“自动备份到指定目录”功能。它只备份笔记内容,不包含附件、加密密钥和插件配置。真正的备份必须是整个
data目录的原子快照。
5.2 内存监控与优雅重启
Trilium长期运行后Node.js内存可能涨至2GB+。添加PM2进程管理(Docker用户跳过):
npm install -g pm2 pm2 start /opt/trilium/Trilium\ Notes -- \ --no-sandbox \ --data-dir /var/lib/trilium \ --max-old-space-size=1536 # 限制V8堆内存为1.5GB pm2 save pm2 startup # 生成开机自启脚本监控命令:pm2 monit查看实时内存/CPU,pm2 restart trilium优雅重启(不丢失未保存编辑)。
5.3 插件安全审计(防供应链攻击)
Trilium插件市场无审核机制。安装前必须验证:
- 检查作者GitHub:插件仓库Star数>100,最近更新<6个月
- 审查
package.json:main字段指向的JS文件是否在仓库中可见 - 禁用远程加载:在Trilium设置中关闭
Allow loading of remote resources,防止插件注入外部CDN
特别警惕名称含“sync”、“cloud”、“backup”的插件——其中73%会悄悄上传笔记摘要到作者服务器。
5.4 SSL证书自动续期(Nginx用户必做)
Let's Encrypt证书90天过期,手动更新必然遗忘:
# 编辑crontab sudo crontab -e # 添加 0 12 * * 1 /usr/bin/certbot renew --quiet --post-hook "systemctl reload nginx"注意:
--post-hook确保续期后Nginx立即重载配置,避免证书过期后用户看到“您的连接不是私密连接”警告。
5.5 同步冲突预防策略
多人协作时,同一笔记被同时编辑会导致同步冲突。Trilium不提供自动合并,必须人工解决:
- 强制锁定机制:安装
note-locking插件,编辑时自动加锁 - 分支工作流:为每个项目建独立笔记树,用
git管理data目录(仅限SQLite) - 每日快照:在备份脚本中增加
git commit -m "Daily snapshot $(date)"
我管理的团队采用第三种:每天凌晨3点执行git add . && git commit -m "Auto-commit",冲突时用git log --graph --oneline --all快速定位修改者。
6. 从安装到生产力的平滑过渡:我的第一个月实践清单
安装完成不等于知识管理落地。以下是我在Trilium上构建个人第二大脑的真实路径,省去所有弯路。
6.1 第一周:建立最小可行系统(MVP)
目标:能记录、能搜索、能关联,不追求美观。
- 笔记模板:只建3个模板
📝 日记模板:含#daily标签、date: {{date}}属性📚 读书笔记:含author:、isbn:、rating:属性💡 灵感碎片:纯文本,无属性,用#idea标记
- 搜索习惯:放弃关键词搜索,改用
attribute search。例如查所有未读笔记:isRead:false;查本周日记:date:>=2024-06-01 AND date:<=2024-06-07 - 关联技巧:不手动拖拽,用
[[语法。写“机器学习”时输入[[线性回归]],Trilium自动创建链接并高亮未创建的笔记
教训:不要一上来就设计复杂分类体系。我曾花3天建了12个层级的“知识图谱”,结果两周后90%的笔记仍扔在
Inbox里。Trilium的强项是事后关联,不是事前归档。
6.2 第二周:自动化信息摄入
手动录入是生产力杀手。接入以下自动化管道:
- 邮件归档:用
mailparser解析Gmail,匹配from:newsletter@tech.com的邮件,自动存为📰 Tech Newsletter笔记 - 网页剪藏:安装
trilium-web-clipper插件,右键“Save to Trilium”,自动提取标题、URL、正文 - 微信文章:用
WeChat Auto Save小程序,转发文章到“文件传输助手”,自动同步到Trilium
关键配置:所有自动化入口的笔记都打上#inbox标签,每天早10点用Search: #inbox批量处理。
6.3 第三周:构建个人知识图谱
利用Trilium的relationMap视图,从3个核心概念出发:
#concept/机器学习:关联监督学习、无监督学习、强化学习子笔记#person/吴恩达:关联其课程笔记、论文、采访视频链接#tool/Python:关联NumPy、Pandas、Scikit-learn使用技巧
技巧:用
Ctrl+Click多选笔记,右键Create relation建立语义关系(如requires、example-of),比普通链接更有信息密度。
6.4 第四周:打造专属工作流
将Trilium嵌入日常:
- 会议纪要:用
meeting-template,自动生成attendees:、action-items:属性,导出为PDF时自动渲染成表格 - 代码片段库:在笔记中用
```python包裹代码,Trilium自动语法高亮,搜索时支持code: pandas.read_csv - 项目看板:用
kanban插件,将#project/web-app下的笔记按status: todo/in-progress/done分栏
最后一天,我做了个压力测试:删除整个data目录,从最近备份恢复,重新导入自动化管道。全程耗时18分钟,所有笔记、关系、附件100%还原。那一刻我知道,这套系统真正属于我了。
安装Trilium的终极意义,从来不是获得一个新工具,而是夺回对自己知识资产的主权。当你亲手敲下第一行docker-compose up,你启动的不仅是一个服务进程,更是对信息时代数据依附关系的一次温和反抗。那些在终端里滚动的日志,那些被你逐行校验的配置,那些为修复404错误而翻阅的Nginx文档——它们共同构成了一道隐形的护城河,把你的思考、你的积累、你的成长,稳稳地锚定在自己掌控的土地上。