为什么是 N100
N100 是 Intel 2023 年发布的低功耗处理器,4 核 4 线程,TDP 仅 6W,但单核性能比树莓派强一大截。配上 16GB 内存和 NVMe SSD,价格在国内大约 600-800 元出头,是目前 Homelab 性价比最高的方案之一。
GitLab 的官方最低要求是 4 核 + 4GB,推荐生产配置是 8 核 + 16GB。N100 的 4 核 16GB 正好卡在”能跑、但要调”的位置——开箱默认配置跑不好,调完之后两人用完全没问题。
这篇文章的所有配置都针对这个硬件规格。
机器配置:
- CPU:Intel N100(4 核 4 线程)
- 内存:16 GB
- 存储:约 900 GB NVMe SSD
- 操作系统:Debian 12(Bookworm)
- 使用人数:2 人
网络架构
在家部署面临两个现实问题:
- 国内宽带的 80/443 端口被运营商封锁,无法直接在标准 HTTPS 端口上对外提供服务
- 家庭公网 IP 通常是动态的,需要一个稳定的域名指向
解法是这条链路:
flowchart LR
CF["Cloudflare\ngitlab.example.com\nDNS Only 灰云"] --> OW["OpenWrt\n端口转发\n自定义端口 → 内网"]
OW --> TR["Traefik\n反向代理\nTLS 终结"]
TR -->|"127.0.0.1:8181"| GW["GitLab Workhorse"]
GW --> GL["GitLab Rails\nPuma / Sidekiq\nPostgreSQL / Redis / Gitaly"]
每一跳的职责:
- Cloudflare:DNS 解析 + DDNS 更新(公网 IP 变了自动同步),不开代理(原因后面说)
- OpenWrt:防火墙端口转发,把自定义端口的入站流量打到内网 Traefik
- Traefik:接管 HTTPS,自动申请和续期 Let’s Encrypt 证书,按域名转发到 GitLab
- GitLab Workhorse:GitLab 内置的智能代理层,处理 Git 大对象传输和文件上传
GitLab EE 还是 CE
GitLab 有两个版本:社区版(CE)和企业版(EE)。
EE 听起来要收费,但实际上 EE 免费功能的范围已经够 Homelab 使用了。EE 的收费功能需要购买 License 才能解锁,不买 License 的话 EE 和 CE 的功能几乎一样。
选 EE 的原因很实际:如果以后想试某个 EE 专属功能,不需要重新部署,直接加 License 就行。CE 想换 EE 需要走升级流程,麻烦。
为什么用 Omnibus 而不是 Docker
GitLab 提供两种主流安装方式:Omnibus 安装包和 Docker 镜像。
Omnibus 是 GitLab 官方打包的 .deb 安装包,把所有组件(Rails、Puma、Sidekiq、PostgreSQL、Redis、Gitaly、NGINX……)打成一个包统一安装,所有配置集中在 /etc/gitlab/gitlab.rb 一个文件里。
Docker 方式则把这些组件分别放在容器里,或者用一个大的 GitLab 官方镜像跑全部组件,配置通过环境变量和卷挂载传入。
两种方式都能跑,选 Omnibus 的理由是:
- 配置更直观:一个 Ruby 配置文件,改完
gitlab-ctl reconfigure生效,不需要理解容器编排 - 排查更简单:组件全在宿主机上,
gitlab-ctl tail puma直接看日志,不需要进容器 - 升级路径清晰:官方文档的升级指南全部基于 Omnibus,Docker 方式有时候需要自己对照着翻译
安装前的准备
域名规划
一个主域名就够用了,其他按需开启:
| 功能 | 推荐域名 | 是否必需 |
|---|---|---|
| GitLab 主站 | gitlab.example.com | ✅ 必需 |
| Container Registry | registry.example.com | 可选 |
| GitLab Pages | pages.example.com | 可选 |
Container Registry 虽然可以挂在主域名子路径下,但 Docker 客户端对非标端口的支持历来一言难尽,强烈建议单独子域名 + 标准 443 端口,省掉很多奇怪的问题。
部署前检查
# 设置时区(GitLab 的日志和备份命名都依赖系统时间)
sudo timedatectl set-timezone Asia/Shanghai
# 检查磁盘(GitLab 本身就需要几十 GB,再加上仓库和制品,建议预留 100GB 以上)
df -h
# 检查 SSH 端口是否占用了 22(GitLab 的 Git SSH 也默认用 22,两个抢同一个端口会冲突)
sudo netstat -tlnp | grep sshd
SSH 端口冲突是第一个容易踩的坑。GitLab 的 Git over SSH 功能和系统 SSH 服务都想监听 22 端口,装之前要么把系统 SSH 改到其他端口,要么提前配好 GitLab 用别的端口。
安装
# 安装基础依赖
sudo apt update && sudo apt upgrade -y
sudo apt install -y curl openssh-server ca-certificates tzdata perl
# 添加 GitLab 官方仓库
curl https://packages.gitlab.com/install/repositories/gitlab/gitlab-ee/script.deb.sh | sudo bash
# 查看可用的 17.11.x 版本
apt-cache madison gitlab-ee | grep 17.11
# 安装(不在这里设置 EXTERNAL_URL,后续在 gitlab.rb 里配)
sudo apt install gitlab-ee=17.11.*
为什么要锁版本:
sudo apt-mark hold gitlab-ee
这一步不是可选的。GitLab 的大版本升级有严格的顺序要求,比如从 16.x 升到 17.x,必须先升到 16.11,再升到 17.0,然后才能到 17.11,不能跳版本。如果不锁版本,某次 apt upgrade 悄悄给你升了几个大版本,数据库 migration 会失败,回滚非常麻烦。锁住版本,升级时按照 GitLab 官方升级路径文档一步一步来。
配置
配置文件是 /etc/gitlab/gitlab.rb,修改完之后运行 sudo gitlab-ctl reconfigure 应用。
gitlab.rb 是 Ruby DSL,不是 YAML,语法错误不会在保存时报,只在 reconfigure 时才炸。改完先检查语法:
sudo gitlab-ctl check-config
最常见的错误是中文引号混入、漏掉逗号、字符串没加引号。
完整配置
# ============================================
# 基础配置
# ============================================
external_url 'https://gitlab.example.com'
# ============================================
# 禁用内置 NGINX,改用 Traefik 做反代
# ============================================
# GitLab Omnibus 内置了一个 NGINX 作为 Web 入口
# 我们已经有 Traefik 了,两个 Web 服务器叠在一起没有意义,关掉
nginx['enable'] = false
# Workhorse 只监听本地回环地址
# 不对外暴露,只让 Traefik 能访问到
gitlab_workhorse['listen_network'] = "tcp"
gitlab_workhorse['listen_addr'] = "127.0.0.1:8181"
# 告诉 GitLab 信任来自这些地址的代理请求头
# 不配这个,所有请求经 Traefik 转发后,GitLab 看到的客户端 IP 全是 127.0.0.1
# 审计日志、登录地理位置、暴力破解检测全部失效
gitlab_rails['trusted_proxies'] = ['127.0.0.1', '::1', '10.0.0.0/8']
# ============================================
# Puma(Web 应用服务器)
# ============================================
# Puma 是跑 GitLab Rails 应用的进程
# worker_processes 是工作进程数,每个进程独立处理请求
# 每个 worker 常驻内存约 1-1.5GB,所以不能开太多
# 4 核机器开 3 个 worker,留一个核给其他组件
puma['worker_processes'] = 3
puma['min_threads'] = 4
puma['max_threads'] = 4
puma['worker_timeout'] = 60
# 内存超过这个阈值就重启 worker(注意:这是触发重启的阈值,不是硬限制)
# 实际高负载下 worker 可能跑到 1.5GB 才触发回收,属于正常现象
puma['per_worker_max_memory_mb'] = 1200
# ============================================
# Sidekiq(后台任务)
# ============================================
# Sidekiq 处理所有异步任务:发邮件、pipeline 调度、镜像同步等
# concurrency 是并发线程数,20 对两人团队足够
sidekiq['max_concurrency'] = 20
# ============================================
# PostgreSQL
# ============================================
# shared_buffers:PostgreSQL 用于缓存数据的内存
# 通常设为系统内存的 25%,16GB 机器设 4GB
postgresql['shared_buffers'] = "4GB"
# work_mem:每个查询操作(排序、哈希)可用的内存
# 注意这是「每个操作」而非「每个连接」,高并发下实际占用可能很高
postgresql['work_mem'] = "32MB"
# maintenance_work_mem:VACUUM、CREATE INDEX 等维护操作的内存上限
postgresql['maintenance_work_mem'] = "512MB"
# effective_cache_size:告诉查询规划器系统有多少内存可用于缓存
# 这个参数不实际分配内存,只影响查询计划的选择
postgresql['effective_cache_size'] = "10GB"
postgresql['max_connections'] = 300
# ============================================
# Gitaly(Git 仓库服务)
# ============================================
# Gitaly 处理所有 Git 操作,仓库多了内存消耗会很明显
# 限制单个 RPC 的最大并发,防止某个操作把 Gitaly 资源耗尽
gitaly['concurrency'] = [
{ 'rpc' => "/gitaly.SmartHTTPService/PostReceivePack", 'max_per_repo' => 3 },
{ 'rpc' => "/gitaly.SSHService/SSHUploadPack", 'max_per_repo' => 3 }
]
# jemalloc 内存分配器调优,减少 Ruby 进程的内存碎片
gitlab_rails['env'] = {
'MALLOC_CONF' => 'dirty_decay_ms:1000,muzzy_decay_ms:1000'
}
# ============================================
# 关闭不需要的组件,节省内存
# ============================================
# 监控栈(Prometheus + Grafana):两人团队暂时不需要,省出几百 MB
prometheus_monitoring['enable'] = false
prometheus['enable'] = false
alertmanager['enable'] = false
node_exporter['enable'] = false
redis_exporter['enable'] = false
postgres_exporter['enable'] = false
gitlab_exporter['enable'] = false
grafana['enable'] = false
# 不用的功能组件
mattermost['enable'] = false
pages_nginx['enable'] = false
gitlab_pages['enable'] = false
registry['enable'] = false
registry_nginx['enable'] = false
# ============================================
# 存储和备份
# ============================================
git_data_dirs({
"default" => {
"path" => "/var/opt/gitlab/git-data"
}
})
gitlab_rails['backup_path'] = "/var/opt/gitlab/backups"
gitlab_rails['backup_keep_time'] = 604800 # 保留 7 天,单位秒
实际内存占用参考
| 组件 | 预计占用 |
|---|---|
| Puma(3 workers) | ≈ 3.6 GB(实测高负载下可到 4.5GB) |
| PostgreSQL | ≈ 5 GB(shared_buffers + 其他) |
| Sidekiq + Redis + Gitaly | ≈ 2-3 GB |
| 系统 + 其他 | ≈ 1 GB |
| 合计 | ≈ 11-13 GB,16GB 机器有 3-5GB buffer |
应用配置并启动
# 应用配置(首次大约需要 5-10 分钟,不要中途 Ctrl+C)
sudo gitlab-ctl reconfigure
# 验证所有服务状态
sudo gitlab-ctl status
# 确认 Workhorse 在监听 8181
sudo netstat -tlnp | grep 8181
# 获取初始 root 密码(这个文件 24 小时后会自动删除)
sudo cat /etc/gitlab/initial_root_password
浏览器访问 https://gitlab.example.com,用 root 和上面拿到的密码登录。
首次加载会很慢,不是配置有问题。GitLab 在跑数据库初始化和资源编译任务,等 2-3 分钟再刷新。
登录后立刻做这三件事:
- 修改 root 密码(初始密码文件 24 小时后消失)
- 创建普通用户账号(日常操作不要用 root,出问题审计日志会乱)
- 配置 SSH 公钥,跑一次
git push验证整条链路
踩过的坑
Cloudflare 橙云 + 非标端口 = 流量直接消失
Cloudflare 的代理模式(橙云)只支持有限的端口列表,80、443、8080 等,其他端口的流量会被 Cloudflare 丢弃,不会有任何有意义的报错,请求就是发出去没有回应。
用非标端口必须设为 DNS Only(灰云),让流量直接打到家庭公网 IP,Cloudflare 只做域名解析,不碰流量。
判断方法:访问 https://gitlab.example.com 超时,但直接访问 公网IP:端口 正常,八九不离十是橙云的问题。
trusted_proxies 不配,IP 全是 127.0.0.1
所有请求经 Traefik 转发后,带着 X-Forwarded-For 头进来,但 GitLab 默认不信任任何代理,这个头会被忽略,记录下来的客户端 IP 全是 127.0.0.1。
后果是:审计日志没有真实 IP,登录异常检测失效,暴力破解防护形同虚设。必须在 gitlab.rb 里把 Traefik 所在的地址段加到 trusted_proxies。
reconfigure 中途被打断
reconfigure 是一个完整的 Chef 收敛过程,中途 Ctrl+C 会让 GitLab 的内部状态停在一半,某些组件初始化了,某些没有,各种奇怪的服务起不来。
遇到这种情况:不要试图局部修复,直接重跑完整的 sudo gitlab-ctl reconfigure。这个命令是幂等的,重跑不会有副作用,会从断点恢复。
Puma worker 内存会超出设定值
per_worker_max_memory_mb = 1200 是触发 worker 重启的阈值,不是硬内存限制。Puma 不会在内存达到这个值时立刻杀掉 worker,而是等当前请求处理完再重启。
高负载下,每个 worker 轻松跑到 1.5GB 才触发回收。16GB 机器开 3 个 worker 是合理上限,不要贪心开 4 个,留足 buffer,否则系统开始 swap,性能会断崖式下跌。
gitlab.rb 语法错误只在 reconfigure 时才暴露
gitlab.rb 是 Ruby DSL,保存文件时不会做语法检查,只有跑 reconfigure 时才会解析。写错了,reconfigure 中途报错退出,GitLab 可能处于半配置状态。
养成习惯,改完配置先跑:
sudo gitlab-ctl check-config
常见错误:全角引号混进来、字符串结尾漏引号、数组最后多了逗号。
常用管理命令
sudo gitlab-ctl status # 查看所有组件运行状态
sudo gitlab-ctl restart # 重启所有服务
sudo gitlab-ctl reconfigure # 应用 gitlab.rb 的配置变更
sudo gitlab-ctl tail puma # 实时查看 Puma 日志
sudo gitlab-ctl tail postgresql # 实时查看 PostgreSQL 日志
sudo gitlab-rake gitlab:check # 环境自检,排查问题用
sudo gitlab-rake gitlab:env:info # 查看 GitLab 版本和环境信息
备份
GitLab 的备份不包括配置文件,需要分开备份。
# 备份数据(仓库、数据库、上传的文件等)
sudo gitlab-backup create
# 配置文件单独备份(包含密钥,妥善保管)
sudo cp /etc/gitlab/gitlab.rb /backup/gitlab.rb
sudo cp /etc/gitlab/gitlab-secrets.json /backup/gitlab-secrets.json
配置定时备份:
sudo crontab -e
# 每天凌晨 2 点自动备份
# 0 2 * * * /opt/gitlab/bin/gitlab-backup create CRON=1
CRON=1 是告诉 GitLab 这是定时任务触发的,只有在出错时才输出日志,避免每天凌晨塞满 crontab 日志。
恢复时有一个关键细节:备份文件的 GitLab 版本必须和当前安装版本完全一致,否则恢复会失败。这也是版本锁定的另一个原因——随意升级之后,旧备份可能无法用于恢复。