Docker 部署 Gitea:与宿主机共用 22 端口的完整方案(含 CDN 无法走 SSH 的解法)
自建 Gitea 的人几乎都会遇到同一个纠结:宿主机自己也要用 22 端口做 SSH 远程管理,而 Gitea 的 SSH 克隆又默认走 22,两边撞车。常见的妥协是把 Gitea 映射到 2222 之类的端口,然后 clone 地址里带一串端口号,又丑又麻烦。
这篇文章记录我如何让 Docker 里的 Gitea 和宿主机共用同一个 22 端口,以及中间踩过的几个坑(尤其是 SSH_ORIGINAL_COMMAND 被 sudo 吞掉这个隐蔽问题)。文末还附上「服务器挂了 CDN 之后 SSH 走不了域名」的解法。所有 IP、域名、密钥等敏感信息均已脱敏。
一、为什么需要共用 22 端口
理想状态下,我们希望:
ssh root@服务器—— 走 22,正常远程管理宿主机;git clone ssh://git@服务器/用户/仓库.git—— 也走 22,直接交给 Gitea。
但 22 端口只能被一个进程监听。宿主机自带的 sshd 已经占着了,Gitea 容器里的 sshd 就没法再绑定 22。
核心思路其实不复杂:让宿主机 sshd 继续占着 22,遇到 git 用户的请求时,把它「转发」给 Gitea 容器去处理。这样对客户端来说,git@服务器:22 就是 Gitea,对宿主机来说,root@服务器:22 还是原来的远程管理。
二、环境准备
我的部署:宿主机是 Debian 12,Gitea 用 Docker 跑(官方镜像 gitea:1.27),容器内 git 用户的数据卷挂载在宿主机的 /root/dpanel-1/compose/gitea/gitea,Gitea 的 SSH 目前映射到宿主的 2222 端口。
关键信息:Gitea 容器内自带的 sshd 监听 22,宿主机通过 -p 2222:22 把它暴露出来。Gitea 的 app.ini 里 SSH_PORT=22、SSH_LISTEN_PORT=22。
三、实施步骤
1. 在宿主机创建 git 系统用户
宿主机需要一个 git 用户来承接转发,注意别和容器里的 git(UID 1000)冲突,我这里用系统用户:
useradd --system --shell /bin/sh --home /home/git --create-home git
2. 配置宿主机 sshd
写一个 drop-in 配置 /etc/ssh/sshd_config.d/60-gitea.conf,专门针对 git 用户:
Match User git
AuthorizedKeysCommand /usr/local/bin/gitea-authorized-keys.sh
AuthorizedKeysCommandUser root
PermitTTY no
X11Forwarding no
AllowTcpForwarding no
为什么用 AuthorizedKeysCommand 而不是直接 AuthorizedKeysFile?这是我踩的第一个坑,下面细说。
3. 让 sshd 能读到 Gitea 的公钥
Gitea 把用户上传的 SSH 公钥写进 authorized_keys 文件里,格式是每行带 command="gitea serv key-N" 前缀的标准格式。我们需要宿主机 sshd 用这份文件来做认证。
最直接的想法是 AuthorizedKeysFile 指向这个文件:
AuthorizedKeysFile /root/dpanel-1/compose/gitea/gitea/git/.ssh/authorized_keys
结果认证时报 Permission denied,查日志:
Could not open user 'git' authorized keys '...authorized_keys': Permission denied
原因:sshd 是以 git 用户的身份去读这个文件的,而这个文件属主是 steam、权限 600,git 用户根本读不到。
解法:改用 AuthorizedKeysCommand + AuthorizedKeysCommandUser root,让脚本以 root 身份读文件:
#!/bin/sh
# /usr/local/bin/gitea-authorized-keys.sh
cat /root/dpanel-1/compose/gitea/gitea/git/.ssh/authorized_keys 2>/dev/null
exit 0
4. 在宿主机放一个 gitea 转发器
Gitea 的 authorized_keys 里,每条公钥的命令是 /usr/local/bin/gitea --config=... serv key-N。宿主机 sshd 认证通过后,会执行这条命令。但宿主机上并没有 gitea 二进制,所以我们要放一个同名的转发脚本:
#!/bin/sh
# /usr/local/bin/gitea —— 把 Gitea SSH 请求转给容器
exec sudo -n -E docker exec -i -e SSH_ORIGINAL_COMMAND -u git gitea /usr/local/bin/gitea "$@"
再给 git 用户配一条精确的 sudoers 规则(/etc/sudoers.d/gitea),允许它免密执行这条 docker 命令:
git ALL=(root) NOPASSWD:SETENV: /usr/bin/docker exec -i -e SSH_ORIGINAL_COMMAND -u git gitea /usr/local/bin/gitea *
然后 visudo -cf 校验、sshd -t 校验、systemctl restart ssh,就完成了。
四、踩坑记录(重点)
这套配置看起来简单,但我在实际调试时卡了三个坑,每一个都花了不少时间定位。
坑 1:authorized_keys 权限
前面说过,sshd 以 git 用户身份读文件失败。日志里那句 Permission denied 很明确,但如果你没往「文件属主」上想,容易误以为是自己 authorized_keys 格式写错了。
坑 2:SSH_ORIGINAL_COMMAND 被 sudo 吞掉
这是最隐蔽的一个。配置好之后,clone 时报:
Hi there, 用户名! You've successfully authenticated, but Gitea does not provide shell access.
说明认证已经通了,但 Gitea 收不到真正的 git 命令。原因在于:Gitea 的 serv key-N 需要从环境变量 SSH_ORIGINAL_COMMAND 里拿到 git-upload-pack 或 git-receive-pack,才能知道该执行什么。
而我的转发脚本里用了 sudo,sudo 默认 env_reset,会把 SSH_ORIGINAL_COMMAND 这个环境变量清掉。于是 Gitea 只看到了 serv,没看到真正的命令。
解法是在 sudoers 规则里加 SETENV,并且 sudo 用 -E 保留环境,同时在 docker exec 里显式用 -e SSH_ORIGINAL_COMMAND 把变量传进容器。
坑 3:docker exec 不透传环境变量
和坑 2 相关但独立——即使 sudo 保留了环境,docker exec 默认也不会把宿主的环境变量带进容器。必须显式 -e SSH_ORIGINAL_COMMAND 才行。
这三点连起来,就是转发脚本那一长串参数的意义:
exec sudo -n -E docker exec -i -e SSH_ORIGINAL_COMMAND -u git gitea /usr/local/bin/gitea "$@"
五、验证
全部配好后,从本机直接克隆(注意:地址里不带端口号):
git clone ssh://git@服务器/用户/仓库.git
如果能看到仓库文件正常拉下来、push 也成功,就说明 22 端口共用已经打通。root 的远程管理也完全不受影响。
顺带一提:调试时我用一个空仓库测试,clone 报了 Internal Server Error,一度以为是链路问题,后来发现是那个仓库本身是空的镜像仓库(origin 指向 GitHub 且容器访问不了 GitHub),换了个有真实内容的仓库就秒通了。排查问题时,先用一个有内容的仓库验证,能少走很多弯路。
六、补充:服务器挂了 CDN,SSH 走不了域名怎么办
如果你的服务器前面挂了边缘加速 / CDN(比如给博客加速),大概率会遇到新问题:CDN 只做七层(HTTP/HTTPS)加速,不代理 22 端口。这时候域名解析到了 CDN 的 CNAME,ssh git@域名 或 git clone ssh://git@域名/... 会直接连不上,只有直连 IP 才能用 SSH。
如果你的 CDN 不支持四层代理(很多都不支持),最省事的解法是在本机 ~/.ssh/config 里写死别名,绕过 DNS 解析:
Host git.example.top git-srv
HostName 1.2.3.4
User git
IdentityFile ~/.ssh/id_ed25519
这样 git clone git-srv:用户/仓库.git 会直接用 IP 走 22,不受 CDN 影响。日常维护则可以再配一个 root 的别名。
如果确实想让域名也能走 SSH,另一种思路是改用 HTTPS clone(CDN 能加速),代价是 push 时要输密码或配 Personal Access Token,不如 SSH 密钥方便,按需取舍即可。
七、总结
让 Docker 里的 Gitea 和宿主机共用 22 端口,本质是「宿主机 sshd 按用户分流 + 转发到容器」。整套下来需要四个文件:
/etc/ssh/sshd_config.d/60-gitea.conf—— sshd 匹配 git 用户/usr/local/bin/gitea-authorized-keys.sh—— root 身份读 Gitea 公钥/usr/local/bin/gitea—— 命令转发器/etc/sudoers.d/gitea—— 精确的免密 docker exec 权限
真正容易踩坑的是 SSH_ORIGINAL_COMMAND 在 sudo 和 docker exec 两层之间被清掉,以及 authorized_keys 的文件权限问题。把这些想通了,方案本身其实很干净。
希望这篇记录能帮你少踩几个坑。如果你也在折腾自托管,欢迎交流。