平时写技术文档最烦的就是不同设备之间的同步问题。白天在公司台式机上码了一下午笔记,回家打开笔记本发现最新内容还躺在公司那台机器里。U盘来回拷?忘过一次就长记性了。用聊天工具发给自己?一不小心发错群就是事故。我后来搭了一套 Typora + GitHub + 云服务器 的组合方案,本地用 Typora 所见即所得地写 Markdown,Git 负责版本管理,云服务器当中转仓库,GitHub 做归档备份。整套链路从手动敲命令到全自动同步,跑了一年多没出过大问题。这篇就把完整的搭建思路、配置过程和踩过的坑都写出来,想给笔记做多端同步的人可以直接照着抄。
这套方案本质上解决的痛点有三个:一是写作体验,Typora 的即时渲染写起来舒服;二是版本历史,每次改动都有记录,改错了能随时回滚;三是多端访问,在公司写、回家接着写,中间不需要任何手工搬运。它适合的人群也很明确:写技术笔记、维护项目文档、积累博客素材的人,只要你日常产出是 Markdown 格式,这套东西就能用。
1. 方案选型:为什么是这三个组件拼在一起
很多人第一反应是“直接用云笔记不就行了吗,费这个劲干嘛”。确实,市面上的云笔记产品很多,功能也齐全,但我个人对笔记工具的要求比较偏执——第一,数据必须掌握在自己手里,纯文本格式、不锁死在某个生态里;第二,要有真正的版本历史,不是只给我一个“历史版本”按钮,而是每次改动都能看 diff;第三,写作时不能被编辑器绑架,今天用这个编辑器写,明天想换另一个工具也能继续编辑。这三个诉求叠加起来,Markdown + Git 几乎是唯一解。
Typora 负责生产内容,它把 Markdown 渲染成类似 Word 的阅读体验,但文件本质是纯文本。Git 负责记录每一次文件的变动,无论删除、修改还是新增,都有迹可循。云服务器和 GitHub 负责让内容在不同设备之间流动起来,本地写完推送到服务器,服务器再归档到代码托管平台。
1.1 三个角色是怎么分工的
各组件在链路里的位置是这样的:
- Typora:本地写作工具,负责把想法落地成 Markdown 文档,设置好图片目录之后,插图和附件也能一并管理。
- Git:版本控制核心,每次写完一段内容就提交一次,相当于给文档拍快照,留下可追溯的变更记录。
- 云服务器:中转站,所有设备的 Git 远程仓库都指向它,内容推送不需要依赖第三方平台,内网访问或者走公网都行。
- 代码托管平台(你用的 GitHub):归档备份。云服务器上的钩子脚本会自动把变更推送上去,相当于异地多了一份副本。
为什么要绕这么一圈?因为我希望本地的每一次 push 都尽可能顺畅。如果所有设备直接连 GitHub,不同网络环境下访问体验差异很大,时好时坏,碰上一次推送失败你得反复重试。而云服务器在公网有个固定入口,从本地连过去基本不会有这种问题。GitHub 在这里的角色是“归档位”,由服务器去跟它打交道,本地设备就不必关心这个过程了。
1.2 对比了一圈,为什么不用现成的云笔记
有对比才有说服力。我把自己曾经用过的几个方案都摆出来对比过:
| 方案 | 数据所有权 | 版本历史 | 编辑器自由度 | 离线可用 |
|---|---|---|---|---|
| 云笔记类产品 | 平台方持有 | 通常不开放完整历史 | 被编辑器绑定 | 有限 |
| 网盘同步盘 | 自己持有 | 冲突覆盖风险大 | 任意本地编辑器 | 可以 |
| 自建 NAS | 自己持有 | 依赖内置套件 | 一般 | 可以 |
| Markdown + Git + 服务器 | 自己持有 | 完整且可 diff | 任意本地编辑器 | 完全 |
网盘同步盘的问题在于文件冲突。一个文件在两台设备上改了不一样的内容,同步盘经常搞出“冲突副本”,命名乱七八糟,得手工合并。Git 的处理方式是提交历史,同样的场景只是多了一个分支节点,拉下来 rebase 一下就能解决,不会凭空多出几个副本文件。
自建 NAS 其实是最理想的方案,但成本偏高,一台像样的入门级 NAS 硬件加上硬盘,前期投入不小,还需要内网穿透才能在公网访问。相比之下,一台入门级云服务器年费便宜得多,资源占用还低——因为文档仓库的体积很小,Git 仓库基本不消耗 CPU 和内存,1核2G 的配置绰绰有余,这点账算下来云服务器是性价比最高的选择。
2. 动手第一步:本地仓库与编辑器的准备
整套链路的第一步不是在服务器上搞什么高深配置,而是先在本地把 Git 仓库和 Typora 的环境理顺。很多人上来就搭服务器、配钩子,结果本地还没 init,后面全是白搭。
2.1 本地 Git 初始化与首次提交
新建一个文档目录,我习惯把笔记按年份或者按项目分子目录,比如docs/2025/、docs/projects/。然后在这个目录下执行:
mkdir my-notes && cd my-notes git init git add . git commit -m "init notes"这里有个细节,Git 默认分支名是master,现在大家普遍用main,如果你希望从一开始就用main,可以执行git branch -M main。这不是必须的,但后面写脚本和钩子的时候,分支名统一会省很多事。
首次提交完成之后,本地仓库就算是建起来了。这个时候你用什么编辑器都可以打开这个目录开始写,但如果用 Typora,我还建议做两件事——打开自动保存,配置图片路径。
Typora 的自动保存在偏好设置里可以打开,默认会把未保存的修改在一段时间内自动写盘。对 Git 工作流来说,自动保存的意义在于:内容不断被写入文件,而每当你觉得一个阶段写完了,手动提交一次,Git 就把这个时刻的内容完整记录了下来。两件事配合,既不担心丢内容,又有清晰的时间线节点。
2.2 Typora 的偏好设置与图片路径
图片和附件的处理是很多人在 Markdown 文档管理里忽略的地方。默认情况下,Typora 插入一张图片,引用的是本机绝对路径,比如C:\Users\xxx\Pictures\xxx.png,这种文件只有你这台机器能看,推到远程仓库别人打不开,换个设备同样打不开。
我的习惯是在 Typora 的偏好设置里,把“插入图片时”的行为设为“复制到 ./assets 目录”,同时勾选“优先使用相对路径”。这样每篇文章旁边会自动生成一个assets文件夹,图片和文档放在一起,路径是相对的:
相对路径的另一个好处是,以后如果你想把 Markdown 发布成静态博客,文档和图片只要整体拷贝过去,图片引用不会断。这个习惯我从一开始就养成了,后来发布文档到博客站点的时候完全没踩过图片丢失的坑。
本地内容准备得差不多之后,开始安排远程仓库。这里要分清两个远程概念:一个是你自己的云服务器,一个是你用的代码托管平台。两个角色不同,配置方式也不同。
3. 云服务器中转仓库的搭建与接入
标题里既然提到了腾讯云,我就直接用腾讯云轻量应用服务器来举例。选服务器的时候不用太纠结配置,文档仓库的 Git 操作基本上只占磁盘写入和网络带宽,CPU 几乎不吃,选基础款就行。操作系统我用的 Debian,Ubuntu 也完全一样,下面命令在两种系统上都能直接跑。
3.1 为什么要自建一个中转仓库
先说清楚中转仓库到底是干嘛的。假设没有这个中转站,你的多台设备都直接把代码托管平台当远程仓库,每次 push 和 pull 都在跟外部平台打交道。实际用下来,不同网络环境下连接外部平台的顺畅程度不太一样,有的设备可能始终连接失败,遇到一次 push 超时,你就得停下来处理网络问题,写作思路全打断了。
自建中转仓库的意义在于,本地设备只跟自己的服务器通信,路径短、可控性强。推送成不成功、快不快,这些因素掌握在你自己手里。至于服务器跟 GitHub 之间的通信,那是服务器自己的网络环境,不需要本地设备关心。
整个数据链路是这样的:
本地设备(Typora + Git) ↓ push 云服务器中转仓库(裸仓库) ↓ 钩子自动触发 云服务器归档工作区 ↓ push GitHub 远程仓库(异地归档)3.2 在服务器上创建裸仓库并用 SSH 接入
登录服务器,创建一个专用的 Git 用户,这样仓库权限跟其他服务隔离,安全性更好:
adduser git mkdir /home/git/notes.git git init --bare /home/git/notes.git chown -R git:git /home/git/notes.git--bare参数很多人第一次见会疑惑,解释一下:裸仓库没有工作区,只保存 Git 的版本对象和索引信息,它就是一个纯中转站,不允许在上面直接改文件。普通仓库会保留一份可编辑的工作文件,裸仓库不要这份文件,因为没有人会在服务器上直接编辑文档,多一份文件只会增加混乱。
本地生成 SSH 密钥,如果没有的话:
ssh-keygen -t ed25519 -C "notes-sync" cat ~/.ssh/id_ed25519.pub把输出的公钥内容追加到服务器的/home/git/.ssh/authorized_keys文件里:
mkdir /home/git/.ssh echo "ssh-ed25519 AAAA... your-key-comment" >> /home/git/.ssh/authorized_keys chown -R git:git /home/git/.ssh chmod 700 /home/git/.ssh chmod 600 /home/git/.ssh/authorized_keys本地这边把远程仓库地址改成服务器:
git remote add origin git@your-server-ip:/home/git/notes.git git push -u origin main第一次 push 的时候 SSH 会提示确认服务器指纹,输入 yes 回车即可。之后每次推送都不需要输入密码,因为走的是密钥认证。这也是我没用 HTTPS 方式连接服务器仓库的原因——HTTPS 每次都要输账号密码,虽然可以用凭据管理器记住,但远不如密钥来得清爽。
到这里,本地到中转仓库的链路已经通了。下面给服务器加上 GitHub 归档的能力。
3.3 通过钩子把变更自动归档到 GitHub
先在 GitHub 上建一个空的私有仓库,比如叫notes-repo,不要勾选初始化 README,保持干净。然后服务器上需要有一个能访问这个仓库的 SSH 密钥:
# 在服务器上执行,如果还没生成过,先生成 sudo su - git ssh-keygen -t ed25519 -C "server-archive" cat /home/git/.ssh/id_ed25519.pub把输出的公钥添加到 GitHub 账户的 SSH keys 里。它是服务器访问 GitHub 的凭证。
GitHub 仓库建好之后,接下来是最关键的一步:给中转裸仓库写一个 post-receive 钩子。这个钩子在每次收到 push 之后自动执行,把新内容拉到服务器上的一个工作区,再推送到 GitHub:
# 在服务器上执行 cd /home/git/notes.git/hooks vim post-receive写入以下内容:
#!/bin/bash export GIT_WORK_TREE=/home/git/notes-backup export GIT_DIR=/home/git/notes-backup/.git # 先拉取最新内容 git pull origin main # 再推送到 GitHub git push github main然后创建钩子里需要用到的工作区:
cd /home/git git clone /home/git/notes.git notes-backup cd notes-backup git remote add github git@github.com:your-name/notes-repo.git git remote set-head origin -d chmod +x /home/git/notes.git/hooks/post-receive这里有个容易踩的坑,git pull origin main拉取裸仓库的引用时,需要先确保本地分支和远端分支的对应关系是对的。如果 clone 出来的是master分支,而裸仓库里是main,就会拉错。建议 clone 完之后直接执行git checkout -b main origin/main把本地默认分支也切到 main。后面推送就不会出现分支不匹配的问题了。
这套钩子跑通的效果是:本地只要git push origin main,服务器收到内容之后自动更新备份工作区,然后自动推到 GitHub。你的本地设备全程只跟自己的服务器通信,GitHub 那边发生什么完全不感知。
4. 把同步从手动变成半自动、全自动
落到日常使用,最影响体验的就是同步的便捷程度。如果每次写完都手动敲三条命令,时间一长就会偷懒,一偷懒内容就不同步了。所以我把同步做成了半自动,并且在部分场景下做到了全自动。
4.1 一个通用的一键同步脚本
脚本的思路很简单:把git add、git commit、git push三步合并成一个命令,给每次提交自动打上时间戳。这样你在任何目录下执行一次脚本,当前的改动就全部推到服务器并归档到 GitHub。
Mac 和 Linux 下我用这样的脚本:
#!/bin/bash cd "$(dirname "$0")" || exit 1 git add -A if git diff --cached --quiet; then echo "没有需要提交的变更" exit 0 fi git commit -m "docs: sync $(date '+%Y-%m-%d %H:%M:%S')" git push origin mainWindows 下对应的.bat版本:
@echo off cd /d %~dp0 git add -A git diff --cached --quit if %errorlevel% equ 0 ( echo no changes exit /b 0 ) git commit -m "docs: sync %date:~0,4%-%date:~5,2%-%date:~8,2% %time:~0,8%" git push origin mainWindows 用户注意,%date%和%time%的输出格式跟系统区域设置有关,不同电脑年月日的排列顺序可能不一样,提交信息里显示成什么格式都不影响 Git 使用,但如果你在意提交日志的整洁性,可以手动调整一下格式。
脚本建好之后,给它加执行权限,然后做个软链接到/usr/local/bin/notes-sync,这样你在任何地方打开终端输入notes-sync都能同步,不用记住脚本放在哪个目录。这个习惯帮我保持了一年多的日更同步,几乎没有漏过。
4.2 利用文件监听做全自动同步
半自动已经很省事了,但还不够——有时候我在 Typora 里写一段话,切到终端执行脚本,回来继续写,这个流程切来切去还是有点打断思路。于是我在 Linux 环境下用文件监听把同步做成了全自动。
引入了一个小工具叫inotifywait(Debian 系用apt install inotify-tools安装)。它的作用是监控目录里的文件变动,一旦有写入、新建、删除事件,就触发我指定的命令。我写过这样一个监听循环:
while inotifywait -r -e modify,create,delete --exclude '.git' /home/your-user/my-notes; do cd /home/your-user/my-notes git add -A git commit -m "auto sync $(date '+%Y-%m-%d %H:%M:%S')" || true git push origin main done把这段脚本放到后台运行(比如用nohup或者直接注册成 systemd 服务),之后我在 Typora 里每次按下保存,监听器都会察觉到变化,自动完成提交和推送。这个过程通常在一两秒内完成,感知不到延迟。
但这里我要说一个负责任的补充:全自动同步不适合所有人。如果你习惯了在文档里写一半就保存、第二天接着改,全自动会把半成品也提交进历史里。虽然 Git 有历史记录可以回滚,但这种操作多了,历史时间线就变得很脏,回滚时反而不好定位。我的建议是,日常内容用半自动脚本,只有写博客草稿这种“想好了再写”的内容才用全自动监听。或者折中一下,把自动提交的信息统一带上WIP前缀,后期整理时一眼就能认出来哪些是草稿。
4.3 移动端怎么办
顺手提一下移动端。Typora 没有手机版,所以这套方案在手机上不能直接“写”,但我用了一个可行的策略:手机上只读。GitHub 的网页端可以直接浏览私有仓库里的 Markdown 文件,渲染效果不错。偶尔在外面想查一个文档,打开手机浏览器进 GitHub 仓库页面就能看。
这种方式牺牲了编辑能力,换来了零成本访问。对大部分场景来说“能查资料”就够了,真正需要写长文的时候还是回电脑前操作。如果你确实需要在手机上改文档,也可以用手机端的 Git 客户端配合同一套仓库,或者用支持 Markdown 的其他编辑器,但说实话,手机小屏打字的体验本身就不好,我不太建议为了这点便利把整个方案复杂化。
5. 多设备实操中的那些坑
这套链路跑久了,各种问题也遇了不少。每次新设备接入、网络波动、多人协作(其实是自己在两台设备上同时写),都暴露过一些之前没注意到的细节。下面这些事,我建议你在开始之前就做好心理准备。
5.1 push 被拒、提交冲突怎么办
最常遇到的情况是:你在家用台式机写了内容并推送了,第二天到公司,笔记本上的仓库还停留在昨天的状态,你直接在旧的快照上写了新内容,提交后 push 被拒——因为远端已经有新的提交了。
遇到这个报错别慌,这不是内容丢了,只是 Git 在保护历史不被覆盖。正确的操作是先把远端内容拉下来合并,再重新推送:
git pull --rebase origin main git push origin main--rebase参数和默认的merge行为有个区别:rebase 会把你的本地提交“接到”远端最新提交的后面,历史线是一条直线;而 merge 会生成一个合并节点,历史线会出现分叉。个人文档仓库建议全程 rebase,历史干净。如果有冲突,Git 会标出冲突文件,用 Typora 打开能看到类似<<<<<<< HEAD的分隔符,手动保留正确的版本,删掉分隔符,再执行:
git add . git rebase --continue git push origin main在日常使用中,我自己基本没有遇到像样的冲突,因为文档内容大多数情况下是一台设备改一个文件,真正冲突的场景太少了。只要你记得换设备后第一件事是git pull,绝大概率不会走到冲突那一步。
5.2 图片、附件与大文件的处理细节
用 Git 管理文档,最大的边界就是“大文件”和“二进制文件”。Git 的设计目标是文本,它对文本文件能给出精确到每一行的差异对比,但对二进制文件只能判断“变没变”,没法告诉你具体哪里变了。所以图片、PDF、视频这类文件一旦频繁变动,仓库体积会迅速膨胀。
我的处理策略是:文档配图用相对路径放进assets目录,体积控制在几 MB 以内,这些推上去完全没有问题。超过 20 MB 的附件,或者像设计稿、安装包这类文件,不放进 Git 仓库,本地移动端需要时单独传输。
图片目录还有一个细节,Typora 默认图片复制会重命名,比如image-20250407-142030.png这种带时间戳的名字。好处是不会重名,坏处是文件多了以后,assets目录里全是这种无意义命名的文件,后期清理很麻烦。我在 Typora 偏好设置里关掉了自动重命名,改用自己在文档目录下建一个images文件夹,手动归档。这一步纯粹是个人偏好,看你自己对整洁度的容忍程度。
5.3 新设备上三分钟恢复全部文档
这套方案的另一个优势体现在换设备、重装系统的时候。我经历过一次硬盘故障,系统盘全没了,当时只焦虑了两分钟,因为文档都同步在远端。新机器上只需要三个步骤:
# 安装 Typora 和 Git # 生成 SSH 密钥,把公钥加到服务器和 GitHub 上 git clone git@your-server-ip:/home/git/notes.git my-notes克隆下来之后,文档结构和历史记录全在。Typora 的偏好设置需要重新配一下(图片路径、主题、导出模板等),但这些一次配置花不了几分钟。相比以前用网盘同步时那种“文件夹还在,但里面文件同步到一半”的尴尬状态,这个方案在恢复数据这件事上让人特别安心。
要注意的是,克隆下来的仓库默认只有远端的分支状态,本地分支需要一条命令指定:
git checkout -b main origin/main否则后续提交会提交到master上,两边分支对不上,push 的时候又会出现分支不匹配的报错。这个问题我第一次换电脑时遇到过,当时还以为是服务器配置有问题,排查了半天。
5.4 服务器和 GitHub 的关系容易混淆
很多人看到这套方案会问,为什么有了自己的服务器,还要再接入 GitHub,两头都同步不是多此一举吗?这个问题我解释过很多次。把服务器当唯一远程仓库的风险在于:如果服务器磁盘故障、数据丢了,你的文档就没有任何备份了。GitHub 在这里承担的是异地容灾的角色,相当于给自己的资料多买了一份保险。
服务器到 GitHub 的推送由钩子触发,不需要人工干预,所以这个备份过程对你完全透明。日常使用中你只需要关心本地到服务器的 push 是否成功,GitHub 那一侧有没有同步好,通过网页端刷新看一下就心里有数了。
6. 日常维护与扩展方向
这套方案不是搭完就一劳永逸的,长期跑下来有几个维护点值得你留意。一是服务器的磁盘空间,Git 仓库虽然体积小,但如果长期把附件也往里面塞,空间迟早会告急,建议每个月看一眼磁盘占用。二是备份策略,GitHub 是外部备份,但如果你愿意,还可以在服务器上写一个定时任务,定期把仓库打成一个压缩包存到对象存储,多一层保险。
6.1 定时任务的配置参考
在服务器上配置一个 crontab 任务,每周凌晨把存档目录压缩并打上日期标记:
0 3 * * 0 tar -czf /home/git/backup/notes-$(date +\%Y\%m\%d).tar.gz /home/git/notes-backup这个功能不是必须的,但如果你管理的是特别重要的文档,多一个离线压缩包总是让人更踏实。压缩包本身很小,一个积累了数年的文档仓库通常也就几十 MB,存多久都不是负担。
6.2 把文档发布成个人博客
最后聊一个扩展方向。GitHub 仓库可以开启 Pages 服务,把 Markdown 文档渲染成静态网页,等于这份笔记顺便变成了一个个人博客。你可以利用 GitHub 的 Actions 功能,提交之后自动触发构建,效果基本等同于一个零成本托管站点。对有写作分享需求的人而言,这套文档管理方案的终点不只是“同步”,而是一个可以对外展示的内容平台。
我个人目前的用法是:每天在本地写笔记,半自动同步到服务器和 GitHub,偶尔把其中一些整理成文章,单独建仓库发布出来。整个过程行云流水,没有一次因为“找不到内容”“版本不对”这些问题浪费过时间。
这套方案我越用越满意的一点,是它把所有环节都控制在了自己手里。写作工具可以换,换任何支持 Markdown 的编辑器都不影响仓库;托管平台可以换,仓库换一个远程地址就行;服务器也可以换,重新配置一下密钥和钩子,半小时搬完。数据本身永远是纯文本格式,永远有版本历史,永远有多个副本。这种掌控感,是用现成云笔记产品体会不到的。如果你也经常在多台设备间切换写文档,强烈建议照着这套思路搭一遍,成本不高,但省下来的心力是实打实的。