飞牛 NAS 部署 Gitea,SSH 克隆失败怎么排查?

Web 能打开,只能证明 HTTP 链路正常,不能证明 Gitea 的 SSH 服务、端口映射、公钥认证和仓库路径没有问题。飞牛 NAS 上遇到 SSH 克隆失败时,最短排查顺序是:先看容器和日志,再检查 /data 挂载与宿主机端口,最后验证 SSH 密钥和仓库地址,不要一上来就重建容器。

在 Compose 项目目录中,可以先执行这组命令:

docker compose config
docker compose ps
docker compose logs --tail=100 gitea
docker compose port gitea 22

# 将下面的地址、owner 和 repo 换成实际值
NAS_IP=192.168.1.10
ssh -T -p 2222 "git@${NAS_IP}"
git ls-remote "ssh://git@${NAS_IP}:2222/owner/repo.git"

docker compose port gitea 22 没有结果,通常表示容器没有发布 SSH 端口;有结果但连接失败,则继续看认证和仓库路径。ssh -T 即使因为 Gitea 不提供普通 Shell 而返回非零状态,只要认证已经成功,就不能把它直接判定为部署失败。把散落的脚本、Docker 配置和代码仓库集中到 NAS,是这类社区方案走红的主要吸引力;后续维护体验则取决于目录、端口和备份配置。

先把报错归到四条链路

排查时,先分别确认四条链路。容器链路检查容器是否运行、是否因目录权限或数据库连接反复重启;存储链路检查 /data 是否挂到了正确目录、容器进程是否有写权限;传输链路检查宿主机端口是否发布、是否被占用,以及路由和防火墙是否放行;身份与仓库链路检查 SSH 公钥是否注册,以及仓库路径、权限和大小写是否正确。

这几条链路要分开验证。例如,浏览器能访问 http://192.168.1.10:3000,只排除了 Web 端口和数据库连接问题,并没有验证宿主机 2222 端口是否真的转发到容器 22 端口。Git 官方的 git ls-remote 可以在不创建新目录的情况下检查远程引用,比等到 git clone 失败后再猜路径更直接。

fnOS 项目目录这样规划

在飞牛文件管理器中建立独立的项目目录,再进入 Docker 的 Compose 面板选择该路径并创建 docker-compose.yml,这是 fnOS 社区案例中采用过的操作方式;具体菜单文字可能随 fnOS 版本变化,应以当前界面为准。

建议目录保持简单:

<fnOS 实际卷路径>/docker/gitea/
├── .env
├── docker-compose.yml
└── data/

社区案例中出现过 /vol1/1000/docker/... 这样的路径,但它只能视为特定设备的示例。正式配置应使用文件管理器显示的实际存储池路径,也不能从路径里的 1000 反推目录属主。

相对挂载 ./data:/data 会以 Compose 项目目录为基准。只要项目路径不变,执行 docker compose down、重新创建容器或强制重建容器,都不会删除这个绑定目录。绝对路径不依赖项目名称,但迁移存储池或调整卷结构时更容易失效。无论选哪一种,都应通过下面这条命令确认 Docker 最终解析出的源目录:

docker inspect gitea --format '{{range .Mounts}}{{println .Source "->" .Destination}}{{end}}'

如果重建后数据“消失”,首先检查这里,而不是怀疑 Gitea 自身。

镜像和数据库先做取舍

镜像标签应固定到经过核验的 Gitea 版本,不要长期追随 latest。官方镜像同时提供 root 和 rootless 变体,两者的权限模型不同:rootless 镜像要求宿主目录从一开始就对运行用户可写;不同版本使用的 UID、GID 环境变量也可能不同,应按所选标签的官方镜像文档配置,而不是把两套变量混用。

下面的 .env 是模板,其中 IP 和镜像标签都必须替换:

GITEA_IMAGE=gitea/gitea:REPLACE_WITH_VERIFIED_TAG
NAS_IP=192.168.1.10
HTTP_PORT=3000
SSH_PORT=2222

REPLACE_WITH_VERIFIED_TAG 不是真实标签。使用 rootless 镜像时,应选择官方提供的对应版本标签,而不是随意拼接后缀。

使用 SQLite 时,Compose 可以写成:

services:
  gitea:
    image: ${GITEA_IMAGE:?set GITEA_IMAGE in .env}
    container_name: gitea
    restart: unless-stopped
    environment:
      GITEA__database__DB_TYPE: "sqlite3"
      GITEA__server__ROOT_URL: "http://${NAS_IP}:${HTTP_PORT}/"
      GITEA__server__SSH_DOMAIN: "${NAS_IP}"
      GITEA__server__SSH_PORT: "${SSH_PORT}"
    ports:
      - "${HTTP_PORT}:3000"
      - "${SSH_PORT}:22"
    volumes:
      - "./data:/data"

这些 GITEA__section__KEY 环境变量依赖官方 Docker 镜像的配置转换机制,字段含义和优先级应按具体版本核对。

启动前先检查 Compose 展开后的最终配置,再拉取镜像并启动,随后查看状态和日志:

docker compose config
docker compose pull
docker compose up -d
docker compose ps
docker compose logs --tail=100 gitea

首次打开 http://192.168.1.10:3000 后,根据当前版本显示的初始化页面创建管理员。初始化完成后,应以 Gitea 仓库页面实际展示的克隆地址为准。

个人使用且仓库量不大时,SQLite 足够简单,数据库文件也位于 /data 的持久化范围内。已有 MySQL 运维习惯,或者需要独立数据库备份时,可以改成外部 MySQL。飞牛上已有容器化运行 MySQL 与 Gitea 的公开实践,可作对照,但要注意:MySQL 只保存数据库数据,仓库、附件、LFS 和配置仍要依靠 /data 持久化。

外部 MySQL 可以增加类似下面的服务,并把 Gitea 的数据库配置改为:

services:
  db:
    image: mysql:${MYSQL_IMAGE_TAG:?set MYSQL_IMAGE_TAG}
    container_name: gitea-db
    restart: unless-stopped
    environment:
      MYSQL_DATABASE: "gitea"
      MYSQL_USER: "gitea"
      MYSQL_PASSWORD: "${DB_PASSWORD:?set DB_PASSWORD}"
      MYSQL_ROOT_PASSWORD: "${DB_ROOT_PASSWORD:?set DB_ROOT_PASSWORD}"
    volumes:
      - "./mysql:/var/lib/mysql"
    expose:
      - "3306"
      GITEA__database__DB_TYPE: "mysql"
      GITEA__database__HOST: "db:3306"
      GITEA__database__NAME: "gitea"
      GITEA__database__USER: "gitea"
      GITEA__database__PASSWD: "${DB_PASSWORD:?set DB_PASSWORD}"

expose 不会把 3306 发布到局域网或公网。Gitea 和 MySQL 位于同一个 Compose 网络时,直接使用 db:3306 连接即可。

/data 挂错就像重新安装

Gitea 的仓库、附件、LFS、头像、SQLite 数据库和大部分运行配置都位于 /data 范围。漏掉这个绑定挂载,容器虽然可以正常启动,页面也可能短暂可用,但重建后看起来就像重新安装了一台 Gitea。

外部 MySQL 也不能替代 /data。数据库还在、仓库目录却为空,通常就是只备份了数据库而漏掉了仓库文件。

权限排查先看容器主进程实际使用的 UID、GID,再与宿主目录属主比较:

docker compose exec gitea sh -c 'grep -E "^(Uid|Gid):" /proc/1/status'
docker compose exec gitea sh -c 'ls -ldn /data'
stat -c '%u:%g %a %n' ./data

还可以直接做一次最小写入测试:

docker compose exec gitea sh -c 'touch /data/.write-test && rm /data/.write-test'

如果目录需要调整属主,先停止服务,再根据上面查到的实际 UID、GID 修改。不要直接把 chmod 777 当成通用修复,它会掩盖挂载路径错误,也无法处理飞牛共享文件夹的 ACL 问题。

docker compose stop gitea
sudo chown <容器UID>:<容器GID> ./data
docker compose up -d gitea

如果只有某个子目录报错,应根据日志中的具体路径定向修改,不要直接对整个存储池执行递归 chown。在 fnOS 场景中,应把目录规划、持久化和备份策略作为一套运维配置记录下来,而不是只留下一个 Compose 文件。

Web 与 SSH 使用两套端口

示例中的 3000:3000 表示宿主机 3000 端口映射到容器 3000 端口;2222:22 则表示宿主机 2222 端口映射到容器 22 端口。2222 只是为了避让 NAS 自身常用的 22 端口,不是 Gitea 的固定要求。

检查映射和宿主机监听状态:

docker compose port gitea 3000
docker compose port gitea 22
sudo ss -lntp | grep -E ':(22|2222|3000)[[:space:]]'

如果系统没有 ss,可以改用 netstat -lntp。端口被其他进程占用时,应修改 Compose 中的宿主机端口,而不是改容器内部监听逻辑。

这里容易混淆两个配置:宿主机发布 2222:22 时,容器仍监听 22,Gitea 展示 SSH 地址所使用的外部端口则由 SSH_PORT=2222 控制。如果容器内部 SSH 监听端口也被改成 2222,映射就应相应写成 2222:2222。ROOT_URL、SSH_DOMAIN 和 SSH_PORT 应与用户实际访问的地址保持一致,具体字段按所选版本核对。

内网直接访问时,可以保持当前配置。公网使用时,HTTP 适合放在 HTTPS 反向代理后面,但 ROOT_URL 要改成公开地址;SSH 不是 HTTP,不能靠普通反向代理转发,更适合使用 VPN 或受控隧道。没有必要把 Gitea 的 HTTP、SSH 和 MySQL 端口全部直接暴露到公网。

HTTP 与 SSH 是两套独立链路

链路 克隆地址示例 认证方式 常见问题
HTTP http://192.168.1.10:3000/owner/repo.git 用户名与密码、令牌 密码错误、令牌无效、二次验证、反向代理配置
SSH ssh://git@192.168.1.10:2222/owner/repo.git SSH 公钥 端口不通、公钥未注册、无仓库权限

HTTP 和 SSH 的端口、认证入口和错误处理逻辑各不相同。HTTP 正常不能证明 SSH 正常,SSH 公钥认证成功也不能证明网页入口的密码或令牌有效。

在客户端生成密钥:

ssh-keygen -t ed25519 -C "gitea-nas"

然后把 .pub 公钥添加到 Gitea 账户的 SSH 密钥中;如果使用的是仓库部署密钥,还要另外确认该密钥对目标仓库具有权限。不同版本的菜单名称可能不同,以当前页面为准。

测试时显式指定密钥和端口:

ssh -T -i ~/.ssh/id_ed25519 -p 2222 git@192.168.1.10
git ls-remote "ssh://git@192.168.1.10:2222/owner/repo.git"

Gitea 内置 SSH 服务通常只允许 Git 操作,不提供普通 Shell。出现“认证成功,但拒绝 Shell”之类的提示属于预期行为。若 git ls-remote 成功返回引用,说明远程仓库和认证链路已经可用;没有输出但退出码正常,也可能是空仓库。

端口映射不等于 SSH 直通

社区文章常把“SSH 直通”“透明 SSH 访问”放在同一标题下,但实现前提并不相同。 SSH 本身不要求使用 22 端口,所以多数情况下没有必要追求真正的端口直通。

方案 客户端地址 优点 前提与代价
Docker 端口映射 ssh://git@NAS:2222/... 配置最少 克隆地址必须带端口
经 NAS SSH 转发 ssh://git@127.0.0.1:2222/... 客户端只连接一个入口 需要保持转发会话,并允许 NAS SSH 转发 TCP
network_mode: host 取决于容器内部监听端口 不再经过端口映射 Linux 专用,容易与 fnOS 自身 SSH 的 22 端口冲突
独立 SSH sidecar 可单独占用宿主机端口 SSH 服务与 Gitea 分离 需要同步授权密钥、仓库权限、UID/GID 和主机密钥,维护项最多

例如,经 NAS SSH 转发时,可以建立本地隧道:

ssh -N -L 2222:127.0.0.1:2222 fnos-user@192.168.1.10

另一个终端再使用:

git clone "ssh://git@127.0.0.1:2222/owner/repo.git"

如果 NAS 禁用了 TCP 转发,这条路径就不能使用。宿主机网络模式也有冲突风险:当 fnOS 已经监听 22 端口时,Gitea 内置 SSH 若仍监听 22,就会直接冲突。

非标准端口不能简单写成:

git@NAS:2222/owner/repo.git

这种写法会把 2222 当成仓库路径的一部分。正确写法是完整 SSH URL:

git clone "ssh://git@192.168.1.10:2222/owner/repo.git"

如果经常使用非标准端口,可以通过 SSH Config 隐藏它:

Host gitea-nas
    HostName 192.168.1.10
    User git
    Port 2222
    IdentityFile ~/.ssh/id_ed25519

之后可以直接执行:

git clone gitea-nas:owner/repo.git

按报错原文缩小排查范围

报错或现象 优先检查 常见原因
Connection refused 容器状态、端口映射、宿主机监听 容器未运行、映射漏写、目标端口没有监听
Connection timed out 路由、防火墙、VPN 地址不可达、数据包被丢弃、隧道未建立
Permission denied (publickey) Gitea 中登记的公钥、SSH Config 密钥未注册、使用了错误私钥、账户或密钥被限制
Repository not found 仓库 URL、访问权限、名称大小写 路径写错、私有仓库无权限、仓库确实不存在
HTTP 401 或 403 密码、令牌、二次验证、反向代理 凭据无效、令牌权限不足、代理附加了认证限制
启动时 Permission denied /data 属主与容器 UID/GID 绑定挂载错误、rootless 目录不可写
REMOTE HOST IDENTIFICATION HAS CHANGED 新旧 SSH 主机密钥 容器重建或恢复后主机密钥发生变化

Repository not found 不一定表示仓库真的不存在。Gitea 可能用同一个提示隐藏私有仓库的访问失败;如果认证阶段已经明确返回 Permission denied (publickey),应先修复公钥,再继续检查仓库路径。

需要看 SSH 握手停在哪一步时,再打开详细日志:

ssh -vvv -T -i ~/.ssh/id_ed25519 -p 2222 git@192.168.1.10

也可以让 Git 通过 SSH 输出握手详情:

GIT_SSH_COMMAND='ssh -vvv -p 2222' \
  git ls-remote "ssh://git@192.168.1.10:2222/owner/repo.git"

遇到主机指纹变化时,不要直接删除整个 known_hosts。先核对服务身份和备份的密钥:

ssh-keygen -F '[192.168.1.10]:2222' -f ~/.ssh/known_hosts

确认确实更换了主机密钥后,只删除对应主机记录:

ssh-keygen -R '[192.168.1.10]:2222'

备份要覆盖数据和部署信息

只备份 docker-compose.yml 只能恢复部署方式,不能恢复代码。完整备份至少要覆盖数据库、Git 仓库与附件/LFS、Gitea 配置和敏感项、SSH 主机密钥,以及 Compose 文件、镜像版本和实际端口。

对提供官方 dump 命令的 Gitea 版本,优先使用官方导出方式。先创建宿主机和容器内的备份目录:

mkdir -p backup
docker compose exec gitea sh -c 'mkdir -p /data/backup'

STAMP=$(date +%Y%m%d-%H%M%S)
docker compose exec gitea gitea dump --file "/data/backup/gitea-dump-${STAMP}.zip"
docker compose cp "gitea:/data/backup/gitea-dump-${STAMP}.zip" "./backup/"

该命令及其参数应以所选 Gitea 版本的官方备份文档为准。 导出完成后,还要确认压缩包中确实包含所需仓库、附件、LFS 和配置,不能只看命令退出状态。

使用外部 MySQL 时,额外执行数据库原生备份:

docker compose exec -T db sh -c 'exec mysqldump --single-transaction -u"$MYSQL_USER" -p"$MYSQL_PASSWORD" "$MYSQL_DATABASE"' > "backup/gitea-db-${STAMP}.sql"

如果应用账号权限不足,应改用官方文档规定的备份账号。不要在 Gitea 运行时只复制正在写入的 SQLite 文件;需要制作原始文件副本时,应先停服务,或者使用官方 dump。

还可以停机后制作一份完整数据归档,作为文件级兜底:

docker compose stop gitea

DATA_DIR="$(pwd)/data"
BACKUP_DIR="$(pwd)/backup"
tar -C "$DATA_DIR" -czf "$BACKUP_DIR/gitea-data-${STAMP}.tgz" gitea

docker compose start gitea

这类归档通常会同时包含 SQLite、仓库、附件、LFS、app.ini 和内置 SSH 主机密钥,但外部 MySQL 的数据不在其中。app.ini 可能包含密钥和令牌,应加密保存,不能提交到 Git。

为备份生成校验值:

sha256sum "backup/gitea-dump-${STAMP}.zip" > "backup/gitea-dump-${STAMP}.zip.sha256"
sha256sum "backup/gitea-db-${STAMP}.sql" > "backup/gitea-db-${STAMP}.sql.sha256"

最后把备份复制到另一块物理磁盘或另一台设备。不要只把备份放在同一个 /data 目录里,否则存储介质损坏时,代码和备份会一起丢失。升级 Gitea 镜像之前,也应先完成一次可校验的备份。

恢复后用真实克隆验收

恢复前先确认 dump 对应的 Gitea 版本与准备启动的版本兼容,并保留当前数据副本。官方恢复命令是 gitea restore --from ...,但运行用户和参数可能随版本调整,应以该版本的恢复文档为准。

对于前面使用的 /data 挂载布局,可以先停止服务,再通过一次性容器执行恢复:

mkdir -p ./data/backup
cp "/path/to/gitea-dump.zip" ./data/backup/gitea-dump.zip

docker compose stop gitea
docker compose run --rm --entrypoint sh gitea -c \
  'gitea restore --from /data/backup/gitea-dump.zip'
docker compose up -d gitea

如果所选版本的官方文档要求以特定 UID、GID 运行恢复命令,应给 docker compose run 加上对应的 --user;rootless 镜像不要为了省事切换成 root,否则可能再次制造属主问题。外部 MySQL 的原生备份应导入准备好的目标数据库,具体顺序不要与官方 dump 恢复流程重复执行。

启动后先检查日志和写入能力:

docker compose logs --tail=100 gitea
docker compose exec gitea sh -c 'grep -E "^(Uid|Gid):" /proc/1/status; ls -ldn /data'

随后分别验证 HTTP 和 SSH:

git ls-remote "http://192.168.1.10:3000/owner/repo.git"
git ls-remote "ssh://git@192.168.1.10:2222/owner/repo.git"

最后做真实克隆,而不是只停留在接口检查:

git clone "http://192.168.1.10:3000/owner/repo.git"
git clone "ssh://git@192.168.1.10:2222/owner/repo.git"

私有仓库应使用实际凭据完成验证。使用 LFS 的仓库还要确认 LFS 对象能正常拉取,附件则应通过 Web 页面实际打开检查。恢复后若 SSH 指纹发生变化,先核对备份的旧密钥或新生成密钥,再决定是否更新客户端记录。

部署收尾时,先保存实际使用的镜像版本、Web 端口和 SSH 端口,再生成第一份 Gitea 官方 dump 并复制到另一块磁盘,最后分别用 HTTP 和 SSH 克隆同一个测试仓库。两条链路都完成真实克隆验证后,才能确认这套 fnOS 上的 Gitea 已具备可维护、可恢复的运行基础。

© 版权声明
THE END
喜欢就支持一下吧
点赞0 分享
评论 抢沙发

请登录后发表评论

    暂无评论内容