这篇文章记录的是 ooya.site 从零到上线的完整过程,不是教程,是实战日志——包括所有踩过的坑。
目标#
一套自托管的静态站发布流程:
- 代码写好,推送到自建 Gitea
- Gitea Actions 自动触发构建
- 构建产物通过 SCP 推送到香港服务器
- Nginx 软链接切换版本,支持秒级回滚
基础设施#
- Hugo:静态站生成器
- Blowfish 主题:颜值在线,中文友好
- Gitea:自托管 Git 服务(
git.ooya.site) - Gitea Act Runner:Docker 部署的 CI 执行器
- 香港服务器:Nginx + HTTPS,无需备案
部署架构#
本地编辑 → git push → Gitea
↓
Act Runner(Docker)
↓
Hugo 构建 public/
↓
SCP 推送到香港服务器
/var/www/ooya.site/release/<commit-id>/
↓
ln -sfn release/<id> current
↓
Nginx root → current/每次部署保留历史版本,回滚只需换个软链接。
踩坑记录#
坑一:hugo.toml 里 baseURL 忘了改#
初始化项目时 baseURL = 'https://example.org/',Hugo 构建出来的所有链接都是错的。
解决:改成 https://www.ooya.site/ 再构建。
坑二:Checkout action 缓存损坏#
第一次跑 CI,actions/checkout@v4 报错:
Unable to pull refs/heads/v4: non-fast-forward updateRunner 本地缓存的 action 引用损坏了,无法快进更新。
解决:改用完整 URL 引用 action,绕过本地缓存:
uses: https://github.com/actions/checkout@v4坑三:submodules: true 拉不到 submodule#
Blowfish 主题作为 git submodule 引入,checkout 时加了 submodules: true,但报错:
exitcode '1': failure(Fetching submodules 阶段)查了半天,发现仓库里根本没有 .gitmodules——主题是直接复制进去的,不是 submodule。submodules: true 找不到 submodule 就报错了。
解决:去掉 submodules: true。
坑四:Runner 根本连不上 GitHub#
解决了上面的问题后,改成真正的 submodule 方式引入 Blowfish,CI 又挂了:
read tcp 172.18.0.2:34458->198.18.0.51:443: read: connection reset by peerRunner 所在服务器无法访问 GitHub——被墙了。
这导致两个问题:
actions/checkout、peaceiris/actions-hugo这些 action 拉不到- Blowfish submodule 的源地址是 GitHub,也拉不到
解决方案:把所有 GitHub 依赖都镜像到自建 Gitea:
# 在 Gitea 上创建镜像仓库
# hermes/checkout ← actions/checkout
# hermes/actions-hugo ← peaceiris/actions-hugo
# hermes/blowfish ← nunocoracao/blowfishworkflow 里改用本地地址:
uses: https://git.ooya.site/hermes/checkout@v4
uses: https://git.ooya.site/hermes/actions-hugo@v2坑五:Gitea 镜像仓库是空的#
把 blowfish 镜像到 Gitea 时,因为仓库体积大(~400MB),migrate API 请求超时,仓库创建了但内容是空的。
CI 拉 submodule 时报:
warning: You appear to have cloned an empty repository.
fatal: remote error: upload-pack: not our ref e9699d8c5da9a64a105eb8401fd2cf79c6015c6fsubmodule 锁定了一个特定 commit hash,空仓库里当然找不到。
解决:放弃 submodule 机制,直接在 CI 里 clone:
- name: Clone Blowfish theme
run: |
git clone --depth 1 https://github.com/nunocoracao/blowfish.git themes/blowfish--depth 1 只拉最新一个 commit,速度快,不需要完整历史。Runner 这台服务器能访问 GitHub(不同于之前那台),这步顺利通过。
坑六:Hugo 构建出来没有 index.html#
第一次 CI 成功,访问网站却是 403。SSH 进服务器一看,current/ 目录里只有:
categories/
index.xml
sitemap.xml
tags/没有 index.html。
原因:仓库里根本没有文章内容,Hugo 只生成了分类和标签的 XML,不会生成首页 HTML。
解决:加了一篇文章,重新构建,这次有 index.html 了。
坑七:首页不显示文章列表#
站跑起来了,但首页空空的,文章不见踪影。
查 Blowfish 配置,找到两处问题:
params.toml里mainSections = ["docs"]——指向的是示例站的文档目录,我的文章放在posts/,改成mainSections = ["posts"]- 首页
layout = "custom",用的是示例站的自定义布局,改成layout = "profile"并开启showRecent = true
最终 workflow#
经过七个坑之后,最终跑通的 deploy.yml:
name: Deploy Hugo Site
on:
push:
branches:
- main
jobs:
build-and-deploy:
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: https://git.ooya.site/hermes/checkout@v4
with:
submodules: false
fetch-depth: 0
- name: Clone Blowfish theme
run: |
git clone --depth 1 https://github.com/nunocoracao/blowfish.git themes/blowfish
- name: Setup Hugo
uses: https://git.ooya.site/hermes/actions-hugo@v2
with:
hugo-version: 'latest'
extended: true
- name: Build
run: hugo --minify
- name: Deploy via SCP
env:
SSH_PRIVATE_KEY: ${{ secrets.DEPLOY_SSH_KEY }}
run: |
COMMIT_ID=${{ gitea.sha }}
RELEASE_DIR=/var/www/ooya.site/release/${COMMIT_ID}
CURRENT_LINK=/var/www/ooya.site/current
mkdir -p ~/.ssh
echo "$SSH_PRIVATE_KEY" > ~/.ssh/deploy_key
chmod 600 ~/.ssh/deploy_key
ssh-keyscan -H <服务器IP> >> ~/.ssh/known_hosts
ssh -i ~/.ssh/deploy_key root@<服务器IP> "mkdir -p ${RELEASE_DIR}"
scp -i ~/.ssh/deploy_key -r public/* root@<服务器IP>:${RELEASE_DIR}/
ssh -i ~/.ssh/deploy_key root@<服务器IP> "ln -sfn ${RELEASE_DIR} ${CURRENT_LINK}"香港服务器 Nginx 配置#
server {
listen 443 ssl http2;
server_name ooya.site www.ooya.site;
ssl_certificate /etc/nginx/ssl/ooya.site.pem;
ssl_certificate_key /etc/nginx/ssl/ooya.site.key;
root /var/www/ooya.site/current;
index index.html;
location / {
try_files $uri $uri/ =404;
}
}current 是软链接,指向最新 release 目录。回滚时:
ln -sfn /var/www/ooya.site/release/<上一个commit-id> /var/www/ooya.site/current总结#
整个过程最大的教训是:自托管环境的网络限制是隐形的。在本地测通的 CI 脚本,放到 Runner 上可能因为访问不了 GitHub 而全部失败。解决方法就是把所有外部依赖都镜像到内网。
搭完这套之后,日常发文章的流程变成:
- 在本地写 Markdown
git push- 等一分钟,站自动更新
这就是为什么值得花时间把 CI/CD 调通——一次投入,长期省事。
这套系统的搭建经验后续会整理成教程包,如果你也想搭一套自己的,关注后续更新。


