用Docker快速搭建Gitea私有代码仓库全流程指南
老实说我一开始对自建代码仓库这件事是有点抗拒的——GitHub 用得好好的为什么要自己折腾一个但当你手上同时出现了几个不想公开、又不想被平台收费绑住的小项目时答案自然就出来了。我腾了一台轻量服务器用 Docker 安装了 Gitea从拉镜像到创建出第一个仓库整个过程大约十几分钟之后再没为代码放哪发过愁。这篇东西就是那次安装的完整记录环境准备、docker-compose 配置、初始化向导的每个选项、以及后来升级备份时踩过的坑目标只有一个让你照着做也能快速得到一个可靠好用的私有代码仓库。尤其适合嫌 GitLab 太重、又觉得裸 Git 仓库没有界面和权限管理的个人开发者或小团队。1. 为什么我最终选了 Gitea 而不是更重的方案1.1 自建 Git 服务的几条路线装之前先把选择看清楚。自建代码仓库常见就这么几条路各有利弊但资源占用差异很大方案资源占用界面与功能适合场景GitHub / Git 托管平台无服务端全功能愿意公开或付费的项目GitLab CE高官方建议 4G 内存起步全功能含 CI、MR、容器镜像仓库二十人以上团队Gitea极低512MB 内存可跑Issues、PR、组织管理、Actions偏弱个人到十几人的团队Gogs低但功能少基础仓库管理只想要最简仓库裸 Git SSH几乎没有无界面纯命令行重度用户GitLab 我其实认真试过功能确实全但在小机器上跑得很吃力。一次内存不足直接把服务干挂连仓库页面都打不开体验非常差。Gitea 是用 Go 写的单一进程启动快、静态资源体积小我自己那台 2 核 4G 机器上Gitea 加上 MariaDB 数据库两个容器日常加起来才占 600MB 左右内存。这个量级在我做取舍时几乎是决定性的省下来的资源拿去跑 CI、跑备份都更宽裕。如果你只是两三个人协作其实 SQLite 模式都够用。但考虑到以后可能会给朋友开账号、并发读写会慢慢变多我最后还是选择挂一个独立数据库。这样的架构更接近正式环境的形态后面真要迁移、要查数据心里也更有底。1.2 Docker 化部署的收益和成本选择 Docker 并不是因为它流行而是它把维护成本压到了最低。传统装 Gitea 要么下载二进制、要么用包管理器升级时要停进程、替换文件、处理配置备份Docker 下只需要换镜像 tag重新拉起容器就行。回滚也简单把镜像 tag 指回上一个版本再 create 一下即可。另一个实打实的收益是迁移成本。Gitea 的代码仓库、配置、附件基本都集中在 /data 目录下数据库放在另一个数据卷里。哪天想换机器把这两个目录打包搬过去在同样一份 docker-compose.yml 里重新启动就行几乎是无感迁移。当然有得必有失。容器默认以 UID 1000 的 git 用户运行宿主机目录属主不对的话一启动就报 Permission denied端口如果没规划好克隆地址会把 22 或 3000 显示错后面我会专门讲。这些问题不致命但都是没装过的人容易一头撞上去的。2. 安装前的准备一台能跑 Docker 的机器和一套清晰的目录规划2.1 硬件、系统和磁盘预期先说我用的环境Ubuntu 22.04 LTS2 核 CPU4GB 内存系统盘 40G。这个配置对 Gitea 来说相当宽裕。如果你只是个人使用1 核 1G 的机器完全够即便 512MB 内存跑 Docker 加 Gitea 加 SQLite 也能转起来只是首次启动和打包大仓库时会明显慢一点需要有点耐心。磁盘方面要特别提一句。Git 仓库是增量增长的很多人低估了 .git 目录的膨胀速度。我建议给 /opt/gitea 所在分区至少留 20G 以上空间如果团队里有人习惯频繁推大文件还要考虑开启 Git LFS 后单独预留存储空间。系统方面 Debian/Ubuntu 系最顺手CentOS 和 Rocky 也完全可以命令大同小异。2.2 Docker 环境安装与验证我是在 Ubuntu 上装的 Docker用官方源一步步来命令如下sudo apt-get update sudo apt-get install -y ca-certificates curl gnupg curl -fsSL https://download.docker.com/linux/ubuntu/gpg | sudo gpg --dearmor -o /usr/share/keyrings/docker-archive-keyring.gpg echo deb [arch$(dpkg --print-architecture) signed-by/usr/share/keyrings/docker-archive-keyring.gpg] https://download.docker.com/linux/ubuntu $(lsb_release -cs) stable | sudo tee /etc/apt/sources.list.d/docker.list /dev/null sudo apt-get update sudo apt-get install -y docker-ce docker-ce-cli containerd.io docker-compose-plugin装完先别急着往下走确认两个东西docker --version能打印版本号docker compose version能正常响应。这里我要多说一句现在推荐用带空格的docker compose子命令它对应的就是 docker-compose-plugin 这个插件老式的docker-compose带横线是独立 Python 工具也能用但命令风格不一样网上很多教程混着写容易把人绕晕。另外强烈建议检查一下系统时间。容器内的 Git 提交时间、备份文件名、日志时间戳全都依赖系统时间时区错乱的话后面排查问题会非常痛苦。最简单的做法是让宿主机时间正确并在 compose 文件里把 timezone 和 localtime 挂进去我的配置里会带上。2.3 端口、目录和域名这三件事先想清楚端口规划是整个部署里最容易被忽略、也最容易返工的一步。Gitea 需要两个入口Web 端口和 SSH 端口。Web 默认 3000容器内固定你可以在宿主机上随便映射成别的比如 8080 或 80SSH 端口则有讲究因为 Gitea 容器内部用 22 作为 SSH 服务端口而宿主机 22 通常已经被系统的 sshd 占用。有两条路线可以选一是把系统 sshd 改到 2222把宿主机 22 让出来映射给 Gitea 容器二是保留系统 sshd 不动把宿主机某个空闲端口比如 10022 映射到容器 22。我选了第二条因为不用动系统 SSH风险最小。但注意一个关键点后面初始化向导里填的 SSH 端口必须是映射后的 10022而不是容器内部的 22——这个数字会直接出现在仓库的克隆地址里填错的话所有 SSH 方式的克隆都连不上。目录建议统一放在 /opt/gitea 下面分两块Gitea 数据挂到 ./gitea 对应容器里的 /data数据库数据挂到 ./db 对应容器里的 /var/lib/mysql。这样备份时看目录就一目了然升级和迁移也知道到底该带走哪些东西。3. 用 docker-compose 把 Gitea 和数据库一起拉起来3.1 先决定数据库SQLite 还是独立数据库我把官方文档和社区反馈都翻过一遍结论很明确个人用、开发环境、不超过 10 人的团队SQLite 完全够部署还简单——一个 Gitea 容器加一个数据卷就完事连数据库环境变量都不用配。但如果你是给多个项目、多个人长期用建议加一个 MariaDB 容器。原因有两个一是并发多的时候SQLite 的写锁会成为瓶颈几个仓库同时被 push 时会有明显卡顿二是独立数据库让备份、查询、迁移都更清晰万一 Gitea 容器坏了业务数据还完整地躺在数据库里。下面这份 compose 配置就是 数据库 Gitea 两个容器的完整形态如果你想偷懒用 SQLite删掉 db 服务和相关环境变量即可。3.2 完整的 docker-compose.yml 配置version: 3.8 services: db: image: mariadb:11 restart: always environment: MYSQL_ROOT_PASSWORD: root_pass_please_change MYSQL_DATABASE: gitea MYSQL_USER: gitea MYSQL_PASSWORD: gitea_pass_please_change volumes: - ./db:/var/lib/mysql networks: - gitea_net gitea: image: gitea/gitea:latest restart: always depends_on: - db environment: GITEA__database__DB_TYPE: mysql GITEA__database__HOST: db:3306 GITEA__database__NAME: gitea GITEA__database__USER: gitea GITEA__database__PASSWD: gitea_pass_please_change GITEA__server__DOMAIN: git.example.com GITEA__server__SSH_PORT: 10022 GITEA__server__ROOT_URL: https://git.example.com GITEA__server__DISABLE_REGISTRATION: true GITEA__server__LFS_START_SERVER: true ports: - 3000:3000 - 10022:22 volumes: - ./gitea:/data - /etc/timezone:/etc/timezone:ro - /etc/localtime:/etc/localtime:ro networks: - gitea_net networks: gitea_net:需要注意如果你是临时测试、手上没有域名把 GITEA__server__DOMAIN、ROOT_URL 改成http://你的服务器IP:3000再启动不然克隆地址会生成一串指向不存在域名的链接。域名这块我后面第 4 章会展开讲。3.3 关键环境变量逐行解读Gitea 官方支持用环境变量覆盖 app.ini 配置规则是GITEA__加 section 名、再加__加 key 名。比如GITEA__server__DOMAIN对应的是 app.ini 里 [server] 段的 DOMAINGITEA__database__PASSWD对应的就是数据库密码。这个机制非常实用意味着你不需要钻进容器改文件改完环境变量、重启容器就生效。几个核心变量整理成表格环境变量作用GITEA__database__DB_TYPE数据库类型用 MariaDB 就写 mysqlGITEA__database__HOST数据库地址compose 网络里用服务名 db:3306GITEA__server__DOMAIN对外展示的域名影响克隆地址GITEA__server__SSH_PORT对外展示的 SSH 端口必须等于宿主机映射端口GITEA__server__ROOT_URL完整访问地址决定 HTTPS 克隆前缀GITEA__server__DISABLE_REGISTRATION设为 true 后禁止开放注册GITEA__server__LFS_START_SERVER开启 Git LFS 大文件支持卷的挂载要理解透彻./gitea:/data挂接的是 Gitea 本体数据里面会生成 gitea/conf/app.ini、gitea/repositories、gitea/lfs 这些目录./db:/var/lib/mysql挂接的是 MariaDB 数据库文件。至于 /etc/timezone 和 /etc/localtime是只读挂载解决容器和宿主机时区不一致的问题。3.4 启动与验证配置写好后按顺序执行cd /opt/gitea docker compose up -d docker compose ps docker compose logs -f gitea看到日志输出正常、无报错后浏览器访问http://服务器IP:3000如果出现安装引导页面就说明服务已经跑起来了。页面打不开时先看三步一是docker compose ps里容器状态是否为 Up二是docker compose logs里有没有数据库连接报错三是确认防火墙和云安全组放行了 3000 端口。我帮别人排查时十次里有六次都倒在最后这个环节。4. 初始化向导里的每一个选项都意味着什么4.1 数据库连接信息怎么填最稳第一次打开 /install 页面会看到一个数据库设置区。因为我们用的是 MariaDB 容器数据库类型要选 MySQLMariaDB 完全兼容 MySQL 协议。这里有几个关键点填错一个就白折腾主机地址填db:3306不要填 localhost 或 127.0.0.1。因为地址是在 compose 创建的网络内部解析的服务名就叫 db不是本机回环地址。数据库名、用户名、密码必须是 docker-compose.yml 里 MYSQL_DATABASE、MYSQL_USER、MYSQL_PASSWORD 写好的那组别自己临时起名。如果报dial tcp: lookup db on 127.0.0.11:53: no such host说明 Gitea 容器和数据库容器不在同一个 compose 网络里或者服务名拼错了优先检查这两点。页面下方还有一个“常规设置”区仓库根目录和 LFS 根目录保持容器内默认的 /data/git/repositories 和 /data/git/lfs 即可。既然已经通过卷把 /data 挂出来了这两个路径不需要改改了反而容易把数据写到容器可写层里容器重建后全丢。4.2 站点 URL、SSH 端口与克隆地址之间的关系很多人在这一步栽跟头。ROOT_URL 会直接参与仓库克隆地址的拼接如果填https://git.example.com那么 HTTPS 克隆地址就是https://git.example.com/owner/repo.git如果是 IP 加端口比如http://192.168.1.10:3000那 HTTPS 克隆也会要求走 3000 端口。ROOT_URL 填错的话Web 界面里复制的克隆地址就是错的非常迷惑。SSH 端口同理。因为采用宿主机 10022 映射容器 22 的方案这里必须填 10022。保存后仓库页面的 SSH 克隆地址会显示成gitgit.example.com:10022/owner/repo.git。很多人部署时直接留默认 22结果 SSH 克隆永远卡在连接超时——因为宿主机 22 是系统 sshd 在监听根本没把连接转给 Gitea。这个坑我帮朋友排过不止一次。4.3 管理员账号与注册开关安装页面最下方是管理员账号设置。绝大多数自建场景都不需要让陌生人注册所以我在 compose 里直接加了GITEA__server__DISABLE_REGISTRATIONtrue安装完成后开放注册接口会被禁用。以后要给同事开账号用管理员后台创建即可全程可控。管理员密码建议选足够长的随机串最好丢进密码管理工具里。Gitea 的管理员权限非常大后台能管理所有仓库、修改所有用户公网裸奔状态下密码太弱就是在给自己挖坑。4.4 用 nginx 把 443 端口转给 Gitea如果只是自己内网访问IP:3000 直接用没问题。但只要给团队成员用、或者要在公网访问我强烈建议在前面加一层 nginx让浏览器统一走 80/443 端口同时把 ROOT_URL 改成 https 域名。否则会出现很尴尬的场景——浏览器地址栏是 HTTPS仓库克隆地址却指向 3000 端口证书和端口两头对不上。下面是一份最简 nginx 配置证书可以用免费方式申请server { listen 80; server_name git.example.com; return 301 https://$host$request_uri; } server { listen 443 ssl; server_name git.example.com; ssl_certificate /etc/letsencrypt/live/git.example.com/fullchain.pem; ssl_certificate_key /etc/letsencrypt/live/git.example.com/privkey.pem; location / { proxy_pass http://127.0.0.1:3000; proxy_set_header Host $host; proxy_set_header X-Real-IP $remote_addr; proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; proxy_set_header X-Forwarded-Proto $scheme; } }配上之后把 Gitea 的 ROOT_URL 改成https://git.example.com重启容器HTTPS 克隆地址就统一了SSH 仍然走 10022两条通道互不干扰。5. 升级、备份还有几个让我印象深刻的坑5.1 先搞清楚数据到底散在哪里不少新手以为备份就是把 docker-compose.yml 存一份这是误解。 Gitea 的 /data 目录下既有代码仓库也有 app.ini 配置、LFS 文件、附件和日志如果加了独立数据库用户表、仓库元数据、Issue 记录这些全在 db 卷里。换句话说任何备份方案都要同时覆盖 gitea 目录和 db 目录缺一个都不是完整备份。因为 app.ini 也在 /data 里所以备份了 gitea 目录也就备份了全部配置。这算是容器化部署顺手带来的好处整个服务状态几乎就是两个目录往 tar 包里一塞就是一份快照。5.2 两条备份线路整卷冷备与逻辑备份最稳妥的是把 /opt/gitea 整个目录打包做冷备但这需要在停机状态下做不适合长期跑的服务。日常建议两条腿走路Gitea 自带的gitea dump命令可以在容器运行状态打出一个包含仓库、配置、LFS 和 SQLite 数据的压缩包执行方式docker compose exec gitea bash -c cd /tmp gitea dump --config /data/gitea/conf/app.ini --type zip docker compose cp gitea:/tmp/gitea-dump-*.zip /opt/backup/如果你用的是独立数据库还需要对数据库单独做逻辑备份docker compose exec db sh -c exec mysqldump -ugitea -p你需要改的密码 gitea /opt/backup/gitea-db-$(date %F).sql这两份产物放到不同磁盘再写一个 cron 定时任务每天凌晨执行一次、保留最近七份基本就能覆盖绝大多数故障场景。注意 mysqldump 的密码如果包含特殊字符要么转义要么使用环境变量引用别直接把明文密码留在 shell 历史里。5.3 升级的正确姿势升级 Gitea 是 Docker 部署最舒服的地方命令就三条docker compose pull gitea docker compose up -d gitea docker compose logs -f gitea但我的习惯是先备份、再读版本更新说明、最后才动手。Gitea 发布新版本时升级过程会触发数据库迁移绝大多数迁移脚本是向后兼容的出问题概率不大。但万一升级后启动失败把 compose 文件里的镜像 tag 改回旧版本docker compose up -d重新拉起就能回滚数据完全不受影响。这也是用镜像 tag 管理版本的核心价值。正式环境我建议不要一直追latest而是固定到具体的版本 tag比如gitea/gitea:1.23.0。这样做升级动作完全由自己掌控不会因为某天 pull 到一个大版本而被动变更。5.4 三个最容易翻车的细节按我接触到的实际反馈把高频问题列出来遇到时直接对号入座SSH 端口和实际映射不一致。不管网页里填 10022 还是 22关键看宿主机到底把哪个端口映射到了容器 22。填写的值要等于最终用户连接时使用的端口不要想当然照抄教程因为每台机器的映射策略可能不同。卷权限不足。容器内 git 用户 UID 是 1000如果宿主机上的 gitea 目录属主不是 1000启动时就会报 permission denied。解决方式是chown -R 1000:1000 /opt/gitea/gitea /opt/gitea/db。如果你的宿主机上恰好存在 UID 1000 的普通用户权限天然匹配也就不会有这个问题。防火墙没放行。装完服务发现页面打不开十有八九是云厂商安全组或本机防火墙没放行 3000 和 10022。排查顺序是先在服务器上curl http://127.0.0.1:3000能通说明服务正常再查外网链路通了外网还连不上优先怀疑安全组配置。最后说个我的工作习惯每次升级前把当前容器镜像 tag 记到备忘录里备份文件丢到另一块磁盘升级完先关掉开放注册再做一次 SSH 和 HTTPS 双路克隆测试确认两条通道都正常后再通知团队。这套动作看着繁琐实际上跑熟之后五分钟就能完成但它帮我避免了很多次“怎么突然克隆不了”的打扰电话。Gitea 本身已经很稳真正脆弱的环节往往是我们对自身环境的掌握程度把备份和回滚这条路走顺了整套服务就能安安静静跑上很久。