使用Docker Compose在VPS上自托管Gitea
部署生产就绪的Gitea实例,包含PostgreSQL、22端口SSH透传、Gitea Actions CI/CD、Git LFS、GitHub镜像同步和自动备份。全部在一台VPS上通过Docker Compose完成。
Gitea是一个用Go编写的轻量级自托管Git服务。它在单个二进制文件中提供pull request、issue追踪、CI/CD、包注册表和Git LFS,空闲时仅占用约150 MB内存。对比GitLab最低4 GB的内存需求,你就明白为什么Gitea非常适合VPS了。
本指南使用Docker Compose和PostgreSQL在VPS上部署Gitea。你将配置SSH透传(passthrough),使git clone git@your-server:user/repo.git在22端口上正常工作,配置带有容器化Runner的Gitea Actions,启用Git LFS,从GitHub镜像仓库,设置webhook,并自动化备份。
安装Gitea之前需要什么?
你需要一台运行Debian 12或Ubuntu 24.04的VPS,已安装Docker和Docker Compose v2,一个指向服务器的域名,以及配置了TLS的反向代理(Nginx或Caddy)。本指南假设你已完成服务器基础加固:具有sudo权限的非root用户、SSH密钥认证、防火墙已启用。
| 前提条件 | 最低要求 |
|---|---|
| 操作系统 | Debian 12 / Ubuntu 24.04 |
| 内存 | 1 GB(配合CI Runner建议2 GB) |
| Docker | 27.x+,含Compose v2 |
| 域名 | A记录指向VPS IP |
| 反向代理 | 配置了TLS的Nginx或Caddy |
如何使用Docker Compose和PostgreSQL部署Gitea?
创建项目目录,在.env文件中生成密钥,然后在docker-compose.yml中定义Gitea和PostgreSQL服务。.env文件将凭据与版本控制和compose文件隔离。
创建项目目录
sudo mkdir -p /opt/gitea
sudo chown $USER:$USER /opt/gitea
cd /opt/gitea
生成密钥
openssl rand -base64 32 > /dev/null # test that openssl works
cat > .env << 'ENVFILE'
POSTGRES_USER=gitea
POSTGRES_PASSWORD=REPLACE_ME
POSTGRES_DB=gitea
GITEA_SECRET_KEY=REPLACE_ME
GITEA_INTERNAL_TOKEN=REPLACE_ME
GITEA_LFS_JWT_SECRET=REPLACE_ME
ENVFILE
将每个REPLACE_ME替换为真实密钥:
sed -i "s/^POSTGRES_PASSWORD=.*/POSTGRES_PASSWORD=$(openssl rand -base64 32)/" .env
sed -i "s/^GITEA_SECRET_KEY=.*/GITEA_SECRET_KEY=$(openssl rand -base64 32)/" .env
sed -i "s/^GITEA_INTERNAL_TOKEN=.*/GITEA_INTERNAL_TOKEN=$(openssl rand -base64 32)/" .env
sed -i "s/^GITEA_LFS_JWT_SECRET=.*/GITEA_LFS_JWT_SECRET=$(openssl rand -base64 32)/" .env
chmod 600 .env
Docker Compose文件
# /opt/gitea/docker-compose.yml
services:
gitea:
image: docker.gitea.com/gitea:1.25.5
container_name: gitea
environment:
- USER_UID=1000
- USER_GID=1000
- GITEA__database__DB_TYPE=postgres
- GITEA__database__HOST=db:5432
- GITEA__database__NAME=${POSTGRES_DB}
- GITEA__database__USER=${POSTGRES_USER}
- GITEA__database__PASSWD=${POSTGRES_PASSWORD}
- GITEA__server__DOMAIN=git.example.com
- GITEA__server__ROOT_URL=https://git.example.com/
- GITEA__server__SSH_DOMAIN=git.example.com
- GITEA__server__SSH_PORT=22
- GITEA__server__LFS_START_SERVER=true
- GITEA__server__LFS_JWT_SECRET=${GITEA_LFS_JWT_SECRET}
- GITEA__service__DISABLE_REGISTRATION=true
- GITEA__service__REQUIRE_SIGNIN_VIEW=false
- GITEA__security__SECRET_KEY=${GITEA_SECRET_KEY}
- GITEA__security__INTERNAL_TOKEN=${GITEA_INTERNAL_TOKEN}
- GITEA__actions__ENABLED=true
restart: always
volumes:
- gitea-data:/data
- /etc/timezone:/etc/timezone:ro
- /etc/localtime:/etc/localtime:ro
ports:
- "127.0.0.1:3000:3000"
depends_on:
db:
condition: service_healthy
networks:
- gitea
db:
image: docker.io/library/postgres:17
container_name: gitea-db
environment:
- POSTGRES_USER=${POSTGRES_USER}
- POSTGRES_PASSWORD=${POSTGRES_PASSWORD}
- POSTGRES_DB=${POSTGRES_DB}
restart: always
volumes:
- postgres-data:/var/lib/postgresql/data
networks:
- gitea
healthcheck:
test: ["CMD-SHELL", "pg_isready -U ${POSTGRES_USER} -d ${POSTGRES_DB}"]
interval: 10s
timeout: 5s
retries: 5
networks:
gitea:
external: false
volumes:
gitea-data:
postgres-data:
将git.example.com替换为你的实际域名。关键设计决策:
- PostgreSQL 17而非多数教程仍推荐的过时版本14
- 命名卷(
gitea-data、postgres-data)而非bind mount,管理更清晰 - PostgreSQL上的健康检查确保Gitea等待数据库就绪
- **
DISABLE_REGISTRATION=true**因为公开实例的开放注册会招致滥用 - **
ACTIONS__ENABLED=true**从一开始就启用Gitea Actions - 端口3000绑定到localhost(
127.0.0.1:3000:3000),只能通过反向代理访问,不能直接从互联网访问 - 容器上无SSH端口映射。SSH将通过宿主机透传处理(下一节)
启动服务栈
docker compose up -d
[+] Running 3/3
✔ Network gitea_gitea Created
✔ Container gitea-db Healthy
✔ Container gitea Started
docker compose ps
NAME IMAGE STATUS PORTS
gitea docker.gitea.com/gitea:1.25.5 Up 2 minutes 127.0.0.1:3000->3000/tcp
gitea-db postgres:17 Up 2 minutes (healthy) 5432/tcp
反向代理配置
将Gitea添加到现有的Nginx配置中:
server {
listen 443 ssl http2;
server_name git.example.com;
ssl_certificate /etc/letsencrypt/live/git.example.com/fullchain.pem;
ssl_certificate_key /etc/letsencrypt/live/git.example.com/privkey.pem;
server_tokens off;
client_max_body_size 512M;
location / {
proxy_pass http://127.0.0.1:3000;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
}
}
client_max_body_size 512M允许大文件推送,在Git LFS启用时尤其有用。server_tokens off隐藏响应头中的Nginx版本,因为版本泄露会帮助攻击者针对已知漏洞发起攻击。
sudo nginx -t && sudo systemctl reload nginx
创建管理员用户
docker exec -it gitea gitea admin user create \
--username admin \
--password "$(openssl rand -base64 16)" \
--email admin@example.com \
--admin \
--must-change-password
New user 'admin' has been successfully created!
将生成的密码保存在安全的地方(密码管理器,不是便签纸)。--must-change-password标志强制首次登录时修改密码。登录后,在Settings > Security > Two-Factor Authentication中启用双因素认证。
如何为Docker中的Gitea设置SSH透传?
在宿主机上创建一个与容器内UID相同的git用户。在/etc/ssh/sshd_config中添加AuthorizedKeysCommand块,调用docker exec gitea /usr/local/bin/gitea keys。这将宿主机22端口上的SSH git操作直接路由到容器内,无需暴露第二个SSH端口。
在宿主机上创建git用户
该用户需要UID 1000以匹配容器内部用户:
sudo adduser --system --shell /bin/bash --group --disabled-password --home /home/git --uid 1000 git
如果UID 1000已被你的常规用户占用,可以修改compose文件中的USER_UID/USER_GID为可用的UID,或调整此处的--uid标志。
创建shell包装器
git用户的shell需要将命令转发到容器内:
sudo tee /usr/local/bin/gitea-shell > /dev/null << 'WRAPPER'
#!/bin/sh
/usr/bin/docker exec -i --env SSH_ORIGINAL_COMMAND="$SSH_ORIGINAL_COMMAND" gitea sh "$@"
WRAPPER
sudo chmod 755 /usr/local/bin/gitea-shell
sudo usermod -s /usr/local/bin/gitea-shell git
ls -la /usr/local/bin/gitea-shell
-rwxr-xr-x 1 root root 99 Mar 20 10:00 /usr/local/bin/gitea-shell
将git用户添加到docker组
sudo usermod -aG docker git
这授予git用户执行docker exec的权限。在专用的Gitea服务器上这是可以接受的。在共享主机上,建议使用限定到特定docker exec命令的sudo规则。
配置sshd
在/etc/ssh/sshd_config末尾添加以下内容:
# Gitea SSH passthrough
Match User git
AuthorizedKeysCommandUser git
AuthorizedKeysCommand /usr/bin/docker exec -i gitea /usr/local/bin/gitea keys -c /etc/gitea/app.ini -e git -u %u -t %t -k %k
AuthorizedKeysCommand让Gitea容器验证SSH客户端提供的公钥是否属于已注册的Gitea用户。如果匹配,SSH将授权访问。Match User git块将此限制为仅git用户,不影响你的常规SSH访问。
sudo sshd -t
没有输出表示配置有效。如果看到错误,检查Match块中的拼写。
sudo systemctl restart sshd
测试SSH访问
通过Web界面将你的SSH公钥添加到Gitea账户(Settings > SSH/GPG Keys)。然后从本地机器:
ssh -T git@git.example.com
Hi there, admin! You've successfully authenticated with the key named "my-laptop", but Gitea does not provide shell access.
现在你可以通过22端口的SSH进行clone、push和pull:
git clone git@git.example.com:admin/my-repo.git
如何启用和配置Gitea Actions进行CI/CD?
Gitea Actions是内置的CI/CD系统,语法与GitHub Actions兼容。通过环境变量启用(我们的compose文件中已设置),部署Runner作为Docker Compose服务,然后在仓库中编写workflow YAML文件。
如何使用Docker Compose注册act_runner?
首先,从Gitea管理面板生成Runner注册令牌:
docker exec -it gitea gitea actions generate-runner-token
NxxxxxxxxxxxxxxxxxxxxxxxN
复制此令牌。将其添加到.env文件:
echo "GITEA_RUNNER_TOKEN=YOUR_TOKEN_HERE" >> /opt/gitea/.env
chmod 600 /opt/gitea/.env
在docker-compose.yml中添加Runner服务:
runner:
image: docker.io/gitea/act_runner:0.3.0
container_name: gitea-runner
environment:
- GITEA_INSTANCE_URL=http://gitea:3000
- GITEA_RUNNER_REGISTRATION_TOKEN=${GITEA_RUNNER_TOKEN}
- GITEA_RUNNER_NAME=vps-runner
volumes:
- runner-data:/data
- /var/run/docker.sock:/var/run/docker.sock
depends_on:
- gitea
restart: always
networks:
- gitea
在文件底部的volumes:部分添加runner-data:。然后启动Runner:
docker compose up -d runner
docker compose logs runner --tail 20
level=info msg="Starting runner daemon"
level=info msg="Runner registered successfully"
Runner挂载Docker套接字(/var/run/docker.sock),以便为每个Job创建容器。这是CI Runner的标准做法,但意味着Job可以访问宿主机的Docker守护进程。对于隔离环境,可以考虑使用Docker-in-Docker(DinD)运行Runner。
| 环境变量 | 用途 |
|---|---|
GITEA_INSTANCE_URL |
Runner访问Gitea的内部URL(使用服务名称,不是公共域名) |
GITEA_RUNNER_REGISTRATION_TOKEN |
来自gitea actions generate-runner-token的一次性令牌 |
GITEA_RUNNER_NAME |
在Gitea管理面板中显示的名称 |
Gitea Actions workflow是什么样的?
Gitea Actions使用与GitHub Actions相同的YAML语法,有一些差异。在仓库中创建.gitea/workflows/build.yml:
name: Build and Test
on:
push:
branches: [main]
pull_request:
branches: [main]
jobs:
build:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Set up Go
uses: actions/setup-go@v5
with:
go-version: '1.23'
- name: Build
run: go build -v ./...
- name: Test
run: go test -v ./...
deploy:
needs: build
runs-on: ubuntu-latest
if: github.ref == 'refs/heads/main'
steps:
- uses: actions/checkout@v4
- name: Deploy via SSH
env:
DEPLOY_KEY: ${{ secrets.DEPLOY_KEY }}
run: |
mkdir -p ~/.ssh
echo "$DEPLOY_KEY" > ~/.ssh/id_ed25519
chmod 600 ~/.ssh/id_ed25519
ssh -o StrictHostKeyChecking=accept-new user@production "cd /app && git pull && systemctl restart myapp"
在仓库设置的Settings > Actions > Secrets中存储DEPLOY_KEY密钥。
| 特性 | Gitea Actions | GitHub Actions |
|---|---|---|
| Workflow目录 | .gitea/workflows/ |
.github/workflows/ |
uses: actions/* |
可用(默认从GitHub获取) | 原生 |
| 容器服务 | 支持 | 支持 |
| 矩阵构建 | 支持 | 支持 |
| 可复用Workflow | Gitea 1.24起支持 | 支持 |
| Marketplace Actions | 大多数可用,部分需要适配 | 原生 |
如何在Gitea中启用Git LFS?
Git Large File Storage允许你追踪二进制文件(图片、模型、数据集)而不会使仓库膨胀。Gitea内置LFS支持。我们已在compose文件中通过LFS_START_SERVER=true启用。LFS数据默认存储在gitea-data卷中。
要显式配置存储路径或提高限制,编辑Gitea配置:
docker exec -i gitea sh -c 'cat >> /data/gitea/conf/app.ini' << 'EOF'
[lfs]
PATH = /data/git/lfs
EOF
重启Gitea以应用更改:
docker compose restart gitea
在客户端安装git-lfs并追踪文件类型:
git lfs install
git lfs track "*.bin" "*.h5" "*.onnx"
git add .gitattributes
git commit -m "Track model files with LFS"
git push
LFS文件存储在容器内配置的PATH路径下。对于大规模部署,可以在app.ini的[lfs]部分配置兼容S3的存储。
如何将GitHub仓库镜像到Gitea?
Pull镜像会在你的Gitea实例上创建GitHub仓库的只读副本。它按照可配置的计划自动同步。适用于备份、作为CI Runner后的本地缓存,或减少对GitHub的依赖。
在Gitea Web界面中:
- 点击**+** > New Migration
- 选择GitHub作为来源
- 输入仓库URL(例如
https://github.com/owner/repo.git) - 对于私有仓库,输入你的GitHub用户名和personal access token作为密码
- 勾选This repository will be a mirror
- 设置镜像间隔(默认8小时)
- 点击Migrate Repository
镜像同步分支、标签和发布。Issue、pull request和wiki也可以在初始导入时迁移,但不会持续同步。
通过API进行镜像:
curl -X POST "https://git.example.com/api/v1/repos/migrate" \
-H "Authorization: token YOUR_GITEA_API_TOKEN" \
-H "Content-Type: application/json" \
-d '{
"clone_addr": "https://github.com/owner/repo.git",
"mirror": true,
"mirror_interval": "8h",
"repo_name": "repo-mirror",
"repo_owner": "admin",
"service": "github"
}'
如何配置Webhook进行部署?
Webhook在仓库发生事件时向指定URL发送HTTP POST请求。它们是触发部署、通知或外部CI系统的简单方式。
在你的仓库中,进入Settings > Webhooks > Add Webhook > Gitea:
- Target URL:
https://deploy.example.com/hooks/gitea - **HTTP Method:**POST
- **Content Type:**application/json
- **Secret:**使用
openssl rand -hex 32生成 - **Trigger events:**Push events(或自定义)
在接收端,你的部署脚本需要验证webhook签名:
# The webhook sends a X-Gitea-Signature header
# Verify it with the shared secret before acting on the payload
EXPECTED=$(echo -n "$PAYLOAD" | openssl dgst -sha256 -hmac "$WEBHOOK_SECRET" | awk '{print $2}')
if [ "$SIGNATURE" != "$EXPECTED" ]; then
echo "Invalid signature"
exit 1
fi
使用轻量级工具webhook的最小webhook接收器:
[
{
"id": "deploy",
"execute-command": "/opt/deploy.sh",
"command-working-directory": "/opt/app",
"trigger-rule": {
"match": {
"type": "payload-hmac-sha256",
"secret": "your-webhook-secret",
"parameter": {
"source": "header",
"name": "X-Gitea-Signature"
}
}
}
}
]
如何备份Gitea实例?
Gitea提供gitea dump命令,将仓库、数据库、配置和LFS对象打包成单个zip文件。对于PostgreSQL,你还需要单独的pg_dump以实现时间点恢复。
手动备份
docker exec -it gitea /usr/local/bin/gitea dump -c /data/gitea/conf/app.ini --file /data/gitea-backup.zip
docker cp gitea:/data/gitea-backup.zip /opt/gitea/backups/
dump包含:
| 内容 | 是否包含在dump中 |
|---|---|
| Git仓库 | 是 |
| 数据库(SQLite或dump) | 是 |
| 配置(app.ini) | 是 |
| LFS对象 | 是 |
| 附件、头像 | 是 |
| 软件包 | 否(单独备份) |
对于PostgreSQL,还需执行:
docker exec gitea-db pg_dump -U gitea gitea | gzip > /opt/gitea/backups/gitea-db-$(date +%Y%m%d).sql.gz
使用cron自动备份
sudo mkdir -p /opt/gitea/backups
sudo chown root:root /opt/gitea/backups
sudo chmod 700 /opt/gitea/backups
创建备份脚本:
sudo tee /opt/gitea/backup.sh > /dev/null << 'BACKUP'
#!/bin/bash
set -euo pipefail
BACKUP_DIR="/opt/gitea/backups"
DATE=$(date +%Y%m%d-%H%M)
# Gitea dump
docker exec gitea /usr/local/bin/gitea dump -c /data/gitea/conf/app.ini --file /data/gitea-backup.zip --quiet
docker cp gitea:/data/gitea-backup.zip "$BACKUP_DIR/gitea-dump-$DATE.zip"
docker exec gitea rm /data/gitea-backup.zip
# PostgreSQL dump
docker exec gitea-db pg_dump -U gitea gitea | gzip > "$BACKUP_DIR/gitea-db-$DATE.sql.gz"
# Keep last 7 daily backups
find "$BACKUP_DIR" -name "gitea-*" -mtime +7 -delete
echo "Backup completed: $DATE"
BACKUP
sudo chmod 700 /opt/gitea/backup.sh
ls -la /opt/gitea/backup.sh
-rwx------ 1 root root 523 Mar 20 10:00 /opt/gitea/backup.sh
设定每天凌晨3点执行:
echo "0 3 * * * root /opt/gitea/backup.sh >> /var/log/gitea-backup.log 2>&1" | sudo tee /etc/cron.d/gitea-backup
sudo chmod 644 /etc/cron.d/gitea-backup
先手动测试一次脚本:
sudo /opt/gitea/backup.sh
Backup completed: 20260320-1030
ls -lh /opt/gitea/backups/
-rw-r--r-- 1 root root 2.3M Mar 20 10:30 gitea-dump-20260320-1030.zip
-rw-r--r-- 1 root root 48K Mar 20 10:30 gitea-db-20260320-1030.sql.gz
将备份复制到服务器外。与磁盘一起消失的本地备份不算备份。
如何安全地更新Gitea?
拉取新镜像,重建容器,让Gitea自动处理数据库迁移。固定镜像版本,确保更新是有意为之而非意外发生。
cd /opt/gitea
# Back up first
sudo /opt/gitea/backup.sh
# Update the image tag in docker-compose.yml, then:
docker compose pull gitea
docker compose up -d gitea
docker compose logs gitea --tail 30
注意观察迁移消息:
2026/03/20 10:35:00 ...les/migration.go:67:Migrate() [I] Migration completed
确认Gitea正常启动后,如果有新版本可用,更新Runner:
docker compose pull runner
docker compose up -d runner
Gitea和Forgejo有什么区别?
Forgejo在2022年底从Gitea分叉,起因是一家营利性公司(Gitea Ltd.)接管了Gitea项目。Forgejo由Codeberg e.V.管理,这是一个德国非营利组织。截至2026年初,Forgejo是一个硬分叉,代码库已经分道扬镳。两者都可以使用类似的Docker配置运行,但在它们之间迁移已不再无缝。
| Gitea | Forgejo | |
|---|---|---|
| 治理 | Gitea Ltd.(营利性) | Codeberg e.V.(非营利性) |
| 许可证 | MIT | GPL-3.0+(Forgejo v9.0起) |
| Docker镜像 | docker.gitea.com/gitea |
codeberg.org/forgejo/forgejo |
| Actions支持 | 是(act_runner) | 是(兼容Runner) |
| API兼容性 | 兼容GitHub | 兼容GitHub |
| 独有功能 | Gitea Enterprise、MCP服务器 | 联邦(ForgeFed)、审核工具 |
如果你想要一个社区治理的项目并使用copyleft许可证,选择Forgejo。如果你想要有商业支持的原始项目,选择Gitea。本指南中的Docker Compose配置稍作修改即可用于两者(更换镜像、调整路径)。
如果你从Gitea开始,之后想切换:使用gitea dump导出数据,搭建Forgejo,然后导入。充分测试。两个代码库已经足够分化,部分数据库架构有所不同。
Gitea需要多少RAM和CPU?
带PostgreSQL的Gitea实例空闲时使用约150-250 MB内存。活跃使用时有5-10个用户和CI Runner,预计总共300-500 MB。这比GitLab实例轻约10倍,后者至少需要4 GB。
这些数据来自在Virtua Cloud VPS(4 vCPU、8 GB RAM)上运行的Gitea实例:
docker stats --no-stream --format "table {{.Name}}\t{{.CPUPerc}}\t{{.MemUsage}}"
NAME CPU % MEM USAGE / LIMIT
gitea 0.15% 148.2MiB / 7.77GiB
gitea-db 0.08% 45.3MiB / 7.77GiB
gitea-runner 0.02% 32.1MiB / 7.77GiB
| 场景 | RAM(整个服务栈) | CPU |
|---|---|---|
| 空闲,少量仓库 | ~230 MB | < 1% |
| 活跃,5-10个用户 | ~400 MB | 2-5% |
| CI构建运行中 | ~600 MB(构建期间峰值) | 每个Job 20-50% |
| GitLab等效 | 4,000+ MB | 空闲时10%+ |
Gitea在2 GB的VPS上运行流畅。配合CI Runner,4 GB为并发构建提供充足余量。
故障排查
SSH连接被拒绝:
检查git用户是否存在、sshd_config中是否有Match块、sshd是否已重启。查看日志:
journalctl -u sshd -f
Runner不接收Job: 确认Runner已在管理面板注册(Site Administration > Actions > Runners)。检查Runner日志:
docker compose logs runner --tail 50
启动时数据库连接错误: 健康检查应该能防止这种情况,但如果Gitea在PostgreSQL就绪前启动:
docker compose restart gitea
LFS推送返回413错误:
增大Nginx配置中的client_max_body_size。512M通常足够,但根据你最大的文件进行调整。
Gitea日志:
docker compose logs gitea --tail 100
或实时查看:
docker compose logs gitea -f
版权所有 2026 Virtua.Cloud。保留所有权利。 本内容为 Virtua.Cloud 团队原创作品。 未经书面许可,禁止复制、转载或再分发。