为什么要自建#
微信公众号写东西要审核,CSDN 满屏广告,掘金算法越来越难懂。我就想有个自己的地方,随便写,能卖东西,SEO 友好,域名和服务器都自己管。
Hugo 静态站是这条路的标准答案:构建快、部署简单、完全静态没有运行时漏洞。代码托管在自建 Gitea,VPS 用的香港机器(不用备案)。
目标很简单:本地写完,push 一下,自动构建部署上线。
但这条路走下来,整整折腾了一天。
整体架构#
本地写文章
↓ git push
Gitea(自建)
↓ 触发 Actions
Act Runner(ARM 服务器)
↓ hugo --minify 构建
↓ rsync 上传
香港 VPS
/var/www/ooya.site/release/<commit-id>/ ← 每次构建独立目录
/var/www/ooya.site/current → 软链接指向最新 release
↓
Nginx → 公网版本化 release + 软链接的好处:回滚只需要一条命令,Nginx 不用重启。
踩坑记录#
坑一:Set up job 直接挂,GitHub 根本连不上#
第一次跑 CI,日志里还没到 Checkout 步骤就死了:
read tcp ... read: connection reset by peer原因是 Gitea Act Runner 拉取 actions/checkout@v4 时要访问 GitHub,国内服务器直接被墙。
解法:把常用 Actions 镜像到本地 Gitea。
# 用 Gitea API migrate(mirror=true 自动同步)
curl -X POST https://git.yoursite.com/api/v1/repos/migrate \
-H "Authorization: token YOUR_TOKEN" \
-H "Content-Type: application/json" \
-d '{
"clone_addr": "https://github.com/actions/checkout",
"repo_name": "checkout",
"uid": YOUR_USER_ID,
"mirror": true,
"private": false
}'actions/checkout、peaceiris/actions-hugo、actions/cache 三个都要镜像。workflow 里改用本地地址:
uses: https://git.yoursite.com/YOUR_USER/checkout@v4坑二:git push 报 413,推不上去#
把仓库(含主题文件)推到 Gitea,报:
error: RPC failed; HTTP 413 curl 22 The requested URL returned error: 413Gitea 前面的 Nginx 默认 client_max_body_size 1m,稍微大点的仓库直接拒掉。
# /etc/nginx/conf.d/gitea.conf,server 块里加一行
client_max_body_size 200m;nginx -s reload坑三:submodule 困局,绕了一圈放弃#
主题用 git submodule 管理,理论上 CI checkout 时加 submodules: true 就能把主题一起拉下来。
但实际情况是:checkout action 的 submodules: true 不遵守 .gitmodules 里的 URL 覆盖,你就算把 submodule URL 改成 Gitea 本地地址,它内部仍然去请求 GitHub,还是被墙。
试了手动 git config submodule.xxx.url 然后 init,但 Gitea migrate 大仓库(blowfish ~400MB)超时,镜像过去是个空仓库。
最后决定:彻底移除 submodule,改成 CI 里直接 clone 主题到缓存目录。
# 清理本地 submodule
git rm --cached themes/blowfish
git commit -m "chore: 移除 submodule"
git pushworkflow 里改成:
- name: Prepare theme
run: |
rm -rf themes/blowfish
mkdir -p themes
if [ -d /cache/blowfish/layouts ]; then
echo "Cache hit"
cp -r /cache/blowfish themes/blowfish
else
git clone --depth 1 https://github.com/nunocoracao/blowfish.git /cache/blowfish
cp -r /cache/blowfish themes/blowfish
fi坑四:cp 嵌套目录,主题 layouts 全消失#
上面那段 cp -r /cache/blowfish themes/blowfish,第一次跑没问题,第二次 Hugo 报:
found no layout file for "html" for kind "home"进容器一看,themes/blowfish/blowfish/layouts/ ——嵌套了一层!
原因:cp -r src dst,如果 dst 已存在,src 会被整体拷进去,变成 dst/blowfish/。
解法:cp 前先 rm -rf:
rm -rf themes/blowfish # 先清,防止嵌套
cp -r /cache/blowfish themes/blowfish加一行验证:
echo "layouts: $(ls themes/blowfish/layouts/ 2>/dev/null | wc -l) files"坑五:站点 403,根目录没有 index.html#
构建成功,部署成功,打开网站:403。
看 release 目录,里面是 zh-cn/、en/、de/… 一堆语言子目录,就是没有根目录的 index.html。
原因:Blowfish exampleSite 默认带了七八种语言配置,Hugo 多语言模式下每种语言输出到各自子目录,根目录为空,Nginx 找不到文件就 403。
解法(推荐单语言中文):
config/_default/下只保留languages.zh-cn.toml,删掉其他语言文件hugo.toml加上:
defaultContentLanguage = "zh-cn"
defaultContentLanguageInSubdir = false注意:languages.zh-cn.toml 必须保留,Blowfish 需要它查找语言参数,完全删掉会报 found no layout file。
坑六:切单语言后,内容全没了#
按照坑五的方案切换完,构建页面数直接变 0。
原因:之前参考 exampleSite,内容文件全是 _index.zh-cn.md、post.zh-cn.md 这种带语言后缀的格式。单语言模式下 Hugo 只认不带后缀的文件名。
# 批量重命名
find content -name '*.zh-cn.md' | while read f; do
git mv "$f" "${f/.zh-cn.md/.md}"
done
git commit -m "fix: 去掉语言后缀,适配单语言模式"_index.zh-cn.md(首页)也要改成 _index.md,否则没有首页。
坑七:Hugo 版本太低,Blowfish 用了 try 函数#
actions-hugo 默认拉的版本太旧,构建报:
shortcodes/ansible.html:52: function "try" not definedBlowfish 最新版(2026年5月)依赖 Hugo 0.158.0 引入的 try 函数。用 0.136、0.147 都不行,必须 ≥ 0.158.0。
所以放弃 peaceiris/actions-hugo,改成手动下载指定版本:
- name: Setup Hugo
run: |
if [ -f /cache/hugo/hugo ]; then
ln -sf /cache/hugo/hugo /usr/local/bin/hugo
else
mkdir -p /cache/hugo
wget -q "https://github.com/gohugoio/hugo/releases/download/v0.158.0/hugo_extended_0.158.0_linux-amd64.tar.gz" -O /tmp/hugo.tar.gz
tar -xzf /tmp/hugo.tar.gz -C /cache/hugo hugo
ln -sf /cache/hugo/hugo /usr/local/bin/hugo
fi
hugo version坑八:actions/cache 始终 miss,两个配置坑#
用 actions/cache 缓存主题和 Hugo 二进制,结果每次 CI 都是 miss,每次重新下载。
排查下来,两个问题同时存在:
问题 A:CONFIG_FILE 环境变量未设
docker-compose 挂载了 config.yaml,但如果没有设 CONFIG_FILE=/config.yaml 环境变量,Runner 根本不读这个文件,用的是内置默认配置,cache 配置全部失效。
问题 B:cache.host 未配置
cache.host 为空时,job 容器连不上宿主机的缓存服务,静默降级为 miss,不报任何错误。
cache.host 的值取决于 container.network 设置:
container.network = "host":填宿主机 IP(如192.168.123.130)- 默认网络:填 Runner 容器 IP(
docker inspect查)
坑九:rsync 双端协议,远端也要装#
Deploy 步骤报:
bash: line 1: rsync: command not found
rsync error: error in rsync protocol data stream (code 12)rsync 是双端协议,本地和远端都必须安装。job 容器是精简 ubuntu 镜像,远端 VPS 是全新 Debian,两边都没有。
# job 容器里
apt-get update -q && apt-get install -y rsync -q
# 远端服务器(SSH 确认/安装,幂等)
ssh -i ~/.ssh/deploy_key root@YOUR_SERVER "which rsync || apt-get install -y rsync -q"最终效果#
所有坑踩完,流水线跑通:push main 后约 45 秒构建完成,rsync 上传,软链接切换,Nginx 无缝更新。
✓ Checkout 3s
✓ Prepare theme 2s (cache hit)
✓ Setup Hugo 1s (cache hit)
✓ Build 8s
✓ Deploy via rsync 18s
Total ~35s回滚只需要一条命令:
ln -sfn /var/www/ooya.site/release/<old-commit-id> /var/www/ooya.site/current完整部署教程#
下面是可以照抄的完整流程,按顺序做就行。
前置条件#
- 自建 Gitea 实例(带管理员账号)
- 一台能 24/7 在线的服务器跑 Act Runner(内网机器即可)
- 香港 / 境外 VPS + Nginx(部署目标)
- Hugo 站代码(Blowfish 主题)
步骤 1:镜像 GitHub Actions 到 Gitea#
Runner 所在服务器访问不了 GitHub,需要把用到的 Actions 镜像到本地。
GITEA_URL="https://git.yoursite.com"
TOKEN="YOUR_GITEA_TOKEN"
USER_ID=1 # 你的用户 ID,Gitea 管理员界面可查
for REPO in "actions/checkout" "actions/cache" "peaceiris/actions-hugo"; do
REPO_NAME=$(echo $REPO | cut -d'/' -f2)
curl -s -X POST "${GITEA_URL}/api/v1/repos/migrate" \
-H "Authorization: token ${TOKEN}" \
-H "Content-Type: application/json" \
-d "{
\"clone_addr\": \"https://github.com/${REPO}\",
\"repo_name\": \"${REPO_NAME}\",
\"uid\": ${USER_ID},
\"mirror\": true,
\"private\": false
}"
echo "Migrated: ${REPO_NAME}"
done等待同步完成(进 Gitea 看仓库有没有 tag v4 / v2 / v4)。
步骤 2:生成 Deploy Key#
ssh-keygen -t ed25519 -C "gitea-deploy@yoursite.com" -f /tmp/deploy_key -N ""
# 公钥追加到目标 VPS(用 >>,不要用 >)
cat /tmp/deploy_key.pub | ssh root@YOUR_VPS_IP "cat >> ~/.ssh/authorized_keys"
# 私钥内容:在本地终端执行 cat /tmp/deploy_key,手动复制进 Gitea 仓库 → Settings → Secrets → Actions → 新建 DEPLOY_SSH_KEY,粘贴私钥全文(含头尾 -----BEGIN/END-----)。
步骤 3:VPS 目录结构 + Nginx#
# VPS 上执行
mkdir -p /var/www/yoursite/releaseNginx 配置:
server {
listen 80;
server_name www.yoursite.com;
root /var/www/yoursite/current;
index index.html;
location / {
try_files $uri $uri/ =404;
}
}Nginx 默认跟随软链接,current 指向哪里就用哪里,无需重启。
步骤 4:部署 Act Runner#
docker-compose.yml(完整配置):
services:
gitea-runner:
image: gitea/act_runner:latest
container_name: gitea-runner
restart: unless-stopped
environment:
- CONFIG_FILE=/config.yaml # 必须设,否则挂载的 config.yaml 被忽略
- GITEA_RUNNER_REGISTRATION_TOKEN=YOUR_TOKEN # 首次注册用,成功后可删除
volumes:
- /var/run/docker.sock:/var/run/docker.sock
- ./config.yaml:/config.yaml
- ./data:/data # 持久化 .runner 和缓存
ports:
- "8088:8088" # 缓存服务端口config.yaml(完整配置):
runner:
file: /data/.runner # 持久化,重建容器不用重新注册
cache:
enabled: true
dir: /data/actcache
host: "192.168.x.x" # 宿主机 IP(container.network=host 时)
port: 8088
container:
network: "host"获取 registration token:Gitea → /-/admin/actions/runners → 创建新运行器 → 复制 token。
docker compose up -d
docker logs gitea-runner # 看到 "Runner registered successfully" 就好了注册成功后可以从 compose 文件删掉 GITEA_RUNNER_REGISTRATION_TOKEN,之后重启直接读 /data/.runner。
步骤 5:deploy.yml#
在仓库根目录创建 .gitea/workflows/deploy.yml:
name: Deploy Hugo Site
on:
push:
branches:
- main
jobs:
build-and-deploy:
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: https://git.YOURSITE/YOUR_USER/checkout@v4 # 改成你的 Gitea 地址
with:
submodules: false
fetch-depth: 0
- name: Prepare Blowfish theme
run: |
rm -rf themes/blowfish
mkdir -p themes
if [ -d /cache/blowfish/layouts ]; then
echo "Cache hit"
cp -r /cache/blowfish themes/blowfish
else
echo "Cache miss, cloning..."
git clone --depth 1 https://github.com/nunocoracao/blowfish.git /cache/blowfish
cp -r /cache/blowfish themes/blowfish
fi
echo "layouts: $(ls themes/blowfish/layouts/ 2>/dev/null | wc -l) files"
- name: Setup Hugo
run: |
if [ -f /cache/hugo/hugo ]; then
echo "Hugo cache hit"
ln -sf /cache/hugo/hugo /usr/local/bin/hugo
else
echo "Hugo cache miss, downloading..."
mkdir -p /cache/hugo
wget -q "https://github.com/gohugoio/hugo/releases/download/v0.158.0/hugo_extended_0.158.0_linux-amd64.tar.gz" -O /tmp/hugo.tar.gz
tar -xzf /tmp/hugo.tar.gz -C /cache/hugo hugo
ln -sf /cache/hugo/hugo /usr/local/bin/hugo
fi
hugo version
- name: Build
run: hugo --minify
- name: Deploy via rsync
env:
SSH_PRIVATE_KEY: ${{ secrets.DEPLOY_SSH_KEY }}
run: |
apt-get update -q && apt-get install -y rsync -q
COMMIT_ID=${{ gitea.sha }}
RELEASE_DIR=/var/www/YOUR_SITE/release/${COMMIT_ID} # 改成你的路径
CURRENT_LINK=/var/www/YOUR_SITE/current
mkdir -p ~/.ssh
echo "$SSH_PRIVATE_KEY" > ~/.ssh/deploy_key
chmod 600 ~/.ssh/deploy_key
ssh-keyscan -H YOUR_VPS_IP >> ~/.ssh/known_hosts # 改成你的 VPS IP
ssh -i ~/.ssh/deploy_key root@YOUR_VPS_IP "which rsync || apt-get install -y rsync -q"
ssh -i ~/.ssh/deploy_key root@YOUR_VPS_IP "mkdir -p ${RELEASE_DIR}"
rsync -az --delete -e "ssh -i ~/.ssh/deploy_key" public/ root@YOUR_VPS_IP:${RELEASE_DIR}/
ssh -i ~/.ssh/deploy_key root@YOUR_VPS_IP "ln -sfn ${RELEASE_DIR} ${CURRENT_LINK}"
echo "Deployed to ${RELEASE_DIR}"需要替换的变量:
git.YOURSITE/YOUR_USER→ 你的 Gitea 域名和用户名YOUR_VPS_IP→ 目标服务器 IP/var/www/YOUR_SITE→ 网站根目录路径
步骤 6:Hugo 配置关键项#
config/_default/hugo.toml:
baseURL = 'https://www.yoursite.com/'
title = '网站标题'
theme = 'blowfish'
defaultContentLanguage = "zh-cn"
defaultContentLanguageInSubdir = false # 必须加,否则中文内容进子目录,根目录 403config/_default/ 目录下:
- 只保留
languages.zh-cn.toml,删掉languages.en.toml、languages.de.toml等 languages.zh-cn.toml不能删,Blowfish 需要它
内容文件命名:不带语言后缀,用 _index.md、post.md,不是 _index.zh-cn.md。
步骤 7:验证和回滚#
推送后进 Gitea → 仓库 → Actions,看构建日志。正常情况约 40s 完成。
回滚:
# VPS 上执行,把软链接指向旧版本
ln -sfn /var/www/yoursite/release/<old-commit-id> /var/www/yoursite/current查看所有历史版本:
ls -lt /var/www/yoursite/release/整个流程通了之后,日常写文章就是:
git add . && git commit -m "新文章" && git push喝杯水回来,网站更新好了。


