深入剖析我的 Forgejo 配置
摘要
详细指南:如何自托管 Forgejo 实例以替代 GitHub,涵盖技术栈、CI/CD 性能提升及配置。
<p>大家好!我最近重新燃起了写技术博客的热情,我想可能也有人正在考虑搭建 Forgejo 实例,所以我花时间记录了我的实践过程。</p>
<p>期待大家的反馈和建议,也乐于回答任何问题。</p>
<p>祝你 Forge 愉快!</p>
<p><a href="https://lobste.rs/s/jhcyuq/deep_dive_into_my_forgejo_setup">评论</a></p>
查看缓存全文
缓存时间: 2026/07/20 17:27
# 深入解析我的 Forgejo 搭建方案
来源:https://a.l3x.in/blog/welcome-to-my-forge/
与许多(https://ziglang.org/news/migrating-from-github-to-codeberg/)其他(https://mitchellh.com/writing/ghostty-leaving-github)近期的(https://www.reddit.com/r/TenacityAudio/comments/10xj8hh/latest_tenacity_developments/)项目一样,我对 GitHub(https://isgithubcooked.com/)服务质量持续且不可逆转的下降(更多内容后续)感到厌倦,于是决定走上**自托管**之路,管理我所有的**Git、Docker、PR 和 CI/CD**。在本文中,我将详细介绍我的新自托管 **Forgejo**[^1](https://a.l3x.in/blog/welcome-to-my-forge/#user-content-fn-forgejo) 设置目前的样子,以及它如何作为 GitHub 的近乎完美替代品(剧透:**非常**适合我)。我会提供实际仓库的链接以及可复用的配置片段,方便你自己尝试。
## 为什么选择 Forgejo
https://a.l3x.in/blog/welcome-to-my-forge/#why-forgejo
说实话,我并没有花太多时间挑选采用哪个开源 Git 平台。我听说过诸如 SourceHut(https://sourcehut.org/) 等其他选项,但:
- 我已经有一个 Codeberg(Forgejo 开发背后的公司)账户,并且喜欢他们的使命(https://docs.codeberg.org/getting-started/what-is-codeberg/#our-mission)
- 它几乎完全兼容 GitHub API,因此 GitHub Actions 应该可以开箱即用(https://forgejo.org/docs/latest/user/actions/github-actions/)
- 功能丰富,被认为稳定,并遵循定期发布周期
- 界面几乎与 GitHub 相同
- 它用 Go 编写,我喜欢 Go
这些理由可能不够全面,但对我来说已经足够。
## 它的样子
https://a.l3x.in/blog/welcome-to-my-forge/#how-it-looks-like
该平台现已上线 https://forge.l3x.in/,并且已经成为我一些开源项目和所有私有仓库的家园,计划将我在 GitHub 上的大部分其他项目也迁移过来(稍后详述)。目前为止一切运行良好:Web UI 熟悉且响应迅速,迁移/镜像仓库只需点击一下,CI/CD 延迟和构建时间非常出色 ⚡️ 以下是一个涉及 Docker(Podman)构建的单个项目的快速对比,以证明这一点:
- 在 GitHub(https://github.com/shaftoe/opengist/actions/runs/28580874938)上:20 分 54 秒
- 在我的 Forgejo Runner(https://forge.l3x.in/alex/opengist/actions/runs/8/jobs/0/attempt/1):3 分 19 秒
构建(并推送到注册表)同一个应用程序,节省了近 18 分钟,更不用说从今以后我不必在 GH 构建队列中等待了。仅此一点就值得付出努力。稍后我会详细说明哪些方面比 GitHub 更好,哪些方面不如,但让我们先了解一些技术细节。
## 技术栈
https://a.l3x.in/blog/welcome-to-my-forge/#the-stack
构成我的平台的组件列表:
- 一个小的 Debian Trixie(https://www.debian.org/releases/trixie/) `amd64` VPS 来托管所有内容
- Systemd(https://systemd.io/) 和 Podman(Quadlet)(https://docs.podman.io/en/latest/markdown/podman-quadlet.1.html) 来管理服务和定时任务
- Caddy(https://caddyserver.com/) 来终止 TLS 请求并自动续期证书
- Forgejo 和 Forgejo runner(https://code.forgejo.org/forgejo/runner) 用于 CI/CD
- Webhook(https://github.com/adnanh/webhook) 以编程方式重启服务
- Ansible(https://docs.ansible.com/projects/ansible/latest/index.html) 和 OpenTofu(https://opentofu.org/) 以 **GitOps** 方式配置**所有内容**
- Renovate(https://forge.l3x.in/alex/renovate-bot) 保持所有安装的软件更新
- Restic(https://restic.readthedocs.io/en/latest/) 每日备份 Podman 卷[^2](https://a.l3x.in/blog/welcome-to-my-forge/#user-content-fn-restic)
- Axiom(https://registry.terraform.io/providers/axiomhq/axiom/latest/docs)(通过 Vector(https://github.com/vectordotdev)) 收集 Journald 日志并报警错误
### 虚拟专用服务器
https://a.l3x.in/blog/welcome-to-my-forge/#virtual-private-server
我每年向 OVH[^3](https://a.l3x.in/blog/welcome-to-my-forge/#user-content-fn-ovh) 支付约 60 欧元的年费,以获得一台专用的 Linux `amd64` VPS,配备 4 个 vCPU 核心、8GB 内存、75GB 存储,外加一个固定 IPv4 和一个 IPv6 地址。这听起来可能不算什么,但你会惊讶于这样低配置的机器如今仍然能发挥多大作用 🥊。操作系统方面也没什么特别的:我是 Debian 的老用户,已经有一堆 Ansible 角色来配置该 VPS 上的常见服务,如 SSHd、Postfix 等;每当有新需求时,只需再添加一个角色即可。现在有了个人 Forgejo,改变的是我运行配置管理/IaC 工具(如 Ansible 和 OpenTofu)的**方式**。稍后详述。
### 服务管理器
https://a.l3x.in/blog/welcome-to-my-forge/#service-managers
在这个项目中,我决定自学一些 Podman,因为到目前为止我所有的容器化历史都使用 Docker。**TL;DR**:我的 MacBook 上已经卸载了 Docker Desktop,现在**我仅使用 Podman** 来处理 macOS 工作站和 Linux 服务器上的**所有容器相关事务**。与 Docker 不同,Podman 不需要专用的守护进程代理命令,并且 Quadlet 与 Systemd 配合得很好,用于运行容器化服务。我现在以**无根模式**运行所有 Podman 容器(https://github.com/podman-container-tools/podman/blob/main/docs/tutorials/rootless_tutorial.md),一旦创建了服务单元文件,我就可以使用你可能已经熟悉的 `systemctl`/`journalctl` 命令来启动/停止/检查服务。启用服务后(`systemctl enable`),Systemd 会负责在主机重启、应用程序崩溃、意外 `podman kill` 等情况下保持服务运行。
#### 示例:管理 Forgejo 进程
https://a.l3x.in/blog/welcome-to-my-forge/#example-manage-forgejo-process
以下是 **Forgejo** 的单元文件:
```
# ~/.config/containers/systemd/forgejo.container
[Container]
Image=codeberg.org/forgejo/forgejo:16
ContainerName=forgejo
Environment=USER_UID=1000
Environment=USER_GID=1000
Environment=FORGEJO____APP_NAME=Forgejo
[...]
Pull=newer
CgroupsMode=disabled
PublishPort=127.0.0.1:3000:3000
PublishPort=[::1]:3000:3000
PublishPort=2222:2222
Volume=forgejo-data:/data
[Service]
Restart=always
[Install]
WantedBy=default.target
```
`Pull=newer` 是为了确保我始终运行最新的 Forgejo 稳定版本(每日定时备份脚本会重启服务,这时会触发镜像拉取)。以下是我检查服务状态的方式:
```
$ systemctl --user status forgejo.service
● forgejo.service
Loaded: loaded (/home/podman/.config/containers/systemd/forgejo.container; generated)
Active: active (running) since Sat 2026-07-18 00:00:43 UTC; 5h 24min ago
Invocation: 2193250002a74867a80486adc9194134
Main PID: 987445 (conmon)
Tasks: 50 (limit: 9257)
Memory: 530.5M (peak: 1G)
CPU: 46min 14.351s
CGroup: /user.slice/user-997.slice/[email protected]/app.slice/forgejo.service
```
查看进程日志:
```
$ journalctl --user -u forgejo.service -f
Jul 18 05:44:23 ovh.l3x.in forgejo[987445]:
2026/07/18 05:44:23 ...eb/routing/logger.go:102:func1() [I] router: completed GET [...]
```
你可能注意到了命令中的 `--user` 标志:这是与非 **root** 用户执行的 Systemd 服务交互所必需的。
#### Podman 自身的依赖
https://a.l3x.in/blog/welcome-to-my-forge/#podman-own-requirements
实际上,我列出使用的工具时并不准确……如果不提及 Podman 正常运行所需的所有包,包括网络和其他部分,这个技术栈是不完整的[^4](https://a.l3x.in/blog/welcome-to-my-forge/#user-content-fn-packages)。以下是我 `podman` Ansible 角色中实际的包列表:
```
podman_packages:
- podman
- uidmap # UID 映射
- passt # 用户态网络
- fuse # 文件系统
- aardvark # DNS / 发现
- buildah # 镜像构建
- crun # 运行时
```
为了保持本文范围可控,我不会详细说明这些包,但它们是随现代 Linux 发行版一起提供的开源包,因此查找文档很容易。不过直接参考 Podman 教程(https://docs.podman.io/en/latest/Tutorials.html)可能就足以让你按需运行一切。
#### 定时任务
https://a.l3x.in/blog/welcome-to-my-forge/#recurring-tasks
我尽可能多地使用 Systemd 功能,包括自定义定时器(https://wiki.archlinux.org/title/Systemd/Timers),这是对 `cronjob` 的现代化改进。例如,我定期运行 Restic 备份(见稍后章节)和 Podman 清理任务,通常是每日频率,但根据任务不同而有所变化。在使用 Podman 运行 CI/CD 作业时,一个重要的任务是清理各种副产品,以避免磁盘被无用废物膨胀;这个**定时器**起到了作用:
```
# ~/.config/systemd/user/podman-prune.timer
[Unit]
Description=Periodic Podman system prune
Documentation=man:podman-system-prune(1)
[Timer]
OnCalendar=daily
Persistent=true
[Install]
WantedBy=timers.target
```
配合相应的**服务**:
```
# ~/.config/systemd/user/podman-prune.service
[Unit]
Description=Podman system prune (stopped containers, dangling images, unused networks, build cache)
Documentation=man:podman-system-prune(1)
[Service]
Type=oneshot
ExecStart=/usr/bin/podman system prune -f
```
提示:`systemctl --user list-timers` 显示当前用户定义的所有定时器:
```
$ systemctl --user list-timers "podman*"
NEXT LEFT LAST PASSED UNIT ACTIVATES
Tue 2026-07-21 00:00:00 UTC 18h Mon 2026-07-20 00:00:17 UTC 5h 14min ago podman-prune.timer podman-prune.service
Tue 2026-07-21 00:09:17 UTC 18h Mon 2026-07-20 00:05:36 UTC 5h 8min ago podman-auto-update.timer podman-auto-update.service
2 timers listed.
```
### 反向 HTTP 代理
https://a.l3x.in/blog/welcome-to-my-forge/#reverse-http-proxy
关于 Caddy 没什么可多说的:
1. 设置 APT 源(https://caddyserver.com/docs/install#debian-ubuntu-raspbian)
2. `apt-get install caddy`
3. 为每个新服务添加一个 `CNAME` DNS 记录到我的域名,例如 `forge CNAME `
4. 为每个要暴露为 HTTPS 的反向代理服务添加一个 Caddyfile(https://caddyserver.com/docs/caddyfile),例如:
```
# /etc/caddy/conf.d/forge.caddy:
forge.l3x.in {
reverse_proxy localhost:3000
}
```
5. 完成!
无聊!从 Nginx 和 Certbot 迁移过来非常顺畅。帮自己一个忙,也考虑一下它,我保证你不会后悔。
### Webhook
https://a.l3x.in/blog/welcome-to-my-forge/#webhook
能够从 CI/CD 环境(在我的情况下是从 Forgejo runner 作业)中与托管操作系统交互非常有用。为此,我使用 https://github.com/adnanh/webhook,当前配置如下:
```json
[
{
"id": "restart-gist",
"execute-command": "/bin/sh",
"pass-arguments-to-command": [
{
"source": "string",
"name": "-c"
},
{
"source": "string",
"name": "sudo /usr/local/bin/podctl restart gist"
}
],
"include-command-output-in-response": false,
"trigger-rule": {
"match": {
"type": "value",
"parameter": {
"source": "header",
"name": "Authorization"
},
"value": "Bearer <SECRET>"
}
}
},
...
]
```
上述配置暴露了一个**UNIX**套接字,Podman 容器可以访问,并接受来自 CI/CD 作业的请求。在这个特定情况下,它重启另一个 Podman 容器(`gist`)。你可以在这里(https://forge.l3x.in/alex/opengist/src/commit/0792dc22800602d5e8cd60572654d42c00c82fbd/.forgejo/workflows/build-push-deploy.yml#L23)看到实际的工作流程,该流程使用了这个可复用工作流程(https://forge.l3x.in/alex/.profile/src/branch/master/.forgejo/workflows/webhook-restart.yml),最终归结为一个简单的 `curl` 请求:
```yaml
- name: Restart gist service
run: |
curl -s -o /dev/null --unix-socket /run/webhook/webhook.sock \
-H "Authorization: Bearer ${{ secrets.WEBHOOK_TOKEN }}" \
http://localhost/hooks/restart-gist
```
实际的 webhook **进程**并非以 `root` 身份运行,因此 webhook **命令**中使用 `sudo` 允许 webhook **进程**重启服务。`/etc/sudoers.d/webhook` 中的 sudo 配置如下:
```
webhook ALL=(root) NOPASSWD: /usr/local/bin/podctl restart
```
其中 `podctl` 是一个简单的 shell 包装器,用于为 **podman** 用户运行命令(以避免在 systemd 命令中添加 `--user` 等)。
### 编排工具
https://a.l3x.in/blog/welcome-to-my-forge/#orchestrators
#### Ansible
https://a.l3x.in/blog/welcome-to-my-forge/#ansible
如前所述,我使用 Ansible 来配置在我的个人 Linux 主机上运行的所有服务。我并不喜欢这个工具,但我还没有找到合适的替代品[^5](https://a.l3x.in/blog/welcome-to-my-forge/#user-content-fn-ansible-go);然而一旦部署好,添加一个新角色来安装这个或那个,并将其分配给 *host1* 但不给 *host2* 等等,就变得轻而易举。Forgejo 的 `main.yaml` Ansible 任务列表如下(为简洁起见,省略了注释和额外的验证步骤):
```yaml
- name: Deploy Caddy site fragment for Forgejo
ansible.builtin.include_role:
name: caddy
tasks_from: site.yml
vars:
caddy_site_name: forge
caddy_site_domain: "{{ forgejo_domain }}"
caddy_site_proxy: localhost:3000
# -- Forgejo container --------------------------------------------------------
- name: Deploy Forgejo container
ansible.builtin.include_role:
name: podman
vars:
podman_quadlets: "{{ [forgejo_container_quadlet] }}"
- name: Admin user creation (skipped in check mode)
when: not ansible_check_mode
block:
- name: List Forgejo users (idempotency check for admin creation)
ansible.builtin.command:
cmd: "{{ podman_helper_path }} podman exec --user 1000 forgejo gitea admin user list"
register: _forgejo_users
changed_when: false
- name: Create the admin user (idempotent -- only if not yet present)
ansible.builtin.command:
argv:
- "{{ podman_helper_path }}"
- podman
- exec
- --user
- "1000"
- forgejo
- gitea
- admin
- user
- create
- --admin
- --username
- "{{ forgejo_admin_user }}"
- --password
- "{{ forgejo_admin_password }}"
- --email
- "{{ forgejo_admin_email }}"
- --must-change-password=false
when: forgejo_admin_user not in _forgejo_users.stdout
changed_when: true
no_log: true
# -- Restic backup (only on hosts configured for backups) -------------------
- name: Restic backup for Forgejo (only on hosts configured for backups)
when: (restic_repository | default('')) | length > 0
block:
- name: Ensure restic backup framework is installed (idempotent)
ansible.builtin.include_role:
name: restic
- name: Register Forgejo restic backup job
ansible.builtin.include_role:
name: restic
tasks_from: job.yml
vars:
restic_job_name: forgejo
restic_job_command: >-
/usr/local/bin/restic-backup forgejo --caddy
--pre "podctl stop forgejo"
--post "podctl start forgejo"
-- {{ podman_user_home | default('/home/podman') }}/.local/share/containers/storage/volumes/forgejo-data/_data
```
我的 playbook 应该是幂等的,因此对 `ansible` 仓库的更改合并到 `master` 分支后,会触发一个作业,将所需状态强制应用到我的所有主机上。更多内容见下面的 GitOps 章节。
#### OpenTofu
https://a.l3x.in/blog/welcome-to-my-forge/#opentofu
对于这个特定项目,OpenTofu 并没有承担太多重量,但它也发挥了作用,所以我也会简要提及。我将各种项目的 Tofu 状态文件(https://forge.l3x.in/alex/terraform-state-bucket) 放在一个 AWS 存储桶中,并且专门用于编辑该存储桶的 AWS IAM 角色是动态假设(https://forge.l3x.in/alex/terraform-state-bucket/src/commit/ff2849344c3446430a9e17100d9e8fc77c48add1/.forgejo/workflows/deploy.yaml#L51)的,这得益于 Forgejo 的 OAuth2 提供者(https://forgejo.org/docs/latest/user/oauth2-provider/)。一个私有
相似文章
为何我要从 GitHub 迁移至 Forgejo
本文探讨了从 GitHub 迁移到自托管的 Forgejo 的决定,主要提及了对数据所有权、可靠性以及 AI 数据收集实践的担忧。文章还介绍了荷兰政府类似的举措,并详细说明了个人 Forgejo 实例的技术部署。
Forge
Forge 是一个全新的 CLI 与 Go 库,通过统一接口和自动 forge 检测,一次性打通 GitHub、GitLab、Bitbucket 与 Gitea/Forgejo 的交互。
Forgejo 月度报告 - 2026年5月
Forgejo 2026年5月月度报告详细介绍了 v15 LTS 版本、v11 和 v14 的安全更新以及 Forgejo Runner 的更新。v14 支持已结束,v11 支持也即将结束,建议用户升级。
在代码锻造平台上,你需要哪些GitHub功能才能进行迁移?
Lobsters上的一场讨论探讨了哪些GitHub功能是迁移到不同代码锻造平台的关键障碍,作者正在构建一个名为juju.bi的锻造平台,专注于离线协作、GitHub API兼容性以及基于change-id的功能。
我们应得的代码锻造平台
本文讨论了对GitHub可靠性日益增长的不满,并提出基于AT协议的去中心化Git锻造平台Tangled,作为一个结合了中心化便利性与用户数据所有权的有前途的替代方案。