引言:为什么要在 VPS 上自建 Git + CI/CD?

对于很多个人开发者和小团队来说,GitHub、GitLab 等托管平台确实方便,但也有不少痛点:私有仓库有数量限制、Actions 免费额度不够用、代码托管在第三方总有隐私顾虑。更重要的是,当你维护一个自己的 VPS 上的项目时,每次改完代码还要 SSH 登录去拉取、构建、重启,这套流程重复几次就让人烦躁了。
好消息是,现在完全可以用一台配置不高的 VPS,自建一套媲美 GitHub 的完整开发工作流——代码托管、CI/CD 流水线、自动化部署一气呵成。本文将以 Gitea(轻量级 Git 服务)和 Drone CI(云原生的 CI/CD 引擎)为核心,从零搭建一套完整的自动化部署体系。
先看下最终效果:当你执行 git push 之后,Drone 自动检测到代码变更,拉取最新代码、运行测试、构建 Docker 镜像、SSH 到生产服务器部署——全程不需要人工介入。如果你用惯了 GitHub Actions,会发现 Drone 的体验几乎一模一样,但完全跑在你自己的硬件上。
方案选型:为什么是 Gitea + Drone CI?
自建 Git + CI/CD 的方案其实不少,我先帮你快速过一下主流的几个选项,方便你做判断:
| 方案 | Git 服务 | CI/CD 引擎 | 资源占用 | 上手难度 |
|---|---|---|---|---|
| Gitea + Drone CI | Gitea(Go 编写) | Drone(Go 编写) | 极低(1G 内存够用) | 低 |
| GitLab CE | GitLab(Ruby + Go) | 内置 CI/CD | 高(4G 内存起步) | 中 |
| Gogs + Woodpecker | Gogs(Go 编写) | Woodpecker(Go) | 极低 | 低 |
| GitHub AE | GitHub 私有实例 | GitHub Actions | 极高(企业级) | 低 |
GitLab CE 虽然功能最全,但对资源要求太高,1G 内存的 VPS 跑起来非常吃力。Gogs 比 Gitea 还要轻量,不过社区活跃度和插件生态不如 Gitea。至于 GitHub AE,那是企业级产品,个人用户基本不用考虑。
Gitea 是目前个人开发者和小团队自建 Git 服务的最优选择——它用 Go 编写,二进制文件不到 100MB,跑起来内存占用大概 200-300MB。Drone CI 同样用 Go 编写,通过 Docker 容器执行流水线任务,资源隔离性好,配置方式和 GitHub Actions 高度相似,学习成本很低。
环境准备与 Docker 部署

假设你已经有一台 VPS,系统是 Ubuntu 22.04 LTS,内存至少 1GB。首先安装 Docker 和 Docker Compose:
# 安装 Docker
curl -fsSL https://get.docker.com | bash
sudo usermod -aG docker $USER
newgrp docker
# 安装 Docker Compose
sudo apt install -y docker-compose-plugin
# 验证安装
docker --version
docker compose version
接下来创建项目目录结构:
mkdir -p ~/gitea-drone/{data/gitea,data/drone,data/caddy}
cd ~/gitea-drone
这里我用 Caddy 作为反向代理——它自动处理 HTTPS 证书,比 Nginx 配置简单得多。如果你更喜欢 Nginx,也可以用 Nginx Proxy Manager 替代。
用 Docker Compose 编排所有服务
创建一个 docker-compose.yml 文件,把所有服务定义在这里:
version: '3.8'
services:
# 1. Gitea - Git 服务
gitea:
image: gitea/gitea:latest
container_name: gitea
environment:
- USER_UID=1000
- USER_GID=1000
- GITEA__database__DB_TYPE=sqlite3
- GITEA__server__DOMAIN=git.yourdomain.com
- GITEA__server__ROOT_URL=https://git.yourdomain.com
- GITEA__server__SSH_PORT=2222
- GITEA__server__SSH_LISTEN_PORT=22
volumes:
- ./data/gitea:/data
- /etc/timezone:/etc/timezone:ro
- /etc/localtime:/etc/localtime:ro
ports:
- "2222:22"
restart: unless-stopped
# 2. Drone Server - CI/CD 控制面
drone-server:
image: drone/drone:latest
container_name: drone-server
environment:
- DRONE_GITEA_SERVER=https://git.yourdomain.com
- DRONE_GITEA_CLIENT_ID=your_client_id
- DRONE_GITEA_CLIENT_SECRET=your_client_secret
- DRONE_RPC_SECRET=your_shared_secret
- DRONE_SERVER_HOST=ci.yourdomain.com
- DRONE_SERVER_PROTO=https
- DRONE_USER_CREATE=username:yourusername,admin:true
volumes:
- ./data/drone:/data
restart: unless-stopped
depends_on:
- gitea
# 3. Drone Runner - CI/CD 执行端
drone-runner:
image: drone/drone-runner-docker:latest
container_name: drone-runner
environment:
- DRONE_RPC_PROTO=https
- DRONE_RPC_HOST=ci.yourdomain.com
- DRONE_RPC_SECRET=your_shared_secret
- DRONE_RUNNER_CAPACITY=2
- DRONE_RUNNER_NAME=runner-1
volumes:
- /var/run/docker.sock:/var/run/docker.sock
restart: unless-stopped
depends_on:
- drone-server
# 4. Caddy - 反向代理 + 自动 HTTPS
caddy:
image: caddy:latest
container_name: caddy
ports:
- "80:80"
- "443:443"
volumes:
- ./data/caddy:/data
- ./Caddyfile:/etc/caddy/Caddyfile
restart: unless-stopped
这个编排文件有几个关键点值得说明:
- Gitea 使用 SQLite 作为数据库——对于个人或小团队来说完全够用,不需要额外跑一个 MySQL 容器
- Gitea 的 SSH 端口映射到宿主机的 2222 端口,避免和宿主机原有的 SSH 冲突
- Drone Runner 挂载了
/var/run/docker.sock,这样它可以在容器内启动新的容器来执行 CI 任务 DRONE_RUNNER_CAPACITY=2表示同时最多并行执行 2 个流水线任务
配置 Caddy 反向代理
在 Caddyfile 中定义路由规则:
git.yourdomain.com {
reverse_proxy gitea:3000
}
ci.yourdomain.com {
reverse_proxy drone-server:80
}
Caddy 会自动从 Let’s Encrypt 申请和续期 SSL 证书,无需手动配置。把 yourdomain.com 换成你自己的域名,并且确保两个子域名的 A 记录都指向你的 VPS IP。
初始化 Gitea 并创建 OAuth 应用
启动所有服务:
cd ~/gitea-drone
docker compose up -d
# 查看启动日志
docker compose logs -f
启动后,访问 https://git.yourdomain.com,首次访问会看到安装向导。填写以下信息:
- 站点标题:随便填,比如 “My Git”
- 服务器域名:
git.yourdomain.com - SSH 服务器端口:
2222(注意,这是给 Gitea 用户看到的 SSH 端口) - HTTP 监听端口:
3000(保持默认,因为 Caddy 会转发) - 数据库:选择 SQLite3(最简单,不需要额外配置)
- 管理员账号:设置你的用户名和密码
安装完成后,登录 Gitea,然后创建一个 OAuth 应用供 Drone 使用:
进入 个人设置 → 应用 → 管理 OAuth2 应用,填写:
- 应用名称:Drone CI
- 重定向 URI:
https://ci.yourdomain.com/login
创建后会得到一个 Client ID 和 Client Secret,把它们填回到之前的 docker-compose.yml 中的 DRONE_GITEA_CLIENT_ID 和 DRONE_GITEA_CLIENT_SECRET。同时生成一个随机的 DRONE_RPC_SECRET(可以用 openssl rand -hex 16 生成)。
修改完成后,重新加载 compose 配置:
docker compose down
docker compose up -d
配置第一个 CI/CD 流水线

现在基础设施已经就绪,我们来创建一个实际项目体验完整的 CI/CD 流程。
假设我们有一个 Node.js 的 Web 应用,项目根目录创建 .drone.yml:
kind: pipeline
type: docker
name: default
trigger:
branch:
- main
- master
steps:
- name: install
image: node:20-alpine
commands:
- npm ci
- name: test
image: node:20-alpine
commands:
- npm run test
- name: build
image: node:20-alpine
commands:
- npm run build
- name: docker-build
image: plugins/docker
settings:
registry: registry.yourdomain.com
repo: yourusername/my-app
tags: latest
dockerfile: Dockerfile
when:
branch:
- main
- name: deploy
image: appleboy/drone-scp
settings:
host: your-production-server.com
username: deploy
password:
from_secret: ssh_password
port: 22
target: /opt/apps/my-app
source: ./dist/
when:
branch:
- main
这份流水线的执行逻辑很清晰:
- install:安装项目依赖
- test:运行测试
- build:构建项目
- docker-build:只有在 main 分支上,才构建 Docker 镜像
- deploy:通过 SCP 把构建产物部署到生产服务器
Drone 的流水线语法和 GitHub Actions 很相似,如果你用过 Actions,上手 Drone 基本不需要学习成本。每个 step 都是一个独立的 Docker 容器,互不干扰,环境隔离非常干净。
在 Drone 的管理界面中,你还需要为仓库配置 Secrets(密钥)。比如上面的 ssh_password,你需要去 Drone 的仓库设置里添加这个 secret,Drone 会在执行流水线时注入到容器中,但不会在日志中暴露明文。
高级配置:Webhook 自动触发与部署通知
Gitea 和 Drone 之间的集成是通过 Webhook 实现的。当你在 Gitea 中激活仓库的 Drone 插件后,Gitea 会自动在仓库中注册一个 Webhook——每次代码推送、PR 合并等事件发生时,Gitea 会通知 Drone 触发流水线。
你可以在 Gitea 仓库的 设置 → Webhooks 中看到自动创建的 Webhook。值得注意的一个优化是:你可以在 Webhook 的触发事件中只勾选 Push 事件 和 Pull Request 事件,去掉不必要的分支/标签删除等事件,减少不必要的流水线触发。
我们还可以加上部署完成后的通知功能,把 CI 结果推送到手机或聊天工具:
# 在 .drone.yml 末尾添加通知步骤
- name: notify-telegram
image: appleboy/drone-telegram
settings:
token:
from_secret: telegram_token
to:
from_secret: telegram_chat_id
message: >
{{#success build.status}}
✅ 部署成功:{{ repo.name }} ({{ build.branch }})
提交者:{{ build.author }}
提交信息:{{ build.message }}
构建耗时:{{since build.finished build.started}}
{{else}}
❌ 部署失败:{{ repo.name }} ({{ build.branch }})
请查看:{{ build.link }}
{{/success}}
配置好 Telegram Bot 后,每次构建完成你都会在手机上收到通知——成功了告诉你部署了什么,失败了直接给链接去排查。这套通知机制配合之前提到的 Prometheus 监控,基本上可以做到出了问题 5 分钟内就发现并定位。
Drone 与 Docker Registry 集成
如果你构建 Docker 镜像并存放到远端仓库,需要一个私有的 Docker Registry。最简单的方案是用 Docker 官方 Registry 镜像自建:
# 在 docker-compose.yml 中添加
registry:
image: registry:2
container_name: registry
environment:
- REGISTRY_STORAGE_DELETE_ENABLED=true
volumes:
- ./data/registry:/var/lib/registry
restart: unless-stopped
然后在 Caddyfile 中添加路由:
registry.yourdomain.com {
reverse_proxy registry:5000
}
不过需要注意,自建 Registry 如果暴露在公网上,一定要加上认证或使用 Token 鉴权,不然任何人都可以往你的仓库 push 镜像。推荐的做法是只在内网使用,或者通过 VPN(如 WireGuard)连接后访问。
对于不想自建 Registry 的用户,也可以用 Docker Hub 的私有仓库——Drone 的 docker 插件原生支持 Docker Hub 认证,只要在 Secrets 中配置 docker_username 和 docker_password 即可。
安全加固要点

把 Git 服务和 CI/CD 暴露在公网上,安全方面有几个必须要注意的点:
1. Gitea 的 SSH 端口不要使用默认的 22
上面我们在 compose 文件中已经将 Gitea 的 SSH 映射到了 2222 端口,这样可以避免与宿主机 SSH 服务的端口冲突。同时,在 Gitea 的配置中建议关闭密码登录,只允许 SSH Key 认证:
# Gitea 配置文件(在容器内 /data/gitea/conf/app.ini)
[service]
DISABLE_REGISTRATION = true
REQUIRE_SIGNIN_VIEW = true
ENABLE_NOTIFY_MAIL = true
2. Drone 的管理接口要限制访问
Drone 默认只在 /login 路径做 OAuth 认证,但管理 API 端点仍然需要保护。建议在 Caddy 层面添加 IP 白名单或基础认证:
# Caddyfile 中增加
@internal {
remote_ip 10.0.0.0/8 172.16.0.0/12 192.168.0.0/16
}
ci.yourdomain.com {
reverse_proxy drone-server:80
# 管理接口仅限内网 IP 访问
handle_path /api/* {
@internal_match @internal
handle @internal_match {
reverse_proxy drone-server:80
}
respond 403
}
}
3. 定期备份数据
Gitea 的所有数据(仓库、数据库、附件)都存储在 ~/gitea-drone/data/gitea/ 目录下,Drone 的流水线配置存储在 ~/gitea-drone/data/drone/。设置一个 cron 定时任务:
# crontab -e 添加
0 3 * * * tar -czf /backups/gitea-drone-$(date +\%Y\%m\%d).tar.gz -C ~/gitea-drone data/
0 5 * * * find /backups/ -name "gitea-drone-*" -mtime +30 -delete
4. 利用 Docker 网络隔离
compose 文件中的所有服务默认在同一个 Docker 网络中,Gitea、Drone、Caddy 之间可以互相通信。但不要把其他业务容器放到这个网络里——让 CI/CD 基础设施保持独立的网络命名空间,可以降低被横向移动攻击的风险。
性能优化与资源调优
对于 1G 内存的 VPS,跑 Gitea + Drone Server + Drone Runner + Caddy 四个容器,再加上可能同时运行的 CI 任务容器,资源还是比较紧张的。这里分享几个调优技巧:
- 限制 Drone Runner 的并行数:
DRONE_RUNNER_CAPACITY=1,一次只跑一个任务,防止内存被打满 - 使用 SQLite 而非 MySQL:Gitea 默认用 SQLite,对个人用户完全够用,节省一个数据库容器的开销
- 给 Docker 设置资源限制:在
/etc/docker/daemon.json中限制 Docker 的全局资源 - 启用 Gitea 的 Git GC:Gitea 后台会自动运行
git gc,但默认频率偏低,可以在配置中调高频率以控制仓库体积 - 使用 Drone 的缓存插件:每次 CI 都重新 npm install 很浪费时间,配置缓存后每次构建能节省 30-60 秒
# 在 .drone.yml 中添加缓存步骤
- name: restore-cache
image: meltwater/drone-cache
settings:
restore: true
mount:
- node_modules
backend: "filesystem"
cache_base: /cache
- name: rebuild-cache
image: meltwater/drone-cache
settings:
rebuild: true
mount:
- node_modules
backend: "filesystem"
cache_base: /cache
这需要把宿主机的 /cache 目录挂载到 Drone Runner 容器中。加上缓存后,第二次及以后的构建速度会有明显提升。
总结与扩展思路
通过本文的实践,我们在一台普通的 VPS 上完成了以下部署:
- Gitea:轻量级的 Git 代码托管服务
- Drone CI:容器化的 CI/CD 引擎
- Caddy:自动 HTTPS 的反向代理
- Docker Registry:私有镜像仓库
整套系统加起来的内存占用大约在 500-600MB(空闲状态下),1GB 的 VPS 完全可以胜任。如果再结合之前介绍过的 Prometheus + Grafana 监控方案,你就有了一套从代码提交到部署监控的完整 DevOps 闭环。
如果还想进一步扩展,可以考虑以下几个方向:
- 集成 SonarQube:在 CI 流程中加入代码质量检查
- 使用 Kubernetes 替代 Docker:当服务数量增多后,迁移到 K3s 可以更方便地管理容器编排
- 搭建 Drone 的 Runner 集群:多台机器注册到同一个 Drone Server,实现 CI 任务的水平扩展
- 引入 ArgoCD:实现 GitOps 风格的声明式部署
最后想说的是,自建 CI/CD 不是为了替代 GitHub/GitLab,而是在你需要更多控制权、更好的隐私保护、或者不想被免费额度限制时,给你一个完全自主的选择。动手试试吧,从 git push 到自动部署,这种”一条龙”的完成感是很有成就感的。
汤不热吧