notepub VPS 部署方案
把 ~/notes/ 这个 markdown 目录变成一个公网可访问的网站。
本文是部署方案/蓝图,讲清楚架构选型、组件分工、部署步骤、典型坑。
当前生产环境的实际状态、命令、路径请见单独的运维文档(如有)。
整体架构
┌─ 你本地 ─────────┐ ┌─ VPS ────────────────────┐
│ │ │ │
│ ~/notes/ │ rsync 同步 │ /var/lib/notepub/notes/ │
│ ├── 写新文章 │ ─────────────▶ │ /usr/local/bin/notepub │
│ └── 改翻译 │ │ │
│ │ │ ┌──────────────────┐ │
│ notepub 源码 │ scp 上传 │ │ systemd 守护进程 │ │
│ └── go build │ ─────────────▶ │ │ ↓ 跑 │ │
│ │ │ │ notepub :8080 │ │
└──────────────────┘ │ │ ↑ 反代 │ │
│ │ Caddy :443 │ │
浏览器 │ └──────────────────┘ │
▲ │ │
└─────│ https://notes.你域名 │
└──────────────────────────┘
四个组件配合:
- notepub —— 真正读 markdown、生成网页的程序
- systemd —— 让 notepub 长期跑着,崩了自动重启
- Caddy —— 接管 80/443 端口,处理 HTTPS,把流量转给 notepub
- rsync —— 本地内容同步到 VPS
也可以单机部署(开发和生产同一台机器),把 scp / ssh vps 这些远程操作换成本地命令即可,整体逻辑不变。
下面逐个讲。
组件 1:notepub(自写的 Go 二进制)
它是什么
一个 HTTP 服务。启动时给它一个 markdown 目录,它就在某个端口(默认 8080)提供这些文档的网页版。
notepub serve /var/lib/notepub/notes --port 8080
它支持什么
- 扫描目录生成左侧导航
- 把 markdown 转成 HTML(带代码高亮、图片等)
- 配对
.en.md/.zh.md,做语言切换 - 提供"源文 / 渲染"切换
- 浅色 / 深色 / 跟随系统三档主题(localStorage 持久化)
- 移动端汉堡菜单 + 全屏抽屉
- 收
.html独立文件,iframe 嵌入显示(自带样式不污染主框架) - 根级
README.md自动渲染为站点首页 - 也支持
notepub build <dir> -o ./dist导出静态站
它解决什么问题
markdown 文件在硬盘里不能直接被浏览器渲染。需要一个程序:扫目录、转 HTML、按双语命名约定配对、提供主题/视图切换。这些事 mdBook、Docusaurus 这类现成工具都能做一部分,但都不识别 .en.md / .zh.md 这种双语命名约定,也不混合 markdown + HTML 文件。所以自己写最省心。
为什么用 Go
- 编译出来是单个二进制,部署简单(scp 上去就能跑)
- 跨平台交叉编译方便(本地 Mac 编出 Linux 版本)
- 自带 HTTP 库 + goldmark(markdown 解析)+ chroma(代码高亮)
二进制大约 18MB,跑起来内存几十 MB。
关键设计:每次请求实时读 markdown
notepub 不缓存内容。rsync 同步后浏览器刷新就是新内容,不用重启服务。这是日常使用最大的便利。
组件 2:systemd(进程守护)
它是什么
Linux 系统自带的服务管理器。你给它一段配置,它负责把指定程序"当作系统服务跑"。
它解决什么问题
如果你 SSH 上 VPS 直接 notepub serve ... 跑起来,关掉 SSH 程序就死了。就算用 nohup 后台跑,崩溃了也没人重启。
systemd 解决三件事:
- 开机自启 —— VPS 重启后服务自动起来
- 崩了自动重启 —— 程序异常退出,systemd 立刻拉起
- 日志统一管理 ——
journalctl -u notepub直接看日志
配置文件(推荐带安全沙箱)
/etc/systemd/system/notepub.service:
[Unit]
Description=notepub — markdown notes site
After=network.target
[Service]
Type=simple
User=notepub
Group=notepub
ExecStart=/usr/local/bin/notepub serve /var/lib/notepub/notes --listen 127.0.0.1:8080
Restart=on-failure
RestartSec=3
# 安全沙箱
NoNewPrivileges=true
ProtectSystem=strict
ProtectHome=true
PrivateTmp=true
ReadWritePaths=/var/lib/notepub
ProtectKernelTunables=true
ProtectKernelModules=true
ProtectControlGroups=true
RestrictAddressFamilies=AF_INET AF_INET6 AF_UNIX
RestrictNamespaces=true
LockPersonality=true
MemoryDenyWriteExecute=true
RestrictRealtime=true
SystemCallArchitectures=native
[Install]
WantedBy=multi-user.target
监听 127.0.0.1:8080 而非 0.0.0.0:8080:notepub 只接受本机连接,外面只走 Caddy。多一层防护。
启用:
systemctl enable --now notepub # 立即启动 + 开机自启
systemctl status notepub # 看运行状态
journalctl -u notepub -f # 实时看日志
组件 3:Caddy(反向代理 + HTTPS)
这是初学者最容易疑惑的一环。详细讲。
它是什么
一个 Web 服务器。但我们这里不让它直接服务文件,而是让它做"反向代理"。
为什么不让 notepub 直接对外
理论上 notepub 也可以监听 443 端口直接对外服务,但有几个问题:
- HTTPS 证书 —— 公网访问必须是 https://(不然浏览器报警)。证书要找 Let's Encrypt 申请,每 90 天续期一次,自己用 Go 写这套逻辑很重
- 端口权限 —— 80/443 是特权端口,需要 root 才能监听。不想让 notepub 跑在 root 下
- 多服务并存 —— 以后可能在同一个 VPS 跑别的服务(博客、API),都要 443,得有一个东西分流
- 静态资源缓存、压缩、限流 —— 这些都是 Web 服务器擅长的事
所以分工:notepub 只在内网跑 8080,外面套一层 Caddy 处理 HTTPS 和公网访问。
反向代理是什么意思
浏览器 ──https──▶ Caddy:443 ──http──▶ notepub:8080
(公网) (本机)
Caddy 收到 https://notes.your-domain.com 的请求,把请求内容原样转给本机的 8080 端口,notepub 处理完返回结果,Caddy 再返回给浏览器。"反向"的意思是"代理服务端"(普通代理是代理客户端,比如翻墙)。
为什么是 Caddy 不是 Nginx
Nginx 也能做同样的事,但 Caddy 有一个杀手锏:自动 HTTPS。
只要域名 DNS 指向 VPS,Caddy 启动时自动从 Let's Encrypt 申请证书、自动续期,零配置。Nginx 要手动配 certbot。
配置文件
/etc/caddy/Caddyfile:
notes.your-domain.com {
reverse_proxy localhost:8080
}
# DNS 没生效前,IP 直访也能用(兜底)
:80 {
@notdomain not host notes.your-domain.com
handle @notdomain {
reverse_proxy localhost:8080
}
}
域名块走自动 HTTPS。:80 兜底块让你在 DNS 切换期或域名过期时还能用 IP 访问。
caddy validate --config /etc/caddy/Caddyfile # 改完先验证语法
systemctl reload caddy # 再重载
组件 4:rsync(本地到 VPS 的同步)
它是什么
一个文件同步工具。比 scp 聪明:只传变化的部分。
它解决什么问题
你在本地 ~/notes/ 写新文章、改翻译。这些变化要怎么到 VPS?
三种选择:
| 方式 | 优点 | 缺点 |
|---|---|---|
| scp 整个目录上传 | 简单 | 每次全量传,慢 |
| git push + git pull | 有版本历史 | 每次要 commit + ssh 上去 pull,两步 |
| rsync | 增量同步,一条命令 | 没有版本历史 |
短期用 rsync 最快。要版本历史可以两个一起用:本地 git commit 留历史,rsync 推 VPS。
用法
rsync -av --delete ~/notes/ vps:/var/lib/notepub/notes/
参数解释:
-a保留权限、时间戳等所有属性-v显示传了什么--delete本地删了的文件,VPS 上也删(保持镜像一致)- 末尾的
/很关键 ——~/notes/表示"目录里的内容",没斜杠表示"目录本身"
进阶:包一层 notepub-sync 脚本
裸 rsync 命令好用但有两个不顺手的地方:
- 没有"先看变更再决定要不要同步"的预览
- 同步后没有自动健康检查
可以包一层脚本:rsync -ain --delete(dry-run)→ 让用户看到变更 → 询问 [y/N] → 真正 rsync -a --delete → curl /healthz 验证服务还活着。这就是 notepub-sync 工具的本质。值得做,因为它把"上线一次笔记"从两条命令缩成一条带交互的命令。
一次性部署(按顺序)
假设:
- VPS 是 Ubuntu/Debian
- 已有域名
notes.example.com,DNS A 记录指向 VPS IP - VPS 用户名是
root或有 sudo 权限的用户
Step 1:本地编译 notepub
cd /path/to/notepub-source
GOOS=linux GOARCH=amd64 go build -o notepub ./cmd/notepub
GOOS=linux GOARCH=amd64 让 Mac/Windows 也能编出 Linux 二进制。如果开发和部署在同一台 Linux 机器,省掉这两个环境变量直接 go build 也行。
Step 2:上传二进制和内容到 VPS
scp notepub vps:/tmp/
ssh vps 'install -m 0755 /tmp/notepub /usr/local/bin/notepub'
ssh vps 'mkdir -p /var/lib/notepub'
rsync -av ~/notes/ vps:/var/lib/notepub/notes/
用 install -m 0755 而不是 mv + chmod:原子写、自动设权限,且如果目标文件正在被运行(systemd 跑着 notepub)也能替换掉,避免 text file busy。
Step 3:在 VPS 上创建专用用户
ssh vps
useradd -r -s /usr/sbin/nologin -d /var/lib/notepub notepub
chown -R notepub:notepub /var/lib/notepub
为什么要专用用户:notepub 服务跑在自己的账号下,不会有任何 root 权限。万一有漏洞也炸不到系统。
Step 4:装 systemd unit
把上面"组件 2"那段配置写到 /etc/systemd/system/notepub.service,然后:
systemctl daemon-reload
systemctl enable --now notepub
systemctl status notepub
最后一条应该看到 active (running)。
Step 5:装 Caddy
⚠️ Ubuntu/Debian 默认仓库的 Caddy 版本太旧,要从 cloudsmith 装:
apt install -y debian-keyring debian-archive-keyring apt-transport-https curl
curl -1sLf 'https://dl.cloudsmith.io/public/caddy/stable/gpg.key' \
| gpg --dearmor -o /usr/share/keyrings/caddy-stable-archive-keyring.gpg
curl -1sLf 'https://dl.cloudsmith.io/public/caddy/stable/debian.deb.txt' \
| tee /etc/apt/sources.list.d/caddy-stable.list > /dev/null
apt update && apt install -y caddy
caddy version
把上面"组件 3"那段 Caddyfile 写到 /etc/caddy/Caddyfile,把日志目录权限交给 Caddy:
mkdir -p /var/log/caddy && chown -R caddy:caddy /var/log/caddy
caddy validate --config /etc/caddy/Caddyfile
systemctl reload caddy
Caddy 启动后会自动从 Let's Encrypt 申请证书。第一次可能要等 30 秒。
Step 6:开防火墙 + 验证
ufw allow 80/tcp && ufw allow 443/tcp
浏览器打开 https://notes.your-domain.com,应该看到目录树和文档。DNS 还没生效时用 http://VPS_IP/ 访问(:80 兜底块)。
日常使用
写完新文章后同步:
rsync -av --delete ~/notes/ vps:/var/lib/notepub/notes/
只要这一条。notepub 不缓存内容,下次请求就是新内容。
改 notepub 代码后更新二进制:
GOOS=linux GOARCH=amd64 go build -o notepub ./cmd/notepub
scp notepub vps:/tmp/notepub.new
ssh vps 'install -m 0755 /tmp/notepub.new /usr/local/bin/notepub && systemctl restart notepub'
install 而不是 mv:原子替换,且能覆盖正在被 systemd 运行的二进制(mv 会报 text file busy 必须先 stop)。
封装成脚本(推荐):
# ~/bin/notepub-deploy
#!/bin/bash
set -e
rsync -av --delete ~/notes/ vps:/var/lib/notepub/notes/
echo "✓ Notes synced"
chmod +x ~/bin/notepub-deploy,以后写完文章 notepub-deploy 一条命令。要更顺手见组件 4 那段"包一层 notepub-sync 脚本"。
隐私分级
~/notes/ 里有些工具笔记可能不想公开。三种方案:
方案 A:全公开
不需要任何配置。
方案 B:子目录白名单(推荐)
让 notepub 启动时只服务指定子目录:
ExecStart=/usr/local/bin/notepub serve /var/lib/notepub/notes \
--include agents-series \
--include sandbox-tech \
--listen 127.0.0.1:8080
guides/ 整个目录就不会出现在网站上。
方案 C:Caddy basic auth 公私分线
notes.your-domain.com {
handle /private/* {
basicauth {
you JDJhJDEwJEVCNm...
}
reverse_proxy localhost:8080
}
handle {
reverse_proxy localhost:8080
}
}
/private/* 路径要密码,其他路径公开。密码哈希用 caddy hash-password 生成。
故障排查
网站打不开 / 浏览器报 502
ssh vps
systemctl status notepub # notepub 跑着没?
journalctl -u notepub -n 50 # 最近 50 行日志
curl localhost:8080/healthz # 本地能访问吗?应返回 ok
如果 curl localhost:8080/healthz 能通但浏览器不行,问题在 Caddy 或防火墙。
HTTPS 证书没签下来
journalctl -u caddy -n 100 | grep -iE 'error|cert'
最常见原因:DNS 还没生效,或者域名指向了别的 IP。可以先用 :80 兜底块跑着,等 DNS 生效后 systemctl reload caddy。
改了内容但网站没更新
ssh vps 'ls -la /var/lib/notepub/notes/'
确认 rsync 真的传到位。如果时间戳没变就是没同步。
典型踩坑
部署中遇到、值得提前知道的 6 个坑:
-
Ubuntu 默认仓库的 Caddy 版本太旧,没法用。从 cloudsmith 装最新稳定版(见 Step 5)。
-
Caddy 日志目录权限:
mkdir + chown顺序错了会让日志文件 root:root,Caddy 以 caddy 用户跑写不进去启动失败。修:chown -R caddy:caddy /var/log/caddy。先创目录再 chown,别先 chown 再 systemctl restart。 -
覆盖运行中二进制报
text file busy:cp src dst不能覆盖正在被 systemd 运行的二进制。两种解法:(a) 先systemctl stop notepub再 cp 再 start;(b) 用install -m 0755 src dst—— 原子替换,不阻塞,推荐。 -
Go 标准 flag 包遇到位置参数停止解析:
notepub serve /var/lib/notepub/notes --port 8080里的--port会被吞掉(flag.Parse在第一个非 flag 参数处停下)。CLI 实现里要在解析前把 flag 和位置参数分开,或者明确文档要求--port 8080 /var/lib/notepub/notes这样的顺序。 -
DNS 没生效就配域名块:Caddy 启动时如果域名 A 记录还没指过来,会日志报
automatic HTTPS will not be applied,域名块没证书。但:80兜底块照常工作。等 DNS 生效后systemctl reload caddy一下即可。 -
rsync itemize 元数据噪音:第一次部署用
cp而非rsync -a会丢 mtime/owner,之后任何rsync -aindry-run 都把全部文件标成 "modified"。这是元数据差异不是内容差异。跑一次完整rsync -a把元数据对齐就清掉了。
VPS 选型
| 服务商 | 价格 | 推荐 |
|---|---|---|
| Hetzner CX11 | €4/月(1C 2G) | 性价比最高 |
| Vultr Regular | $6/月(1C 1G) | 节点多 |
| DigitalOcean | $4/月(1C 0.5G) | 文档好 |
| 阿里云 ECS | ¥30/月起 | 国内访问快 |
notepub 实际只吃几十 MB 内存,最便宜的就够。
成本一览
| 项目 | 成本 |
|---|---|
| VPS | €4/月 |
| 域名 | $10/年 |
| HTTPS 证书 | 免费(Caddy + Let's Encrypt) |
| 流量 | VPS 自带,纯文档站撑得住 |
总计约 €5/月。
参考链接
- notepub 项目(暂未公开)
- Caddy 文档:https://caddyserver.com/docs/
- systemd unit 安全加固:https://www.freedesktop.org/software/systemd/man/systemd.exec.html
- Let's Encrypt:https://letsencrypt.org/