
1. 为什么我不用Docker Compose而把Gitea搬进K8s先说结论如果你的 Kubernetes 集群已经稳定跑了一段时间手头资源又不紧张那么把 Gitea 迁到 K8s 里面总体收益是大于折腾成本的。这套操作我从裸机二进制部署、Docker Compose 部署一路走过来最后停在 K8s 部署上原因很实际。Gitea 是一个非常轻量的 Git 托管服务Go 语言写的单二进制文件就能跑内存占用在几百 MB 级别。很多人第一反应是这么轻的东西有必要上 Kubernetes 吗用 Docker 跑一下不就行了这个问题我在动手之前也纠结过。后来让我下决心的几个点分别是统一基座、滚动更新不中断、故障自动恢复、和现有监控/日志体系无缝对接。如果团队里已经用 K8s 管理其他服务那单独给 Gitea 搞一台 Docker 主机反而成了另类运维上多一套心智负担。而且 Gitea 对团队来说是基础服务挂了大家都不能提交代码它的高可用优先级比一般内部工具要高。K8s 的控制器模型天然给了我们 restartPolicy、探针、滚动更新这些能力不用自己写 systemd 脚本或者 docker restart 策略。另外一个很现实的原因是热词里反复出现了 kubeadm、kubernetes v1.26.0、preflight 检查这些东西说明很多人在自己搭 K8s 集群然后想在集群里跑点实际业务。Gitea 恰恰是那种适合拿来当第一个吃螃蟹的应用。它不像数据库那样对 IO 极其敏感也不像消息队列那样有复杂的分布式协调逻辑但又有持久化、网络暴露、配置管理、SSH 通道这些典型需求。把它完整部署一遍等于把 K8s 里面的 StatefulSet、PVC、Service、Ingress、ConfigMap 全串了一遍。等这套通了后面再部署别的有状态应用套路基本就熟了。当然也不是说所有人都应该这么做。如果你只有一个节点、两块硬盘、内存不到 4G那还是直接用二进制或者 Docker 省事K8s 本身会吃掉一部分资源。本篇面向的是已经有一个 K8s 集群不管单节点还是多节点、想在集群里跑 Gitea、并且希望部署完以后不是一次性的能跑就行而是能长期稳定用下去的场景。接下来的内容全部基于我自己实际操作的经验涉及到的 YAML 也都是经过验证的。2. 部署前的物料清单镜像、存储与版本兼容性2.1 Gitea 镜像版本怎么选Gitea 官方镜像在 Docker Hub 上仓库名是 gitea/gitea官方也提供了 ghcr.io 的镜像国内环境建议用 Docker Hub。镜像 tag 的选择上我建议不要追 latest。之前亲眼见过一次事故某次 Gitea 发版后 latest 指向了新版本但新版本对 PostgreSQL 的版本要求做了调整团队里几个人同时 pull 了最新镜像结果数据库迁移脚本报错服务起不来。后来统一改成固定版本号比如 gitea/gitea:1.21.0再也没出过这种问题。这里还有一个选择维度镜像的操作系统基础。官方镜像分 alpine 和 debian 两类。alpine 镜像小大概 50MB 左右适合对磁盘敏感的场景debian 镜像大约 100MB 出头但内置了更多依赖遇到问题排查时可用工具更多。我在生产环境用的是 alpine 镜像但如果你对容器内排查不太熟悉建议先用 debian 镜像至少里面能翻出 bash、curl、ps 这些基础工具。2.2 Kubernetes 版本兼容性确认从项目正文里给的信息来看集群是 v1.26.0用 kubeadm 初始化的。这个版本对应的是 K8s 在 2023 年初左右的 Release内置的 apiserver、kubelet 对 StatefulSet、PVC、Ingress 这些资源对象的校验规则已经相当成熟。Gitea 部署涉及的 API 资源没有特别新潮的字段比如 volumes、探针、环境变量注入这些在 1.26 上完全没问题。需要注意的一点是如果你用的 Ingress Controller 版本过老可能不支持 1.26 的 networking.k8s.io/v1 API 中新增的某些字段比如 pathType 的 ImplementationSpecific。我们集群里用的 ingress-nginx 控制器版本是 1.7 左右和 K8s 1.26 配合没问题。如果是更早的 0.x 版本建议升级后再跑 Gitea 的 Ingress。2.3 存储方案直接决定 Gitea 后期好不好用Gitea 要持久化的东西主要有三块仓库数据、数据库文件或者外接数据库、配置文件。仓库数据是核心资产数据库是元数据配置文件是应用状态。我见过有人图省事直接挂 emptyDir结果 Pod 一重启仓库全没了这种教训不能再传播了。存储方案我分三种场景给建议场景存储方案说明单节点测试环境local-path 或者 hostPathk3s 自带 local-path-provisioner如果是 kubeadm 搭的可以手动装 rancher/local-path-provisioner多节点集群且仓库量不大NFS 动态供给存储池统一管理支持 ReadWriteMany方便后续扩容生产多节点云厂商块存储或者 Ceph RBD性能最好就是成本高一点我自己用的是 NFS具体原因后面第五部分会讲到我踩过的坑。这里先给出结论如果你是单节点用 local-path 是最省心的如果你是多节点但 PV 数量不多NFS 也够用不要试图用 hostPath 在多节点上跑Pod 漂移后数据跟不过去。2.4 数据库取舍SQLite 还是 PostgreSQLGitea 默认支持 SQLite、MySQL、MariaDB、PostgreSQL。部署在 K8s 里的时候很多人喜欢开一个 MySQL 实例配套其实这中间是有隐形成本的。对于 Git 仓库元数据这种量级的写入SQLite 完全够用。我的一个内部团队用了三年 SQLite仓库数量 200操作人数十几个人从来没出过性能问题。但 SQLite 有一个致命弱点不支持网络并发锁。如果把 SQLite 文件放在 NFS 上多个 Pod 同时访问同一个 SQLite 文件时轻则性能骤降重则直接触发 database is locked 错误。所以如果你是单副本部署Gitea 官方也建议单副本它对多副本支持有限SQLite 放 local-path 上是没问题的。但如果你用了 NFS 存储我强烈建议直接用 PostgreSQL。PostgreSQL 本身可以跑在集群之外也可以部署在集群内的另一个 StatefulSet 里用内网 Service 连接这样 Gitea 的无状态性更好将来升级副本数也更容易。我在部署时选择的是外接 PostgreSQL 13单独部署数据目录在独立 PV 上。这样的好处是Gitea 的 StatefulSet 可以随时删掉重建仓库数据在 NFS 上数据库在另一个机器上互不干扰。3. 核心 YAML 拆解StatefulSet 与 PVC 的设计思路3.1 创建 Namespace 和专用配置我习惯把所有 Gitea 相关的资源放在独立 Namespace 里名字就叫 gitea。之前见过有人直接把所有东西丢在 default Namespace 下后面看资源列表的时候乱成一团。一条命令的事隔离清楚对自己后面排障有莫大好处。kubectl create namespace gitea为什么用 Namespace 隔离还有一个原因网络策略、资源配额可以精确控制在这个范围内的资源用量。如果团队里多套系统共存不隔离的情况下 Gitea 有可能被别人的大任务挤占资源到时候 Git 操作卡顿排查半天才发现是别的命名空间在抢 CPU。3.2 创建 PVC 的几种姿势Gitea 容器官方镜像内部有两个目录需要持久化/data存放 Git 仓库、LFS 对象、配置文件、SSL 证书等如果你用了 SQLite数据库文件也在 /data 下/etc/gitea旧版本在某些镜像中会把配置放在这个目录1.21 之后统一收到 /data/gitea/conf 了如果你用 local-path 或者 NFS 动态供给创建 PVC 的写法比较统一apiVersion: v1 kind: PersistentVolumeClaim metadata: name: gitea-data-pvc namespace: gitea spec: accessModes: - ReadWriteOnce storageClassName: nfs-storage resources: requests: storage: 50Gi这里 accessModes 使用的是 ReadWriteOnce因为我们的 Gitea 只跑一个副本。50Gi 是给仓库和 LFS 留的初始空间这个量对几十人的团队来说能用很久了。如果后面不够扩容 PVC 需要存储驱动支持动态扩容NFS 这类方案通常都支持。如果你是单节点测试环境storageClassName 没有匹配的 provisioner 时可以手动建一个 PV 指向宿主机的目录。这种属于应急方案能跑但要注意宿主机目录的权限问题。Gitea 镜像默认以 git 用户运行UID 是 1000你挂载的宿主机目录如果不开放写权限容器启动时会报 permission denied这个坑我下面第五部分还会说。3.3 StatefulSet 的完整 YAML 解读Gitea 部署在 K8s 里最适合的控制器是 StatefulSet而不是 Deployment。原因不复杂StatefulSet 能给 Pod 提供稳定的网络标识和稳定的存储绑定虽然我们只跑一个副本但这种有状态应用的语义用 StatefulSet 表达更准确。下面是我实际在用的 YAML去掉了无关字段apiVersion: apps/v1 kind: StatefulSet metadata: name: gitea namespace: gitea spec: serviceName: gitea replicas: 1 selector: matchLabels: app: gitea template: metadata: labels: app: gitea spec: initContainers: - name: init-data-dir image: busybox:1.36 command: - sh - -c - | chown -R 1000:1000 /data volumeMounts: - name: data mountPath: /data containers: - name: gitea image: gitea/gitea:1.21.0 ports: - containerPort: 3000 name: http - containerPort: 22 name: ssh env: - name: GITEA__server__ROOT_URL value: https://git.example.com - name: GITEA__server__HTTP_PORT value: 3000 - name: GITEA__server__SSH_PORT value: 22 - name: GITEA__database__DB_TYPE value: postgres - name: GITEA__database__HOST value: postgres.gitea.svc.cluster.local:5432 - name: GITEA__database__NAME value: gitea - name: GITEA__database__USER value: gitea - name: GITEA__database__PASSWD valueFrom: secretKeyRef: name: gitea-db-secret key: password - name: GITEA__service__DISABLE_REGISTRATION value: false - name: GITEA__service__REQUIRE_SIGNIN_VIEW value: false volumeMounts: - name: data mountPath: /data readinessProbe: httpGet: path: /api/v1/health port: 3000 initialDelaySeconds: 10 periodSeconds: 10 livenessProbe: httpGet: path: /api/v1/health port: 3000 initialDelaySeconds: 30 periodSeconds: 20 resources: requests: memory: 512Mi cpu: 500m limits: memory: 1Gi cpu: 1 volumeClaimTemplates: - metadata: name: data spec: accessModes: - ReadWriteOnce storageClassName: nfs-storage resources: requests: storage: 50Gi这个 YAML 里有几个地方我要特别解释一下initContainer 的作用容器镜像首次以非 root 用户运行时如果挂载的目录权限不对Gitea 进程就没法写数据。我们看到的这个 initContainer 会在主容器启动前把 /data 目录的属主改成 UID 1000。这个操作在裸机部署时不需要但在 K8s 里因为挂载的卷可能带宿主机目录属主这一步就是保平安的。你可以把 initContainer 理解为装修队进场前先干一票粗活的工人。环境变量下划线双重下划线Gitea 官方支持通过环境变量覆盖 app.ini 配置项。规则是 GITEA__ 后面的内容依次对应 section、key用双下划线分隔。比如 GITEA__server__ROOT_URL 对应的是 app.ini 里 [server] 下的 ROOT_URL。用这个机制不需要手工修改 app.ini 文件全部通过环境变量注入配置在 K8s 里变得可版本化。如果你喜欢把所有配置集中在 ConfigMap 里也可以用 volumes 挂载 app.ini但环境变量方式的侵入性更低不会覆盖镜像默认配置。数据库密码用 Secret 引用这里没有把数据库密码硬编码在 YAML 里而是从 Secret 中读取。实践中这是必须养成的习惯。Git 仓库管理系统的密码泄露出去等于把代码托管的入口大门钥匙丢了。探针配置readinessProbe 请求的是 /api/v1/health 接口这个接口是 Gitea 自带的健康检查。如果数据库连接异常这个接口会返回非 2xxK8s 会把这个 Pod 从 Service 的 Endpoints 列表里摘掉不会让流量打到坏的实例上。livenessProbe 是检测进程是否假死如果多次失败kubelet 会重启容器。初始延迟时间的设置很关键Gitea 首次启动需要做数据库迁移可能耗时 30 秒以上initialDelaySeconds 配小了会误杀。我最初部署时把 initialDelaySeconds 设成了 5结果每次都检测失败被重启这个细节值得注意。3.4 Service 的设计普通 HTTP 和 SSH 分开Gitea 有一个服务端口3000走 HTTP/HTTPS 的 Web 界面和 Git HTTP(S) 协议另一个端口22走 SSH 协议。这两个端口的暴露方式应该分开设计。HTTP 端口走 IngressSSH 端口一般走 NodePort 或者 LoadBalancer。原因是 Ingress 控制器工作在网络层之上真正负责 HTTP 流量转发但 SSH 是裸 TCP 长连接标准的 HTTP Ingress 处理不了。虽然某些 Ingress Controller 也能通过 TCP 端口透传但配置复杂度明显上升不值得为这么小的一个需求去动全局配置。HTTP 的 Service 配置apiVersion: v1 kind: Service metadata: name: gitea-http namespace: gitea spec: selector: app: gitea ports: - name: http port: 3000 targetPort: 3000这个 Service 的类型是 ClusterIP默认的。Ingress 拿它当后端转发即可。SSH 的 Service 配置apiVersion: v1 kind: Service metadata: name: gitea-ssh namespace: gitea spec: type: NodePort selector: app: gitea ports: - name: ssh port: 22 targetPort: 22 nodePort: 30022这里 nodePort 必须指明不能让它自动分配。因为 Gitea 的 SSH clone URL 是写在页面顶部的如果端口随机分配用户看到的 clone 地址就会变来变去体验极差。设定为 30022 这种固定端口后所有节点的防火墙规则也方便统一配置。4. SSH 通道的坑Pod 重建后你的 Git 远程地址会失效4.1 SSH 认证的本质和 known_hosts 问题这部分单独拿出来写因为几乎所有人部署完以后都会在某天突然发现自己推不了代码提示The authenticity of host [git.example.com]:30022 cant be established. ECDSA key fingerprint is SHA256:xxxxxxxx. Are you sure you want to continue connecting (yes/no/[fingerprint])?如果是首次连接你输入 yes 就完事了。但如果之前已经连接过某次 Pod 重建后你再次推送代码会出现Host key verification failed.原因在于Gitea 容器内部跑的 sshd 服务在首次启动时会生成一对主机密钥host key存放在容器内 /data/ssh 目录。如果你的 PVC 没有把 /data 持久化Pod 删掉重建后新的容器会重新生成一套主机密钥客户端的 known_hosts 里面还保留着旧密钥。密钥对不上了Git 客户端就会拒绝继续连接。这个问题的解法很简单让主机密钥持久化。其实上面 StatefulSet 的 YAML 里面/data 整个目录已经持久化了所以 Gitea 的 SSH 主机密钥默认就被保存在了 PV 里Pod 重建后密钥不会变。但如果你之前的部署方式是不挂载 PVC 的或者只挂载了 /data/git 这样的子目录就会遇到这个问题。验证方法在 Gitea 容器的终端里看一下 /data/ssh 目录下有没有 ssh_host_ecdsa_key 这类文件。有的话说明密钥持久化了没有的话就是丢失状态。如果真丢了最省事的办法是把客户端的 known_hosts 里对应条目删掉重新接受一次指纹。但这只解决眼前问题等下次 Pod 重建又会出现治标不治本。4.2 SSH clone 地址的构造逻辑Gitea 页面上显示的 SSH clone 地址由三部分组成用户域名:端口/仓库名。这个域名和端口不是自动识别的而是根据配置动态生成。我们在环境变量里设置了 GITEA__server__ROOT_URLhttps://git.example.com对应的 SSH clone 地址往往是ssh://gitgit.example.com:30022/user/repo.git注意这里域名如果是通过 Ingress 暴露的域名一般指向 80/443 端口但 SSH 走的是 NodePort 30022。所以用户看到 clone 地址时端口一定是 30022 而不是 22。如果你前面配置的 nodePort 是 30022那没问题。如果忘记了 nodePort自动分配到了一个不固定的值那 clone 地址每次都会不一样这个坑踩过的人才懂。另外要注意Gitea 的 SSH 端口配置有两个地方。一个是容器内 sshd 监听的端口上面配置为 22另一个是用户在网页上看到的和 Git 命令实际连接的端口。两者不一定相同通过 GITEA__server__SSH_PORT 环境变量控制的其实是后者。我把容器内部的 sshd 留在 22SSH_PORT 也设成 22NodePort 层做了一层端口转换用户连的是 30022NodePort 转发到 Pod 的 22。整个过程对用户透明。4.3 多实例场景下的 SSH 会话保持单副本 Gitea 不需要考虑 SSH 会话保持K8s 层面 NodePort 会自动把请求转发到唯一 Pod。但如果你加了副本数比如做了一次横向扩容测试SSH 的流量会被负载均衡器轮询到不同 Pod。这种场景下如果 SSH 密钥没有同步到所有 Pod 的共享卷就会导致连不上、密钥不匹配之类的诡异问题。Gitea 官方其实不建议多副本跑因为 LFS 锁、Git hooks、配置文件的缓存一致性都很难保证。所以这里我拍个结论Gitea 在 K8s 里就老老实实跑单副本把 PVC 和探针配好稳定性排在花活前面。5. 实测踩坑排查链路从 Pod Crash 到无法推送这一节我给三个真实的排查案例都是我在部署过程中遇到过的。每一个我都会按照当时排查的顺序完整走一遍。这样的话你在自己环境里遇到类似问题时可以先从我这儿对号入座。5.1 小白一样的问题权限不足导致 Gitea 容器一直 CrashLoopBackOff现象是 Pod 创建后进入 CrashLoopBackOffkubectl logs 里能看到mkdir /data/gitea/conf: permission denied原因很典型PVC 绑定的是一个 hostPath 或者 NFS 目录目录属主是 rootGitea 容器以 UID 1000 运行没有写权限。裸机部署不存在这个问题因为二进制文件通常在宿主机上以当前用户运行容器部署时镜像内部用户和宿主机目录属主的权限映射就成了隐患。排查链路我走的是先看 Pod 状态kubectl get pod -n gitea发现反复重启。看日志kubectl logs gitea-0 -n gitea看到了 permission denied。进入 Pod 检查身份kubectl exec -it gitea-0 -n gitea -- id确认容器内用户是 UID 1000。登录宿主机检查挂载目录属主ls -ld /data/k8s/gitea发现属主是 root。解法就是前面提到的 initContainer 方案。在 Pod 模板里加一个 busybox init 容器启动时把 /data 目录的属主改成 1000:1000。当然如果 PV 是动态供给的本地存储你可以在创建 PV 时用 fsGroup 指定组 ID效果一样。initContainer 的方式更通用不依赖存储驱动的特性。5.2 NFS 存储遇上 SQLite白屏、超时、database is locked最开始我图省事NFS 上直接放了 SQLite 数据库文件。Gitea 页面刚打开还能正常访问一提交代码或者做一次登录经常卡住后端日志频繁出现[SQLITE_ERROR] SQL logic error: database is locked (5) (SQLSTATE 5A000)SQLite 在 NFS 上表现不好这个坑不是 Gitea 独有的。原因不复杂SQLite 依赖操作系统层面的 POSIX 文件锁来保证多进程互斥NFS 协议对锁语义的支持在多个客户端共享同一个文件时本身就有限。Gitea 是单进程应用理论上单 Pod 不会出现多进程竞争但 NFS 客户端缓存、延迟写、文件属性缓存这些机制叠加起来可能导致 Gitea 进程内部看到的锁状态和 NFS 服务端不一致最终表现为 database is locked。排查链路出现白屏后先看容器日志发现大量 SQLITE_ERROR。进入容器确认数据库路径ls -l /data/gitea/gitea.db确认是 SQLite。检查挂载类型mount | grep /data看到nfs字样。确认是 NFS SQLite 的组合问题。解法把数据库迁到 PostgreSQL。具体操作是先在集群里或集群外搭一个 PostgreSQL然后在 Gitea 的管理面板或者通过环境变量修改数据库连接指向 PostgreSQL再重启 Pod。Gitea 启动时会自动做数据库迁移旧数据会从 SQLite 同步进 PostgreSQL。这个流程不需要手动导出导入只要确保 PostgreSQL 里创建了同名数据库和账号Gitea 会自己完成。迁移完之后SQLite 的锁问题彻底消失。如果不想外接数据库另一个变通方案是换存储。把 Gitea 的数据卷从 NFS 换到本地盘SQLite 在本地盘上表现依然稳健。但这种方案意味着仓库数据不再具备跨节点漂移能力Pod 重建后只能调度到绑定磁盘的那个节点上灵活性打折。权衡下来我还是推荐外接 PostgreSQL。5.3 Ingress 层大仓库推送超时http timeout, client timeout这个问题藏得比较深。团队里有人新建了一个仓库推了一大堆历史代码上去首次推包传了几百 MB快推完的时候 Git 客户端报error: RPC failed; HTTP 504 curl 22 The requested URL returned error: 504 fatal: the remote end hung up unexpectedly504 这个状态码指向的是网关超时多数情况下不是 Gitea 本身的问题而是前面的 Ingress Controller 的代理超时配置太短。我的环境用的是 ingress-nginx它给后端请求设置的默认 proxy-read-timeout 是 60 秒。大仓库的首次推送可能超过 60 秒后端还在接收数据代理就切断了连接。排查链路直接通过 Service 做端口转发测试推送看是否复现问题kubectl port-forward -n gitea svc/gitea-http 3000:3000如果推包正常那问题就在 Ingress 层。选中 Ingress 控制器 Pod查看它的配置块kubectl exec -it ingress-nginx-controller-xxx -n ingress-nginx -- cat /etc/nginx/nginx.conf | grep proxy-read-timeout。确认超时参数为默认值后定位到 Ingress 对象需要加注解。解法是在 Ingress 的 metadata.annotations 中增加nginx.ingress.kubernetes.io/proxy-body-size: 0 nginx.ingress.kubernetes.io/proxy-read-timeout: 600 nginx.ingress.kubernetes.io/proxy-send-timeout: 600proxy-body-size 设置为 0 表示不限制 HTTP body 大小避免大对象推送时被 nginx 的默认 1MB 上限拦截。read-timeout 和 send-timeout 统一调整到 600 秒一般够用。如果你经常推送单文件超过 1GB 的巨型仓库可以考虑再往上调但正常的代码仓库用不到那个级别。完整 Ingress 配置供参考apiVersion: networking.k8s.io/v1 kind: Ingress metadata: name: gitea-ingress namespace: gitea annotations: nginx.ingress.kubernetes.io/proxy-body-size: 0 nginx.ingress.kubernetes.io/proxy-read-timeout: 600 nginx.ingress.kubernetes.io/proxy-send-timeout: 600 spec: rules: - host: git.example.com http: paths: - path: / pathType: Prefix backend: service: name: gitea-http port: number: 3000里面 pathType 用的是 Prefix对应路径前缀匹配。如果你只部署了这一个 Ingress写在根路径 / 下面没问题。如果同一个域名下还有其他服务可以考虑用子路径把 Gitea 放在 /git 下面但 Gitea 对 ROOT_URL 的配置就要同步改会增加一些匹配复杂度能分开域名就用独立域名省心。5.4 探针误杀健康检查路径不能乱配还有一个常见问题是探针路径配错导致 Pod 反复重启。我一开始用/作为健康检查路径理论上 Gitea 返回 200 就能通过但因为 Gitea 登录页面是重定向到/user/login而探针跟随了重定向逻辑有时会判断为失败尤其当首次迁移数据库期间页面响应特别慢探针就会误判挂掉。排查方式很直接kubectl describe pod gitea-0 -n gitea在 Events 区块能看到 Liveness probe failed 这类字样配合探针的失败次数统计就能判断是不是探针设置问题。正确的探针路径是/api/v1/health注意这个接口在 Gitea 1.18 及以上版本才提供老版本可能返回 404需要自己确认。上面的 YAML 直接用这个接口实测效果很稳。Gitea 官方文档也推荐了这个健康检查端点它能同时反映 Web 进程和数据库连接的健康状态。6. 部署完成后的自检清单与日常维护建议6.1 部署完成后怎么确认一切正常部署完 Gitea 以后我不建议马上就开始建仓库。先过一遍自检清单确定这套系统不是看起来在运行实际一用就挂。按下面这个顺序走一遍Pod 状态kubectl get pod -n gitea确认 READY 为 1/1STATUS 为 Running。日志干净kubectl logs gitea-0 -n gitea | tail -100确认没有 ERROR、FATAL 级别的日志。网页访问浏览器打开 https://git.example.com能看到 Gitea 的首页且能登录。提交代码测试在页面上新建一个测试仓库本机用 SSH clone 和 HTTP clone 各自试一次提交、推送、拉取。SSH 通道再测确认 clone 地址里的域名端口是期望值不要等到同事来问为什么 clone 地址端口老是变。扩容测试临时把副本数调整到 2看是否会出现问题。虽然生产环境跑单副本但至少要知道多副本在当前配置下能不能起得来。测试完缩回 1。持久化验证kubectl delete pod gitea-0 -n gitea等 StatefulSet 自动重建重新登录页面确认仓库数据、用户数据都还在。第七条非常重要。很多人部署完从来没验证过持久化的真实性直到某天节点宕机Pod 被调度到新节点才发现数据丢了或者配置没同步。6.2 日常维护的几个习惯Gitea 的运维量不大但有几个习惯我一直保持定期备份数据库。PostgreSQL 的备份用 pg_dump 就够了定时任务在集群外做一个 crontab每天凌晨 dump 一份保留 7 天。仓库数据备份则是把 NFS 存储里的 git 仓库目录打包到别的存储上频率可以低一点比如每周一次。仓库数据是 git 分布式的团队成员本地都有完整副本所以仓库备份的重点不是代码本身而是 issues、PR、里程碑这些只存在于服务器上的元数据这些都在数据库里。关注镜像安全更新。Gitea 项目迭代速度挺快的尤其涉及 Git 命令执行这类高风险逻辑。建议每隔一两个月看一下官方 Release 页面如果有重要更新可以用 kubectl set image 在线升级镜像 tagStatefulSet 会自动做滚动更新。记录 Ingress 注解变更。每次改 Ingress 的 annotations 之前先确认改的是不是当前生效的配置。我在生产环境做过一次失误操作为了给另一个服务加白名单把 annotations 里的一段配置覆盖到了 Gitea 上导致 Gitea 入口瞬间 403。这个问题的预防办法是把所有 Ingress 的注解变更都通过代码仓库管理不要直接在集群里 vim 改 YAML。6.3 日志收集与监控建议Gitea 的日志默认输出到容器 stdout这部分 K8s 已经帮你接了 kubelet 的日志体系用 kubectl logs 就能看到。如果想要集中式日志可以给 Gitea 的 Namespace 打标签让采集器自动发现。日志内容里最有价值的是 SSH 登录日志和 Git 操作日志排查用户问题的时候基本上都靠它们。监控方面Gitea 自身没有暴露 Prometheus metrics 端点官方还在开发中但可以通过黑盒监控的方式检测页面是否正常一个简单的 Deployment定期请求 /api/v1/health如果连续三次失败就告警。这比我见过的大多数自建 Git 服务要靠谱了。白盒监控的缺失不影响基本可用性因为 Gitea 的特性决定了它常用常挂的几率不高黑盒足够兜底。7. 这套部署方案后面还能怎么扩展7.1 接入现有的 SSO / OAuthGitea 支持通过 OAuth2 接入现有的身份认证系统。部署完以后可以到管理后台的认证源设置里添加一个 OAuth2 应用填入公司内部的人认证服务的 Client ID 和 Secret这样团队人员可以直接用统一账号登录 Gitea不用单独维护一套密码。账户同步方面Gitea 支持自动创建账户用户第一次登录时会自动映射到已有的 Git 用户。这一步配置不难但能省掉新员工入职时手工创建账号的流程。7.2 启用 Actions 做轻量 CI如果你不想引额外的 CI/CD 系统Gitea Actions 是一个成本很低的备选方案。因为 Gitea 的 Runner 可以部署为集群内的一个 Deployment构建任务作为 K8s Job 在集群里跑资源调度由 K8s 管不需要额外维护构建机。首次配置 Runner 需要把 token 填到 Runner 的启动参数里之后的 CI 工作流文件直接放到仓库的 .gitea/workflows 目录下。这个扩展能让 Gitea 从单纯的代码托管变成小团队的轻量 DevOps 平台配合 Kubernetes 本身的能力日常的自动化构建、镜像打包、部署触发都能在里面完成。7.3 仓库镜像与灾备如果你的团队同时使用多个 Git 平台可以用 Gitea 内置的镜像仓库功能。在仓库设置里把外部仓库地址填进去Gitea 会定时从远程拉取更新起到异地备份、统一入口的效果。这个功能部署完不一定要立刻启用但知道有这个选项后面业务上需要的时候能少走不少弯路。我个人现在的方案是把 Gitea 的仓库数据目录单独用一个 NFS export 管理数据库用 PostgreSQL容器本身随时可以销毁重建。这样即使整个集群出了问题只要存储和数据库还在Gitea 就能在半小时内恢复。这也是 K8s 里跑有状态应用的通用思路把状态从容器里剥离出去容器只是无状态的执行者。这可能是这套部署方案里最有价值的一句话总结。