使用Docker Compose在VPS上自托管Gitea

3 分钟阅读·Matthieu·ci-cdself-hostingdocker-composegiteapostgresqldockergit|

部署生产就绪的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-datapostgres-data)而非bind mount,管理更清晰
  • PostgreSQL上的健康检查确保Gitea等待数据库就绪
  • **DISABLE_REGISTRATION=true**因为公开实例的开放注册会招致滥用
  • **ACTIONS__ENABLED=true**从一开始就启用Gitea Actions
  • 端口3000绑定到localhost127.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界面中:

  1. 点击**+** > New Migration
  2. 选择GitHub作为来源
  3. 输入仓库URL(例如https://github.com/owner/repo.git
  4. 对于私有仓库,输入你的GitHub用户名和personal access token作为密码
  5. 勾选This repository will be a mirror
  6. 设置镜像间隔(默认8小时)
  7. 点击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 团队原创作品。 未经书面许可,禁止复制、转载或再分发。

准备好亲自尝试了吗?

几秒内部署您自己的服务器。支持 Linux、Windows 或 FreeBSD。

查看 VPS 方案