notepub VPS 部署方案

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.你域名    │
                                    └──────────────────────────┘

四个组件配合:

  1. notepub —— 真正读 markdown、生成网页的程序
  2. systemd —— 让 notepub 长期跑着,崩了自动重启
  3. Caddy —— 接管 80/443 端口,处理 HTTPS,把流量转给 notepub
  4. rsync —— 本地内容同步到 VPS

也可以单机部署(开发和生产同一台机器),把 scp / ssh vps 这些远程操作换成本地命令即可,整体逻辑不变。

下面逐个讲。


组件 1:notepub(自写的 Go 二进制)

它是什么

一个 HTTP 服务。启动时给它一个 markdown 目录,它就在某个端口(默认 8080)提供这些文档的网页版。

notepub serve /var/lib/notepub/notes --port 8080

它支持什么

它解决什么问题

markdown 文件在硬盘里不能直接被浏览器渲染。需要一个程序:扫目录、转 HTML、按双语命名约定配对、提供主题/视图切换。这些事 mdBook、Docusaurus 这类现成工具都能做一部分,但都不识别 .en.md / .zh.md 这种双语命名约定,也不混合 markdown + HTML 文件。所以自己写最省心。

为什么用 Go

二进制大约 18MB,跑起来内存几十 MB。

关键设计:每次请求实时读 markdown

notepub 不缓存内容。rsync 同步后浏览器刷新就是新内容,不用重启服务。这是日常使用最大的便利。


组件 2:systemd(进程守护)

它是什么

Linux 系统自带的服务管理器。你给它一段配置,它负责把指定程序"当作系统服务跑"。

它解决什么问题

如果你 SSH 上 VPS 直接 notepub serve ... 跑起来,关掉 SSH 程序就死了。就算用 nohup 后台跑,崩溃了也没人重启。

systemd 解决三件事:

  1. 开机自启 —— VPS 重启后服务自动起来
  2. 崩了自动重启 —— 程序异常退出,systemd 立刻拉起
  3. 日志统一管理 —— 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 端口直接对外服务,但有几个问题:

  1. HTTPS 证书 —— 公网访问必须是 https://(不然浏览器报警)。证书要找 Let's Encrypt 申请,每 90 天续期一次,自己用 Go 写这套逻辑很重
  2. 端口权限 —— 80/443 是特权端口,需要 root 才能监听。不想让 notepub 跑在 root 下
  3. 多服务并存 —— 以后可能在同一个 VPS 跑别的服务(博客、API),都要 443,得有一个东西分流
  4. 静态资源缓存、压缩、限流 —— 这些都是 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/

参数解释:

进阶:包一层 notepub-sync 脚本

rsync 命令好用但有两个不顺手的地方:

可以包一层脚本:rsync -ain --delete(dry-run)→ 让用户看到变更 → 询问 [y/N] → 真正 rsync -a --deletecurl /healthz 验证服务还活着。这就是 notepub-sync 工具的本质。值得做,因为它把"上线一次笔记"从两条命令缩成一条带交互的命令。


一次性部署(按顺序)

假设:

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 个坑:

  1. Ubuntu 默认仓库的 Caddy 版本太旧,没法用。从 cloudsmith 装最新稳定版(见 Step 5)。

  2. Caddy 日志目录权限mkdir + chown 顺序错了会让日志文件 root:root,Caddy 以 caddy 用户跑写不进去启动失败。修:chown -R caddy:caddy /var/log/caddy。先创目录再 chown,别先 chown 再 systemctl restart。

  3. 覆盖运行中二进制报 text file busycp src dst 不能覆盖正在被 systemd 运行的二进制。两种解法:(a) 先 systemctl stop notepub 再 cp 再 start;(b) 用 install -m 0755 src dst —— 原子替换,不阻塞,推荐。

  4. Go 标准 flag 包遇到位置参数停止解析notepub serve /var/lib/notepub/notes --port 8080 里的 --port 会被吞掉(flag.Parse 在第一个非 flag 参数处停下)。CLI 实现里要在解析前把 flag 和位置参数分开,或者明确文档要求 --port 8080 /var/lib/notepub/notes 这样的顺序。

  5. DNS 没生效就配域名块:Caddy 启动时如果域名 A 记录还没指过来,会日志报 automatic HTTPS will not be applied,域名块没证书。但 :80 兜底块照常工作。等 DNS 生效后 systemctl reload caddy 一下即可。

  6. rsync itemize 元数据噪音:第一次部署用 cp 而非 rsync -a 会丢 mtime/owner,之后任何 rsync -ain dry-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/月。


参考链接