用 Git + 轻量 CI 自动部署到 VPS:webhook / Drone / Gitea 实战

每次 push 自动上线,告别手动 scp。讲清 GitHub webhook 触发脚本、Drone 流水线、自建 Gitea(Gitea Actions / act_runner)私仓方案,以及零停机回滚与备份。

你有没有过这种经历:本地改完代码,git commit、git push,然后 ssh 登录服务器,手动 git pull,再重启服务,生怕哪一步敲错把线上搞挂。一次两次还行,天天这么干不仅浪费时间,还容易在半夜发布时手抖。其实只要把"push 之后自动上线"这条链路打通,部署就从一件需要集中注意力的大事,变成一次普通的 git push。

更进一步,你可能会问:代码和流水线本身,为什么非要放在别人的平台上?大多数团队一开始都直接用 GitHub,因为它方便、生态成熟。但随着仓库变多、私有库变多、CI 跑得越来越频繁,三个痛点会浮现:私有库 Actions 免费额度有限、超额按分钟计费,高频构建的小团队月底账单线性上涨;代码数据躺在别人的服务器上,数据主权不在自己手里;想按自己的节奏调流水线却受限于平台。更微妙的是,2025 年底 GitHub 曾释放出对私有仓库的自托管 runner 也要收费的信号,虽然随后推迟,但已经提醒很多人:连用自己的机器跑 CI 都可能被平台纳入计费。于是"数据自有 + 分钟不计费"的自建方案,价值被重新看见。

这篇文章不讲那些需要专职运维、搭一堆 Kubernetes 的重型方案,而是聚焦在预算型 VPS(比如 RackNerdContaboVultr 这类几美元一个月的机器)上,用 Git 配合轻量 CI 就能跑起来的自动部署实战:从"push 自动拉代码"的最小脚本,到 Drone 流水线,再到把代码仓库和 CI 一起收回到自己 VPS 上的 Gitea 私仓,最后落到零停机部署、回滚与备份。

延伸阅读

更多相关攻略推荐:Ollama AI系列(2):VPS上的AI推理与API应用Ollama AI系列(2):VPS上的AI推理与API应用ARM / Ampere 席卷 VPS:性价比真香还是兼容陷阱?【VPS 硬件选型指南 (内存 篇) 02】大内存 VPS 能干嘛?2026 独立站 PCI-DSS 自查清单:SAQ A 还是 A-E

一、为什么要从手动部署走向自动部署

手动部署最大的问题不是"慢",而是"不可重复、不可追溯"。你这次登录服务器敲了七条命令,下周换个网络环境、换台电脑,很可能就忘了其中两步,于是线上跑起了和本地不一样的版本。更糟的是,手动发布没有审计记录——哪次发布引入了 bug,回查时只能靠记忆。自动部署把"发布"变成一段可版本化、可重放的代码:同样的输入永远得到同样的输出,每次上线在 CI 日志里都有据可查。

对预算型 VPS 来说,自动部署还有一个容易被忽视的好处:它把"发布风险"从人的状态里抽离出来。你不用在精神最好的白天专门排一个发布窗口,半夜合进 main 分支,第二天早上自然就上线了。下面我们从最小可用方案讲起,一步步加料到生产级。

二、最小可用方案:GitHub webhook 触发部署脚本

如果你只想解决"push 之后服务器自动拉代码"这个核心痛点,其实不需要任何 CI 平台。GitHub 的 webhook 加上一个小小的接收程序就够了。社区里最常用的是 adnanh/webhook:它监听一个端口,收到 GitHub 打过来的 POST 请求后,按你写的规则执行本地脚本。

先在 VPS 上装好 webhook,写一个 hooks.json 描述哪个路径触发哪个命令:

[
  {
    "id": "deploy-app",
    "execute-command": "/home/deploy/deploy.sh",
    "command-working-directory": "/home/deploy",
    "trigger-rule": {
      "match":
        {
          "type": "payload-hash-sha256",
          "secret": "这里填你的随机字符串",
          "parameter":
            {
              "source": "header",
              "name": "X-Hub-Signature-256"
            }
        }
    }
  }
]

然后写 deploy.sh,里面就是原本你手动敲的那几步:

#!/usr/bin/env bash
set -e
cd /var/www/app
git pull --ff-only
npm ci --omit=dev
npm run build
systemctl restart myapp
echo "deploy done at $(date)" >> /var/www/deploy.log

启动 webhook:webhook -hooks hooks.json -port 9000 -verbose,再用 Nginx 或 Caddy 把 https://你的域名/hooks/deploy-app 反代到本机 9000。最后在 GitHub 仓库的 Settings → Webhooks 里填上这个 URL,Content type 选 application/json,Secret 填和上面一致的随机串,只订阅 push 事件。注意三点:第一,deploy 用专用低权限账号,不要拿 root 跑;第二,靠 X-Hub-Signature-256 校验来源,别裸奔在公网;第三,如果服务器在国内,GitHub 的 webhook 偶尔抽风,可以加个定时 git pull 兜底。

三、进阶方案:Drone CI 流水线

当项目开始需要"先跑测试再上线""不同分支走不同环境""构建成镜像再推送"时,纯脚本就不够了,这时候上 Drone 正合适。Drone 是开源的、配置即代码的 CI,核心就两个容器:drone-server 负责调度和页面,drone-runner-docker 负责真正干活。两者用一个共享密钥 DRONE_RPC_SECRET 通信,runner 必须挂上宿主机的 /var/run/docker.sock 才能为每一步起容器。

用 GitHub 登录需要在 GitHub 建一个 OAuth App,拿到 Client ID 和 Client Secret,回调地址填 https://ci.你的域名/login。一份最小 docker-compose.yml 长这样:

services:
  drone-server:
    image: drone/drone:2
    restart: always
    ports:
      - "8080:80"
    volumes:
      - ./data:/data
    environment:
      DRONE_GITHUB_CLIENT_ID: 你的client_id
      DRONE_GITHUB_CLIENT_SECRET: 你的client_secret
      DRONE_RPC_SECRET: 共享随机串
      DRONE_SERVER_HOST: ci.你的域名
      DRONE_SERVER_PROTO: https
      DRONE_USER_CREATE: username:你的github名,admin:true

  drone-runner:
    image: drone/drone-runner-docker:1
    restart: always
    depends_on:
      - drone-server
    volumes:
      - /var/run/docker.sock:/var/run/docker.sock
    environment:
      DRONE_RPC_HOST: drone-server
      DRONE_RPC_PROTO: http
      DRONE_RPC_SECRET: 共享随机串
      DRONE_RUNNER_CAPACITY: "2"

密钥用 openssl rand -hex 16 生成。仓库根目录放一个 .drone.yml,典型的"测试 + 部署"两条流水线:

kind: pipeline
type: docker
name: default

steps:
  - name: test
    image: node:20-alpine
    commands:
      - npm ci
      - npm test

  - name: deploy
    image: alpine:latest
    environment:
      SSH_KEY:
        from_secret: deploy_ssh_key
    commands:
      - echo "$SSH_KEY" > /tmp/key
      - chmod 600 /tmp/key
      - apk add --no-cache openssh-client
      - ssh -i /tmp/key -o StrictHostKeyChecking=no deploy@服务器 './deploy.sh'
    when:
      branch:
        - main

敏感信息(比如 SSH 私钥)存在 Drone 后台的 Secret 里,流水线用 from_secret 引用,永远不会进日志。用 when.branch 可以把"部署"限制在主分支,feature 分支只跑测试不碰线上。跑测试这一步尤其值:以前你本地过了就敢推,现在 CI 先替你拦一道,红绿一目了然。

四、自建私有仓库与 CI:在 VPS 上跑 Gitea

如果你连代码都不想放在 GitHub 上,或者团队有合规要求,Gitea 是预算 VPS 上最轻量的选择。它是 Go 写的,二进制单文件、依赖少、启动快,提供你期望的代码托管核心能力:Web 界面、仓库管理、Issues、Pull Request、Wiki、SSH 与 HTTP 克隆,以及可选的 Actions 流水线开关。

它最打动预算敏感用户的是内存占用:社区普遍实测空闲 150 到 300MB,很多人把整站跑在 1GB 内存的 VPS 上毫无压力;有测评在 Hetzner $4.50/月的机器上实测 Gitea 空闲约 180MB。相比之下,GitLab 社区版官方建议 4GB 起步,资源差距是数量级的。数据库上,Gitea 内置 SQLite 就够小团队用、零额外运维;想要更高并发或多仓库高频跑 Actions,可以换 PostgreSQL 或 MySQL——这种"先 SQLite 跑着、以后再说"的弹性,正是它适合新手的原因。

把 Gitea 和托管 GitHub 摆在一起,差距在成本、隐私、功能三处最明显:GitHub 省心但按量花钱、数据在别人那;Gitea 自己扛运维但分钟免费、数据自有。一句话,如果你在意数据主权、又不想被构建分钟数绑架,自建的性价比会随构建量上升而越来越明显。

维度Gitea(自建)GitHub(托管)
代码数据归属在你自己的机器在 GitHub 服务器
CI 分钟计费不计费,跑自己机器私有库免费额度有限,超额约 $0.008/分钟
自托管 runner天然支持,免费曾拟对私有库收费 $0.002/分钟,后推迟
内存门槛1GB 可跑,空闲约 150–300MB无需自运维
生态够用,第三方 CI 可接海量 Actions 市场、协作功能完整
运维责任自己负责备份、更新、HTTPS平台负责

配合 PostgreSQL 16 做元数据库,再套一层 Caddy 反代自动签 HTTPS,就是一个功能接近 GitHub 的私仓。关键是把配置写进环境变量而不是手改 ini。一份生产向的 compose 要点:

services:
  gitea:
    image: gitea/gitea:1.22
    restart: unless-stopped
    environment:
      GITEA__server__DOMAIN: git.你的域名
      GITEA__server__ROOT_URL: https://git.你的域名/
      GITEA__server__SSH_DOMAIN: git.你的域名
      GITEA__server__SSH_PORT: "2222"
      GITEA__server__HTTP_PORT: "3000"
      GITEA__actions__ENABLED: "true"
      GITEA__actions__DEFAULT_ACTIONS_URL: https://github.com
      GITEA__database__DB_TYPE: postgres
      GITEA__database__HOST: db:5432
      GITEA__database__NAME: gitea
      GITEA__database__USER: gitea
      GITEA__database__PASSWD: 强密码
    ports:
      - "127.0.0.1:3000:3000"
      - "2222:22"
    volumes:
      - gitea_data:/data

  db:
    image: postgres:16-alpine
    restart: unless-stopped
    environment:
      POSTGRES_DB: gitea
      POSTGRES_USER: gitea
      POSTGRES_PASSWORD: 强密码
    volumes:
      - pg_data:/var/lib/postgresql/data

volumes:
  gitea_data:
  pg_data:

首次访问 https://git.你的域名 走安装向导,设好管理员账号即可。几个要点:SSH 端口映射到宿主的 2222,克隆时用 git clone ssh://git@git.你的域名:2222/用户/仓库.git;装完立刻 GITEA__service__DISABLE_REGISTRATION=true 关掉公开注册,账号用管理员后台建;.env 文件 chmod 600 收紧权限;数据库务必放本地 SSD,别挂网络盘。把 GITEA__actions__ENABLED 设成 true 才能开 CI;DEFAULT_ACTIONS_URL 指向 GitHub 以复用官方 Action。Gitea 从 1.19 起自带 Actions,语法兼容 GitHub Actions,你已有的 workflow 文件几乎不用改就能在这儿跑 CI。

生产环境务必上 HTTPS。官方最推荐 Caddy,配置极简且自动申请续期 Let's Encrypt 证书:

git.你的域名 {
    reverse_proxy gitea:3000
}

若用 Nginx,要补大文件超时与 WebSocket 支持,否则克隆大仓库或长连接会断:

server {
    listen 443 ssl;
    server_name git.你的域名;
    client_max_body_size 0;
    location / {
        proxy_pass http://127.0.0.1:3000;
        proxy_set_header Host $http_host;
        proxy_set_header X-Forwarded-Proto $scheme;
        proxy_set_header X-Real-IP $remote_addr;
        proxy_read_timeout 310s;
        proxy_http_version 1.1;
        proxy_set_header Upgrade $http_upgrade;
        proxy_set_header Connection "upgrade";
    }
}

防火墙只开 80/443,Gitea 内部端口(3000/2222)不要裸暴露。SSH 克隆走 2222 时,记得在客户端用 ssh -p 2222,或在 ~/.ssh/config 里给域名指定端口。

CI 怎么接:Gitea Actions、Woodpecker 与 Drone

Gitea 的 CI 有两条路。第一条是内置的 Gitea Actions:自 v1.19 起正式可用,配置文件语法兼容 GitHub Actions 的 YAML,你写的 workflow 文件放在仓库的 .gitea/workflows/ 目录,几乎可以照搬 GitHub 的流水线。执行器是 act_runner,每个 job 起一个 Docker 容器做隔离,干净且可复现。第二条是第三方 CI:Woodpecker(Go 写、极轻,单实例空闲约 50MB,用 .woodpecker.yml,OAuth 接入 Gitea)和 Drone(本文第三节详述的老牌方案,drone.yml 配置),提供更专业 CI 体验但要额外维护一套服务。取舍建议:想最低迁移成本、直接复用 GitHub Actions 经验,选 Gitea Actions + act_runner;想更独立的 CI 产品或已经在用 Woodpecker/Drone,就接第三方。

维度Gitea ActionsWoodpeckerDrone
配置语法兼容 GitHub Actions YAML.woodpecker.ymldrone.yml
隔离方式Docker 容器Docker 容器Docker 容器
常驻资源runner 约 512MB–1GB约 50MB中等
接入内置OAuth 接 GiteaOAuth 接 Gitea
适合想直接搬 GitHub 流水线轻量独立 CI老牌成熟 CI

用 Gitea Actions 跑通一次 CI 分两步:写 workflow,再注册并启动 runner。先在仓库里建 .gitea/workflows/build.yml,语法和 GitHub Actions 几乎一样:

name: build
on: [push]
jobs:
  build:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - run: go build ./...

然后在 Gitea 的 Actions 页面拿注册令牌,在 runner 机器上注册(把仓库数据挂到 /srv/runner):

docker run --rm -v /srv/runner:/data actions/act_runner register --instance https://git.你的域名 --token <token>

注册成功后常驻启动 runner,并挂上宿主 Docker socket 让它能起 job 容器:

docker run -d -v /srv/runner:/data -v /var/run/docker.sock:/var/run/docker.sock actions/act_runner

注意:挂 docker.sock 意味着 runner 能操作宿主 Docker,属于信任边界,只对自己可信的仓库开。跑通一次 push,看到 Actions 面板绿灯,恭喜,你的私有 CI 已上线。这套流水线同样可以加一个 deploy 步骤,通过 SSH 到应用机执行 deploy.sh,把"测试 + 部署"收回到自建 VPS 上。

五、备份、迁移与 3-2-1

自建最怕机器没了、仓库也没了。Gitea 的备份要同时覆盖数据库和仓库数据。用 SQLite 时直接备份 db 文件即可:

sqlite3 /data/gitea/gitea.db ".backup /backup/gitea.db"
rsync -az /data/gitea /backup/gitea-data/

用 PostgreSQL 时改备 SQL:

docker exec gitea_db pg_dump -U gitea > /backup/gitea.sql

顺序别错:先备数据库或 SQL,再 rsync 仓库与配置目录,否则恢复时数据库指向不存在的对象会坏。runner 的 /srv/runner 缓存也要一并备,或允许它重建(重新注册即可)。记住 3-2-1 原则:三份副本、两种介质、一份离机,并定期演练恢复。备份顺序错等于不可恢复,别只 rsync 一份在本机就当保险。

六、回滚与零停机部署思路

自动部署一旦顺滑,最大的隐患就变成"上错了怎么退"。回滚必须和部署一起设计,别等出事才想。最简单粗暴的是在 deploy.sh 里先备份再覆盖:发布前把当前版本打成 tar 包,新版本拉取失败或健康检查不过就解包还原。更优雅的是"符号链接切换":服务器上维护 releases/时间戳Areleases/时间戳B 两个目录,current 是个软链指向其中一个;新版本构建到另一个目录,验证通过后把软链一瞬切过去,切错了切回来也是一瞬间。零停机则靠反向代理:蓝绿部署先起新容器、摘除旧容器流量再下线旧容器;纯静态站更可以用上面这种软链切换,用户完全无感。

把软链切换落成脚本其实很简洁,关键是"建新目录、验证、再切"三步分离:

#!/usr/bin/env bash
set -e
RELEASES=/var/www/releases
NEW="$RELEASES/$(date +%s)"
git clone --depth 1 /var/repo/app.git "$NEW"
cd "$NEW" && npm ci --omit=dev && npm run build
# 先对本地端口做健康检查,过不了直接删掉新目录并退出
curl -f http://127.0.0.1:3000/health || { echo "新版本不健康"; rm -rf "$NEW"; exit 1; }
ln -sfn "$NEW" /var/www/current
systemctl reload nginx
echo "已切换到 $NEW"

这个脚本的妙处是:current 软链任何时候都指向一个"已通过健康检查"的目录,回滚只需把软链指回上一个时间戳目录再 reload 一次。配合 Git 的浅克隆(--depth 1),每次发布都不用拉全量历史,几秒就能完成。把它接到 webhook、Drone 或 Gitea Actions 的 deploy 步骤里,发布和回滚就是同一条命令的两个方向,半夜合代码也能安心睡。

无论哪种方案,真正救命的是"先验证再切流量":部署脚本末尾加一个 curl 健康检查,返回非 200 就自动回退。把这套逻辑写进 Drone 的 deploy 步骤、Gitea Actions 的 deploy job 或 webhook 脚本里,半夜自动发布也能睡安稳。

七、选机器与线路(含大陆用户)

三款相关商家摆一起,算一笔私有 Git 加 CI 的月成本:Vultr 按小时计费、删机退余额,装不顺手就删,最适合试错;Contabo 的 16G 内存机型很香,一台机器既跑 Gitea 又跑多 runner 不慌;RackNerd 年付小机十几美元就能把 Gitea 跑起来,无信用卡用户用支付宝也友好。对比 GitHub Teams 按席位 $20/月,自建平摊下来约 $5–7/月,构建越多越值。给 runner 预留 1–2GB 内存(或单独跑一台小机器),避免和 Git 服务抢资源;高并发多仓库跑 Actions 时建议上 PostgreSQL 而非 SQLite。

对大陆自建党还有几个只有我们会纠结的点:线路上 Git/CI 对延迟敏感但更吃带宽,普通 BGP 能跑,体验稳比极致快重要;支付上 RackNerd 支持支付宝、Vultr 部分地区也支持;退款上 Vultr 按小时删机退余额最灵活。下单前看清退款与设置费条款。合规上本文只讲数据自有、内网协作等合规用途,访问权限、HTTPS、更新、备份都要自己负责。

延伸阅读:想从零搞懂 VPS 基础操作,先看 VPS 新手入门指南;部署脚本涉及 SSH 密钥和最小权限,补一补 VPS 安全加固基础;要把 Gitea、Drone 和你的应用一起编排,参考 VPS 自托管全家桶编排;想挑机器可对比 与 。