把程式碼放在別人的伺服器上總有些不安,尤其是內部專案、私人筆記、還沒公開的作品。自己架一台 Git 服務就能把這份掌控權拿回來,而在眾多選項裡,Gitea 大概是最省心的一個——它是用 Go 寫成的單一執行檔,記憶體佔用低、安裝簡單,介面又跟大家熟悉的 GitHub 很接近,一台便宜的 VPS 甚至家裡的小主機就能跑得很順。
這篇會用 Docker 的 rootless 版本把 Gitea 從零架起來,並且把重點放在「安全」這件事上。畢竟自架服務一旦對外,等於自己要扛起原本平台幫忙擋掉的那些風險。我們會一步步處理容器權限、資料庫、管理員建立,以及一組真正該打開的安全設定——包含很多人架完才想到要關的「使用者自行註冊」。
為什麼選 rootless 版
Gitea 官方的 Docker image 有兩種:一般版與 rootless 版。一般版的容器以 root 啟動,再在內部降權;rootless 版則是整個容器內的進程從頭到尾都以非 root 的使用者(UID 1000)執行。差別聽起來細微,但在安全上很關鍵:萬一容器內的服務被攻破,rootless 版能拿到的權限從一開始就被綁死在一個普通使用者身上,要再往外提權到主機 root 的路徑更長、更難。對一台會對外開放的 Git 服務來說,這種「預設就把權限關到最小」的設計值得優先採用。
rootless 版跟一般版有幾個實際差異要先記住,後面設定會用到:
| 項目 | rootless 版 | 一般版 |
|---|---|---|
| image 標籤 | gitea/gitea:1.26-rootless | gitea/gitea:1.26 |
| 容器內身分 | 非 root(UID 1000) | root 啟動後降權 |
| 資料目錄 | /var/lib/gitea 與 /etc/gitea 分開 | 單一 /data |
| SSH port | 內建 SSH server,2222 | 22(可走主機 OpenSSH) |
SSH 為什麼從 22 變成 2222?因為在 Linux 上,1024 以下的 port 屬於特權範圍,只有 root 能綁定。rootless 版的進程是普通使用者,綁不了 22,所以官方 image 內建了一台監聽在 2222 的 SSH server 來處理 git 操作。這也表示 rootless 版不走「主機 OpenSSH 轉發」那套進階做法,直接用內建的就好,反而單純。
事前準備
需要的東西不多:一台裝好 Docker 與 Docker Compose 的 Linux 主機就夠了。先建立放資料的目錄,這裡用一個專屬資料夾把所有東西集中管理:
# 建立專案目錄與資料夾
mkdir -p ~/gitea/data ~/gitea/config ~/gitea/postgres
cd ~/gitea
# rootless 容器內是 UID 1000,host 上的資料目錄要把擁有者改成 1000
# 否則容器啟動時沒權限寫入會直接失敗
sudo chown -R 1000:1000 data config
這個 chown 是 rootless 版最容易踩的坑:容器內的使用者是 UID 1000,如果 host 上掛進去的目錄擁有者不是 1000,容器一啟動就會因為寫不進資料而退出。先把 data 與 config 交給 1000,後面就順了。
用 docker-compose 啟動 Gitea 與 PostgreSQL
資料庫的部分,官方支援 SQLite、MySQL/MariaDB 與 PostgreSQL。SQLite 不需要額外服務、單一檔案就能跑,適合個人或測試;但只要預期會有多人協作、或單純想跑得久一點,官方建議改用 PostgreSQL 這類正式的資料庫——SQLite 在高並發寫入時會卡在鎖上。所以這裡直接用 PostgreSQL,跟 Gitea 放在同一份 compose 裡。
在 ~/gitea/ 下建立 docker-compose.yml:
services:
server:
image: gitea/gitea:1.26-rootless # 注意是 -rootless 標籤
container_name: gitea
restart: always
environment:
# ---- 資料庫連線 ----
- GITEA__database__DB_TYPE=postgres
- GITEA__database__HOST=db:5432 # db 是下面 service 的名字
- GITEA__database__NAME=gitea
- GITEA__database__USER=gitea
- GITEA__database__PASSWD=請改成一組強密碼
# ---- 安全設定(後面章節逐項說明)----
- GITEA__security__INSTALL_LOCK=true # 鎖住安裝精靈頁
- GITEA__service__DISABLE_REGISTRATION=true # 關閉使用者自行註冊
- GITEA__service__REQUIRE_SIGNIN_VIEW=true # 要登入才能瀏覽
- GITEA__service__DEFAULT_KEEP_EMAIL_PRIVATE=true # 預設隱藏 email
- GITEA__service__DEFAULT_ALLOW_CREATE_ORGANIZATION=false # 一般使用者不能建組織
- GITEA__security__TWO_FACTOR_AUTH=enforced # 全站強制兩階段驗證
networks:
- gitea
volumes:
- ./data:/var/lib/gitea # rootless 的資料路徑
- ./config:/etc/gitea # rootless 的設定路徑
- /etc/timezone:/etc/timezone:ro
- /etc/localtime:/etc/localtime:ro
ports:
- "3000:3000" # 網頁介面
- "2222:2222" # 內建 SSH server
depends_on:
- db
db:
image: postgres:16
restart: always
environment:
- POSTGRES_USER=gitea
- POSTGRES_PASSWORD=請改成一組強密碼 # 要跟上面的 PASSWD 一致
- POSTGRES_DB=gitea
networks:
- gitea
volumes:
- ./postgres:/var/lib/postgresql/data
networks:
gitea:
external: false
這份設定有幾個地方值得拆開講。最明顯的是 所有 Gitea 設定都用環境變數帶進去,格式是 GITEA__區段__鍵(兩個底線分隔)。Gitea 會把這些變數寫進容器內的 app.ini,等於不用手動編輯設定檔,整份部署都收斂在這一個 compose 檔裡,日後要遷移或重建一目了然。如果偏好用 MySQL,只要把 DB_TYPE 改成 mysql、HOST 指向 MySQL 容器,再把 db service 換成 mysql image 即可,其餘結構一樣;什麼資料庫都不設的話,Gitea 會退回預設的 SQLite。
兩個 service 靠同一個 gitea 網路溝通,Gitea 用 service 名稱 db 就能連到資料庫,PostgreSQL 的 port 完全不對外開放,只在 compose 內網流通——這本身也是一種安全收斂:資料庫沒有任何理由暴露給外面。整體架構長這樣:

準備好就啟動:
# 在 ~/gitea/ 下,背景啟動
docker compose up -d
# 看一下兩個容器有沒有正常跑起來
docker compose ps
# 需要追問題時看 log
docker compose logs -f server
初始化:建立第一個管理員
第一個管理員怎麼建,其實有兩條路,差別在於初始化那段時間服務對不對外開放。如果能確保初始階段外界完全連不到,用 Gitea 內建的網頁安裝精靈最直覺;如果服務一啟動就可能被外界連到,就改走「鎖死安裝頁+命令列建管理員」這條更保險的路。前面 compose 範例裡的 INSTALL_LOCK=true 對應的是後者,兩個方案擇一即可。
方法一:網頁安裝精靈
如果能保證初始化期間外界連不到 3000 port——例如機器還沒對外開放、防火牆只放行自己的 IP,或透過 SSH tunnel 連進去操作——那直接走網頁安裝精靈最省事。做法是先把 compose 範例裡的 GITEA__security__INSTALL_LOCK=true 這行拿掉(或改成 false)再啟動,然後用瀏覽器開 http://主機:3000,精靈會引導把設定填完,過程中直接建立管理員帳號、並取消勾選「允許使用者自由註冊」。精靈一旦完成,Gitea 會自動把 INSTALL_LOCK 寫成 true 鎖住安裝頁。確認管理員建好、安裝頁也鎖上之後,再把服務對外開放。這是官方建議的標準初始化路徑。
方法二:CLI 建管理員
如果沒辦法保證初始階段是隔離的——例如機器直接綁公網 IP、DNS 也指好了,一 up 起來就可能被掃到——那網頁安裝精靈反而是個風險:在它被走完之前,任何能連到網站的人都能打開它、把自己設成管理員,這段裸奔空窗很危險。這種情況就保留 compose 裡的 INSTALL_LOCK=true,讓安裝頁從第一秒就鎖死、資料庫設定由環境變數提供,管理員改用命令列建立,從頭到尾沒有空窗:
# 進到容器內用 gitea CLI 建立管理員帳號
docker compose exec server gitea admin user create \
--username admin \
--email [email protected] \
--password '請改成一組強密碼' \
--admin \
--must-change-password=false
--admin 給這個帳號管理員權限。這條路刻意避開了「讓第一個註冊的人自動成為管理員」這種賭運氣的做法——用 CLI 明確建立、再搭配下一節關閉註冊,順序清楚也沒有競態空窗。
不管走哪條路,管理員建好後用瀏覽器開 http://主機:3000 就能用這組帳號登入了。
真正該打開的安全設定
前面 compose 裡已經塞了好幾條安全設定,這節把它們逐一講清楚為什麼要設,也補上幾個視情況可加的選項。這些設定多數落在 [service] 與 [security] 兩個區段,對應的環境變數就是 GITEA__service__ 與 GITEA__security__ 開頭。
關閉使用者自行註冊
這是自架 Gitea 最該優先處理的一條。預設情況下任何人都能在登入頁自己註冊帳號,對一台私人或團隊內部的服務來說,這等於把大門敞開。設定 GITEA__service__DISABLE_REGISTRATION=true 之後,註冊入口就消失了,往後要加人只能由管理員在後台手動建立,或之後接上 OAuth/LDAP 等外部來源。對私有服務而言,這條幾乎是必設。
要求登入才能瀏覽
REQUIRE_SIGNIN_VIEW=true 會讓所有頁面——包含 repository 列表、使用者頁、程式碼內容——都必須登入才看得到。如果這台 Gitea 純粹是私人或公司內部使用,沒有任何內容想公開,打開它能擋掉所有匿名瀏覽,連 repo 名稱都不外洩。反過來,如果有些 repo 想開放給匿名訪客看,就別設這條,改用個別 repo 的公開/私有權限去控制。
別把 git hooks 的保護關掉
這條跟其他不太一樣——它預設就是安全的,重點是不要去把它放開。[security] 區段的 DISABLE_GIT_HOOKS 預設值是 true,意思是禁止使用者自訂 git hooks。為什麼重要?因為 git hooks 本質上是會在伺服器上執行的 shell 指令,一旦允許有權限的使用者自己寫 hook,等於開了一條在主機上跑任意程式碼的路。網路上偶爾會看到「要記得把 DISABLE_GIT_HOOKS 設成 true」的說法,其實它本來就是 true,真正要做的是別在設定裡把它改成 false,保持預設即可。
強制兩階段驗證與隱藏個資
把 [security] 的 TWO_FACTOR_AUTH 設成字串 enforced,全站使用者下次登入就會被要求綁定兩階段驗證(2FA),帳號密碼外洩也多一道防線。另外 DEFAULT_KEEP_EMAIL_PRIVATE=true 會讓新帳號預設隱藏 email,避免 commit 紀錄把每個人的信箱攤在外面。這兩條成本很低,卻能明顯收斂掉帳號與個資的暴露面。
收斂其他探索面
剩下幾條視需求斟酌。DEFAULT_ALLOW_CREATE_ORGANIZATION=false 讓一般使用者不能隨意建立組織,把組織管理權收回給管理員。如果之後決定開放註冊(例如改成只接受 OAuth 外部帳號),那 ENABLE_CAPTCHA=true 可以擋掉一部分機器人。下面這張表把這節提到的設定整理在一起,方便對照:
| 設定(環境變數) | 作用 | 建議值 |
|---|---|---|
GITEA__security__INSTALL_LOCK | 鎖住安裝精靈頁 | true |
GITEA__service__DISABLE_REGISTRATION | 關閉自行註冊 | true |
GITEA__service__REQUIRE_SIGNIN_VIEW | 要登入才能瀏覽 | 私有服務設 true |
GITEA__security__DISABLE_GIT_HOOKS | 禁自訂 git hooks | 保持預設 true |
GITEA__security__TWO_FACTOR_AUTH | 強制 2FA | enforced |
GITEA__service__DEFAULT_KEEP_EMAIL_PRIVATE | 預設隱藏 email | true |
GITEA__service__DEFAULT_ALLOW_CREATE_ORGANIZATION | 一般使用者建組織 | false |
關於 SECRET_KEY 與 INTERNAL_TOKEN 這兩把內部金鑰,單機這種「設定寫回 config volume」的情境不用特別操心——Gitea 第一次啟動會自動產生並寫進 app.ini。只有在需要跨多個實例共用、或完全用環境變數驅動而不落地設定檔時,才需要自己用 gitea generate secret 產生並固定下來。
對外發布與 HTTPS
到這裡 Gitea 已經在 3000 port 跑起來,安全設定也都到位了。但目前它只在 HTTP 上、而且直接把 port 暴露在主機上。要正式對外,標準做法是讓 Gitea 留在內網跑 HTTP,前面擺一台反向代理負責 HTTPS 與憑證,TLS 在反向代理那層就終結,Gitea 本身完全不用碰憑證。架構會變成這樣:

反向代理與憑證的設定細節不在這篇的範圍,但流程跟一般 Web 服務沒兩樣,之前的兩篇文章可以直接套用:架設與設定 Nginx 反向代理的部分,參考 用 Nginx 反向代理把服務跑在 systemd 上 那篇的 Nginx 段落;HTTPS 憑證的自動申請與續期,則看 用 Let’s Encrypt 自動續期 HTTPS 憑證 。把 Gitea 當成被反向代理的後端服務(指向 http://localhost:3000)即可。
接上反向代理之後,Gitea 這邊有幾個 [server] 設定要對齊,否則它產生的 clone 連結、webhook 網址會用錯位址:
| 設定(環境變數) | 作用 | 範例 |
|---|---|---|
GITEA__server__DOMAIN | 對外網域 | git.example.com |
GITEA__server__ROOT_URL | 對外完整網址 | https://git.example.com/ |
GITEA__server__SSH_DOMAIN | clone 連結顯示的網域 | git.example.com |
GITEA__server__SSH_PORT | clone 連結顯示的 SSH port | 2222 |
其中 ROOT_URL 最關鍵,設成對外的 HTTPS 網址,Gitea 產生的所有連結才會正確。SSH_DOMAIN 與 SSH_PORT 則是控制網頁上那串「複製我去 clone」的 SSH 連結內容,要對齊使用者實際連得到的入口——我們對外開的是 2222,所以 SSH_PORT 就填 2222。
小結
整理一下這台 Gitea 的安全骨架:用 rootless 版讓容器以非 root 執行、把提權路徑縮到最短;資料庫藏在內網不對外;安裝頁直接鎖死、改用 CLI 建管理員,沒有裸奔空窗;自行註冊關閉、私有瀏覽開啟、git hooks 保持預設保護、2FA 全站強制。每一條都是同一個原則的延伸——預設關到最小,需要才打開。
剩下的就是把它接上反向代理與 HTTPS 正式對外,這部分沿用既有的 Nginx 與 Let’s Encrypt 流程即可。架好之後,這台完全屬於自己的 Git 服務,從程式碼到帳號權限都握在手裡,心裡踏實不少。