跳过正文
  1. AI 技术/

用 Gitea CI/CD 自动部署 Hugo 静态站——我踩过的 13 个坑

8 分钟· loading · ·
作者
清幽
分享自托管、副业、被动收入的实战经验
目录

为什么要自建
#

微信公众号写东西要审核,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/checkoutpeaceiris/actions-hugoactions/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: 413

Gitea 前面的 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 push

workflow 里改成:

- 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。

解法(推荐单语言中文):

  1. config/_default/ 下只保留 languages.zh-cn.toml,删掉其他语言文件
  2. hugo.toml 加上:
defaultContentLanguage = "zh-cn"
defaultContentLanguageInSubdir = false

注意languages.zh-cn.toml 必须保留,Blowfish 需要它查找语言参数,完全删掉会报 found no layout file


坑六:切单语言后,内容全没了
#

按照坑五的方案切换完,构建页面数直接变 0。

原因:之前参考 exampleSite,内容文件全是 _index.zh-cn.mdpost.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 defined

Blowfish 最新版(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/release

Nginx 配置:

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   # 必须加,否则中文内容进子目录,根目录 403

config/_default/ 目录下:

  • 只保留 languages.zh-cn.toml,删掉 languages.en.tomllanguages.de.toml
  • languages.zh-cn.toml 不能删,Blowfish 需要它

内容文件命名:不带语言后缀,用 _index.mdpost.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

喝杯水回来,网站更新好了。

相关文章