ESSAY
个人博客自动化部署实践:从本地到云端的完整链路
“当你在 Obsidian 中写下最后一个字,保存,推送,然后看到文章出现在自己的域名下时,那种掌控感是无可替代的。“
核心观点 / 起源
在社交媒体算法主导内容分发的时代,个人博客似乎成了一种”过时”的存在。但当我决定将博客从本地部署迁移到个人域名时,我发现它正在成为一种稀缺的数字资产。
最初的动机很简单:想要一个可以随时访问的作品集,一个能够证明”我不仅会思考,还能动手实现”的平台。但更深层的原因是,我需要一个完全由自己掌控的内容发布渠道——在平台规则随时可能改变的环境下,这种掌控感变得尤为重要。
然而,要实现这一切,首先要解决一个核心问题:如何打通从写作到发布的完整链路?这不仅是技术问题,更是工作流设计的问题。
第一道关卡:Git 网络连接问题
部署的第一步是将代码推送到 GitHub。我在 VS Code 中执行 git push,却看到了令人困惑的报错:Failed to connect to github.com port 443 after 21072 ms。
困惑的是,我的浏览器可以正常访问 GitHub,为什么命令行就连不上?
这是一个典型的”网络割裂”现象。浏览器能够访问 GitHub,是因为它自动使用了系统的代理网络。但 Git 命令行工具默认是直接向目标服务器发起请求的,并不会自动走代理通道。
解决方案很直接:为 Git 手动配置代理。
首先,需要获取本地代理的端口号。如果使用的是 VPN 客户端,通常可以在设置或状态页面找到本地监听端口(常见的如 1080、10809 等)。
然后,在终端中配置 Git 代理:
git config --global http.proxy http://127.0.0.1:你的端口号
git config --global https.proxy http://127.0.0.1:你的端口号
配置完成后,再次尝试推送,连接问题就解决了。
但这里有一个需要注意的地方:如果以后需要访问国内的 Git 仓库(比如 Gitee),挂着代理可能会导致连接变慢或报错。这时可以用以下命令取消全局代理:
git config --global --unset http.proxy
git config --global --unset https.proxy
这个问题看似简单,但它揭示了一个重要的认知:浏览器和命令行是两个相对独立的网络环境,不能想当然地认为”浏览器能访问就代表命令行也能访问”。
第二道关卡:SSH 密钥认证失败
解决了 Git 推送问题后,下一步是配置 GitHub Actions 自动部署。这需要让 GitHub 的服务器能够通过 SSH 连接到我的云服务器,将构建好的文件传输过去。
我按照标准流程生成了 SSH 密钥对,将公钥添加到服务器,将私钥配置到 GitHub Secrets。但 GitHub Actions 却一直报错:ssh: handshake failed: ssh: unable to authenticate, attempted methods [none publickey], no supported methods remain。
我检查了私钥的完整性,确认了服务器上的公钥配置,设置了正确的文件权限,但问题依然存在。这让我开始怀疑:是不是哪里的配置有问题?
真正的突破来自于查看服务器的 SSH 访问日志。通过 sudo tail -n 50 /var/log/auth.log,我看到了两条关键信息:
# 失败的连接
Apr 14 10:08:31 ... Connection closed by authenticating user root 52.161.82.101 ... [preauth]
# 成功的连接
Apr 14 10:14:53 ... Accepted publickey for root ... ssh2: ED25519 SHA256:...
这揭示了问题的根源:服务器可以完美接受 ED25519 格式的密钥,但拒绝旧版的 RSA 密钥。新版的 OpenSSH 出于安全考虑,默认禁用了基于 SHA-1 的 RSA 签名。
解决方案是改用 ED25519 密钥:
# 生成新的 ED25519 密钥对
ssh-keygen -t ed25519 -C "github-actions-deploy"
# 将公钥添加到服务器
cat ~/.ssh/id_ed25519.pub >> ~/.ssh/authorized_keys
# 将私钥配置到 GitHub Secrets
# 在 GitHub 仓库的 Settings -> Secrets and variables -> Actions 中
# 更新 SSH_PRIVATE_KEY
更新密钥后,SSH 连接问题彻底解决。
这个过程让我明白了一个道理:服务器日志是排查问题的最可靠依据。与其在客户端反复尝试不同的配置,不如直接去服务器端查看”拒绝理由”。日志会告诉你服务器接受什么,拒绝什么,往往能一针见血地找到问题所在。
过程 / 推演
GitHub Actions 自动化部署
SSH 连接打通后,接下来是配置 GitHub Actions 工作流。这是整个自动化部署的核心环节。
在项目根目录创建 .github/workflows/deploy.yml 文件:
name: 自动部署流水线
on:
push:
branches:
- main
workflow_dispatch: # 允许手动触发
jobs:
build-and-deploy:
runs-on: self-hosted # 使用自托管的 runner
steps:
- name: 📥 获取最新代码
uses: actions/checkout@v4
- name: ⚙️ 配置 Node.js 环境
uses: actions/setup-node@v4
with:
node-version: '22' # 注意版本匹配
- name: 🔨 安装依赖与构建网页
run: |
npm install
npm run build
- name: 🚀 部署到服务器
uses: appleboy/scp-action@v0.1.10
with:
host: ${{ secrets.SERVER_IP }}
username: root
key: ${{ secrets.SSH_PRIVATE_KEY }}
source: "dist/*"
target: "/var/www/your-site"
strip_components: 1
但在实际运行中,我遇到了两个典型的错误:
错误 1: Node.js 版本不匹配
报错信息:Node.js v20.20.2 is not supported by Astro! Please upgrade Node.js to a supported version: ">=22.12.0"
这是因为 Astro 6.x 要求 Node.js 版本至少为 22.12.0,而工作流中配置的是 20。解决方法是将 node-version 改为 '22'。
错误 2: 内容校验失败
报错信息:blog → 文章名 data does not match collection schema. excerpt: Required
这是因为 Astro 的 Content Collections 对文章的 frontmatter 有严格的校验。如果定义了某个字段为必填,但文章中缺少这个字段,构建就会失败。
解决方法有两种:
- 在文章中补全缺失的字段
- 在 schema 定义中将该字段改为可选:
excerpt: z.string().optional()
通过这些实践,我总结出几个调试技巧:
- 添加手动触发器:在工作流中加入
workflow_dispatch,可以随时手动触发部署,而不用每次都提交代码 - 开启 debug 模式:在 action 配置中添加
debug: true,可以看到详细的执行日志 - 检查提交哈希:确认 Actions 运行的提交 ID 与本地最新提交一致,避免运行旧代码
写作工具与项目同步
技术问题解决后,我面临一个体验问题:我的写作环境在 Obsidian,但博客项目在另一个文件夹。难道每次写完文章都要手动复制到博客项目中吗?
这违背了 DRY (Don’t Repeat Yourself) 原则。理想的工作流应该是:在 Obsidian 中写作,保存后自动同步到博客项目,然后一键发布。
解决方案是使用 Windows 的目录联接 (Directory Junction) 功能,在博客项目中创建一个”虫洞”,直接连接到 Obsidian 的写作文件夹:
# 以管理员身份运行 PowerShell
cmd /c mklink /J "D:\blog-project\src\content\blog" "D:\Obsidian\vault\Blog_published"
这样,文件物理上只存在于 Obsidian 中,但在博客项目和 Git 的视角里,它们就像是在 src/content/blog 目录下一样。
但这里有一个需要注意的陷阱:图片文件的同步问题。
当在 Obsidian 中插入图片时,需要确保图片和文章在同一个文件夹下。为此,需要在 Obsidian 中进行以下设置:
- 打开设置 -> 文件与链接
- 将”新附件的默认位置”设置为”当前文件夹下指定的子文件夹”
- 子文件夹名称设置为
assets - 关闭”使用 [[Wikilinks]]“,改用标准的 Markdown 链接语法
这样,当在文章中粘贴图片时,Obsidian 会自动在文章旁边创建 assets 文件夹,并使用标准的  语法引用图片。
但还有一个容易忽略的问题:如果在其他文件夹写好文章后再移动到 Blog_published,Obsidian 默认只会移动 .md 文件,而图片会被留在原地。
解决方法有两种:
- 直接在
Blog_published文件夹中创作,避免移动 - 移动文章时,同时移动对应的
assets文件夹
另外,Git 对图片文件的处理也需要特别注意。我曾遇到过这样的情况:第一次提交时图片和文章一起上传了,但后来因为文章格式问题,在 GitHub 上删除了那次提交。修改文章后重新提交时,因为图片内容没有变化,Git 认为不需要再次提交,导致云端缺少图片文件,构建时报错 [ImageNotFound]。
解决方法有两种:
- 手动强制添加:
git add .然后提交 - 重命名图片文件:在 Obsidian 中重命名图片,它会自动更新文章中的引用,Git 会将其识别为新文件
这个经验告诉我:Git 只关心文件内容的变化,不关心你的操作意图。当遇到”明明有文件但 Git 不提交”的情况时,往往是因为 Git 认为这个文件”没有变化”。
结语 / 反思
回顾整个部署过程,从 Git 代理配置到 SSH 密钥认证,从 GitHub Actions 到文件同步,每一个环节都遇到了具体的技术障碍。但正是解决这些问题的过程,让我对自动化工作流有了更深的理解。
最直接的收获是效率的提升。现在,从写完一篇文章到它出现在个人域名下,只需要在 Obsidian 中保存,然后执行一次 git push。不再需要手动构建、打包、上传,不再需要担心哪个步骤出错。标准化的流程让发布变得可靠而流畅。
更重要的是创作体验的改变。当发布的门槛降低后,我发现自己更愿意将想法转化为文字。因为我知道,只要写完,它就能立刻与世界见面,而不是躺在本地文件夹里等待”某个合适的时机”。
但最大的收获,是这个过程本身带来的成长。它涉及网络配置、服务器管理、CI/CD 流程、文件系统操作等多个领域,是一次完整的全栈实践。每解决一个问题,都是对技术能力的一次验证和提升。
而当这一切都配置完成,当我在 Obsidian 中写下最后一个字,保存,推送,然后看到文章出现在自己的域名下时,那种掌控感是无可替代的。这不仅是一个内容发布平台,更是一个技术能力的证明,一个持续学习的动力,一个数字时代的个人主权宣言。
未来,这个工作流还可以继续优化:实现多平台分发自动化,建立内容管理系统,构建个人知识库。但现在,最重要的是:开始写作,持续发布,让这个自动化工作流真正运转起来。