~/yazilar/gitea-ile-kendi-git-sunucunu-kurmak
Gitea ile kurum içinde kendi Git sunucunu kurmak
içindekiler
Kodun bir kısmı dışarıda durmamalı. Ağ cihazlarının config yedekleri, Ansible envanterleri, iç servislerin deploy script'leri, içinde IP planı geçen her şey. Bunları GitHub'daki private bir repoya koymak çoğu ekip için sorun değil, ama bazı kurumlarda politika gereği olmuyor, bazılarında da sunucuların internete çıkışı zaten kapalı. O durumda ihtiyaç basit: içeride çalışan, web arayüzü olan, SSH ile clone edilebilen, kullanıcıları Active Directory'den gelen bir Git sunucusu.
Gitea bu iş için bence en hafif seçenek. Tek bir Go binary'si, birkaç yüz MB RAM ile rahat çalışıyor, issue, pull request, wiki, paket deposu ve GitHub Actions'a benzeyen bir CI tarafı da içinde geliyor. GitLab daha fazlasını yapıyor, ama küçük bir sunucuda GitLab'ı çalıştırmak ve güncel tutmak ayrı bir iş.
Bu yazının sonunda git.ornek.local adresinden açılan, PostgreSQL kullanan, SSH'ı 2222 portundan veren ve AD kullanıcılarının kendi parolalarıyla girebildiği bir Gitea olacak.
Dizin ve .env
gitea/
├── compose.yaml
├── .env
└── .env.examplePOSTGRES_USER=gitea
POSTGRES_PASSWORD=degistir-beni
POSTGRES_DB=gitea
GITEA_TAG=1.27chmod 600 .env
sed 's/=.*/=/' .env > .env.exampleVeritabanı parolasını openssl rand -hex 24 ile üretiyorum. Hex çıktıda özel karakter olmadığı için config dosyalarında kaçış derdi yok.
GITEA_TAG için 1.27 gibi sadece major ve minor içeren etiketi kullanıyorum. Bu etiket aynı serinin yama sürümlerini (1.27.3, 1.27.4 ...) takip ediyor, yani docker compose pull güvenlik yamalarını getiriyor ama seni kendiliğinden yeni bir minor sürüme taşımıyor. Tam sabitlemek istersen 1.27.3 gibi üç haneli etiketi yaz.
compose.yaml
name: gitea
services:
gitea:
image: gitea/gitea:${GITEA_TAG:?GITEA_TAG tanimli degil}
restart: unless-stopped
environment:
USER_UID: "1000"
USER_GID: "1000"
GITEA__database__DB_TYPE: postgres
GITEA__database__HOST: db:5432
GITEA__database__NAME: ${POSTGRES_DB:?POSTGRES_DB tanimli degil}
GITEA__database__USER: ${POSTGRES_USER:?POSTGRES_USER tanimli degil}
GITEA__database__PASSWD: ${POSTGRES_PASSWORD:?POSTGRES_PASSWORD tanimli degil}
GITEA__server__DOMAIN: git.ornek.local
GITEA__server__ROOT_URL: https://git.ornek.local/
GITEA__server__SSH_DOMAIN: git.ornek.local
GITEA__server__SSH_PORT: "2222"
GITEA__server__SSH_LISTEN_PORT: "22"
GITEA__service__DISABLE_REGISTRATION: "true"
GITEA__service__REQUIRE_SIGNIN_VIEW: "true"
volumes:
- gitea-data:/data
- /etc/timezone:/etc/timezone:ro
- /etc/localtime:/etc/localtime:ro
ports:
- "127.0.0.1:3000:3000"
- "10.0.10.20:2222:22"
depends_on:
db:
condition: service_healthy
networks:
- frontend
- backend
db:
image: postgres:17-alpine
restart: unless-stopped
environment:
POSTGRES_USER: ${POSTGRES_USER}
POSTGRES_PASSWORD: ${POSTGRES_PASSWORD}
POSTGRES_DB: ${POSTGRES_DB}
volumes:
- pgdata:/var/lib/postgresql/data
healthcheck:
test: ["CMD-SHELL", "pg_isready -h 127.0.0.1 -U $${POSTGRES_USER} -d $${POSTGRES_DB}"]
interval: 5s
timeout: 3s
retries: 10
start_period: 30s
networks:
- backend
networks:
frontend:
backend:
internal: true
volumes:
gitea-data:
pgdata:Birkaç satırın neden orada olduğunu anlatayım.
GITEA__bolum__ANAHTAR biçimindeki değişkenler, image'ın açılışta app.ini dosyasına yazdığı ayarlar. GITEA__server__ROOT_URL, app.ini içindeki [server] bölümünün ROOT_URL anahtarı oluyor. Ayarları böyle compose dosyasında tutmanın iyi tarafı, sunucuyu sıfırdan kurduğunda hepsinin dosyada hazır olması. Container her açılışta bu değerleri tekrar yazıyor, yani web arayüzünden ya da elle app.ini üzerinden değiştirdiğin bir ayar burada da tanımlıysa bir sonraki yeniden başlatmada geri dönüyor. Hangi ayarın nerede durduğunu karıştırmamak için kuralım şu: buradaki değişkenlerde olan ayara app.ini içinden dokunmuyorum.
SSH için iki port ayarı var ve ilk bakışta kafa karıştırıyor. SSH_LISTEN_PORT container'ın içinde sshd'nin dinlediği port, yani 22. SSH_PORT ise Gitea'nın arayüzde gösterdiği clone adresine yazdığı port. Host'ta 22 zaten sunucunun kendi SSH'ı tarafından kullanıldığı için container'ın 22'sini host'ta 2222'ye bağlıyorum ve kullanıcıya ssh://git@git.ornek.local:2222/... göstermesini istiyorum. Bu ikisi tutmazsa arayüzdeki clone adresi çalışmıyor.
SSH portunu 10.0.10.20:2222 diye sunucunun iç IP'sine bağladım. Docker'ın yayınladığı portlar UFW kurallarını atlıyor. Sunucunun bir ayağı dış ağa da bakıyorsa sadece 2222:22 yazmak Gitea'nın SSH'ını oraya da açıyor. Web portu ise sadece 127.0.0.1 üzerinde, önündeki reverse proxy'ye bırakıyorum.
REQUIRE_SIGNIN_VIEW giriş yapmayan birinin public işaretlenmiş repoları bile görmesini engelliyor. Kurum içinde birinin yanlışlıkla bir repoyu public yapması sık oluyor, bu ayar o hatanın etkisini sınırlıyor. DISABLE_REGISTRATION ile de herkesin kendine hesap açması kapanıyor, hesaplar ya yönetici tarafından ya da AD üzerinden gelecek.
USER_UID ve USER_GID container içindeki git kullanıcısının kimliği. Named volume kullandığım için pek önemi yok. /data'yı host'taki bir dizine bind mount edersen o dizinin sahibinin bu UID olması gerekiyor, yoksa ilk açılışta izin hatası alırsın.
İlk açılış ve kurulum ekranı
docker compose up -d --wait
docker compose logs -f giteaReverse proxy henüz yokken arayüze SSH tüneliyle bakıyorum:
ssh -L 3000:127.0.0.1:3000 yonetici@10.0.10.20Tarayıcıda http://127.0.0.1:3000 açınca Gitea'nın ilk kurulum sayfası geliyor. Veritabanı ve sunucu alanları compose dosyasındaki değerlerle dolu geliyor, onlara dokunmuyorum. Sayfanın en altındaki "Yönetici Hesabı Ayarları" kısmını açıp burada bir yönetici hesabı oluşturuyorum.
Bu adımı geciktirme. Kurulum tamamlanana kadar bu sayfa, adrese ulaşabilen herkesin veritabanı ayarlarını değiştirebileceği bir sayfa. Port sadece localhost'ta olduğu için burada risk düşük, ama doğrudan dış porta açılmış bir kurulumda risk gerçek. Kurulum bitince Gitea app.ini içine INSTALL_LOCK = true yazıyor ve sayfa bir daha açılmıyor.
Yönetici hesabını kurulum ekranında açmazsan kayıt kapalı olduğu için arayüzden kimse giremiyor. O zaman komut satırından açabilirsin:
docker compose exec -u git gitea gitea admin user create \
--admin --username yonetici --email yonetici@ornek.local --random-passwordÜretilen parola komut çıktısında generated random password is '...' satırında görünüyor. Sistemdeki ilk kullanıcı için ilk girişte parola değiştirme zorunluluğu yok, sonradan açılan hesaplarda varsayılan olarak var.
-u git önemli. Komutu root olarak çalıştırırsan Gitea Expect user 'git' (RUN_USER in app.ini) but current user is: root diyerek duruyor.
Önüne reverse proxy koymak
Traefik yazısındaki kurulum aynı sunucuda çalışıyorsa, gitea servisine web ağını ve label'ları eklemek yetiyor. 127.0.0.1:3000 satırı da o zaman kalkıyor:
gitea:
# ...
ports:
- "10.0.10.20:2222:22"
networks:
- web
- backend
labels:
- "traefik.enable=true"
- "traefik.http.routers.gitea.rule=Host(`git.ornek.local`)"
- "traefik.http.routers.gitea.entrypoints=websecure"
- "traefik.http.routers.gitea.tls=true"
- "traefik.http.services.gitea.loadbalancer.server.port=3000"
networks:
web:
external: true
backend:
internal: trueTraefik yazısından farklı olarak burada certresolver yok. ornek.local gibi iç bir alan adına Let's Encrypt sertifika vermiyor, sunucu internetten de erişilebilir değil. İç ağda iki makul yol var: kurumun kendi CA'sından (AD Certificate Services kuruluysa oradan) bir sertifika alıp Traefik'e dosya olarak vermek, ya da kurumun gerçek bir alan adı varsa git.ic.example.com gibi bir isim kullanıp Let's Encrypt'ten DNS challenge ile sertifika almak. DNS challenge'da sunucunun dışarıdan erişilebilir olması gerekmiyor, sadece DNS sağlayıcısının API'sine ulaşabilmesi yetiyor.
ROOT_URL mutlaka kullanıcının tarayıcıda gördüğü adresle aynı olmalı. Proxy HTTPS veriyor ama ROOT_URL hâlâ http:// ise giriş sonrası yönlendirmeler ve clone adresleri yanlış çıkıyor.
SSH ile clone
Kullanıcı açık anahtarını arayüzden (Ayarlar, "SSH / GPG Anahtarları") ekledikten sonra:
ssh -T -p 2222 git@git.ornek.localHi there, ali! You've successfully authenticated with the key named laptop, but Gitea does not provide shell access.Port numarasını her clone'da yazmamak için istemci tarafında ~/.ssh/config:
Host git.ornek.local
Port 2222
User git
IdentityFile ~/.ssh/id_ed25519Bundan sonra git clone git@git.ornek.local:altyapi/ag-yedekleri.git doğrudan çalışıyor.
Container içinde ayrı bir sshd çalıştırmak bence bu kurulumda en sade yol. Alternatifi host'un kendi SSH'ını 22'de bırakıp git kullanıcısının bağlantılarını container'a yönlendirmek (Gitea dokümanında "SSH passthrough" diye geçiyor). Kullanıcılar port yazmaktan kurtuluyor, ama host'ta bir git kullanıcısı, UID eşlemesi ve küçük bir yönlendirme script'i gerekiyor. Ekip büyükse ve port yazmak sürekli soru doğuruyorsa o yola bakılabilir.
Active Directory ile giriş
Kullanıcıların ayrı bir parola ezberlememesi için Gitea'yı AD'ye bağlıyorum. Önce AD'de iki şey hazırlıyorum: sadece okuma yapacak bir servis hesabı (svc-gitea) ve Gitea'ya girebilecek kişileri toplayan bir grup (Gitea-Kullanicilari). Servis hesabının parolasının süresinin dolmaması ve etkileşimli oturum açamaması lazım. AD tarafının kurulumunu Active Directory yazısında anlattım.
Gitea'da "Site Yönetimi", "Yetkilendirme Kaynakları" sayfasında "Kimlik Doğrulama Kaynağı Ekle" diyorum ve yetki türü olarak "LDAP (via BindDN)" seçiyorum. Doldurduğum alanlar (Türkçe arayüzdeki adlarıyla):
| Alan | Değer |
|---|---|
| Güvenlik Protokolü | LDAPS |
| Sunucu | dc01.ornek.local |
| Bağlantı Noktası | 636 |
| Bağlama DN'i | CN=svc-gitea,OU=Servis Hesaplari,DC=ornek,DC=local |
| Kullanıcı Arama Tabanı | OU=Personel,DC=ornek,DC=local |
| Kullanıcı Filtresi | (&(objectClass=user)(sAMAccountName=%[1]s)(memberOf=CN=Gitea-Kullanicilari,OU=Gruplar,DC=ornek,DC=local)) |
| Kullanıcı Adı Özelliği | sAMAccountName |
| E-posta Özelliği | mail |
| Ad Özelliği / Soyad Özelliği | givenName / sn |
Filtredeki %[1]s yerine girişte yazılan kullanıcı adı geliyor. Gitea filtreyi Go'nun Sprintf fonksiyonundan geçiriyor, bu yüzden eski dokümanlarda gördüğün %s de çalışıyor. %[1]s biçiminin farkı, kullanıcı adını filtrede birden fazla yerde kullanabilmen. memberOf koşulu sayesinde gruba eklenmeyen kişi doğru parolayla bile giremiyor, Gitea erişimini AD'deki grup üyeliğiyle yönetmiş oluyorsun.
LDAPS kullanmamın sebebi basit: 389 üzerinden düz LDAP ile kullanıcı parolası ağda açık gidiyor. LDAPS için DC'lerin bir sertifikası olması ve Gitea container'ının bu sertifikayı imzalayan CA'ya güvenmesi gerekiyor. Kurum CA'sının kök sertifikasını container'a vermenin en kolay yolu onu /etc/ssl/certs altına bağlamak:
volumes:
- ./ornek-root-ca.pem:/etc/ssl/certs/ornek-root-ca.pem:roImage açılışta update-ca-certificates çalıştırmıyor, o yüzden dosyayı /usr/local/share/ca-certificates altına koymak tek başına işe yaramıyor. Ama Gitea Go ile yazıldığı için buna gerek de yok: Go, güvenilen sertifikaları yüklerken /etc/ssl/certs dizinindeki bütün dosyaları okuyor. Tek şart dosyanın PEM biçiminde olması. AD Certificate Services'ten dışa aktarırken "Base-64 encoded X.509" seç. Elindeki dosya DER biçimindeyse (.cer dosyaları genelde öyle) önce çevir:
openssl x509 -inform der -in ornek-root-ca.cer -out ornek-root-ca.pemBağlantıyı Gitea'ya eklemeden önce aynı sorguyu sunucudan denemek hatayı ayırmayı çok kolaylaştırıyor:
ldapsearch -H ldaps://dc01.ornek.local -D "CN=svc-gitea,OU=Servis Hesaplari,DC=ornek,DC=local" -W \
-b "OU=Personel,DC=ornek,DC=local" "(sAMAccountName=ali)" mail memberOfBu komut sonuç döndürmüyorsa sorun Gitea'da değil, ya DN'lerde ya da sertifikada.
Kaynağı ekledikten sonra AD kullanıcısı ilk girişte Gitea'da otomatik olarak oluşturuluyor. Kaynakta "Kullanıcı senkronizasyonunu etkinleştir" seçeneğini açarsan AD'de devre dışı bırakılan ya da gruptan çıkarılan kullanıcılar da periyodik görevle Gitea'da pasif hale geliyor. İşten ayrılan birinin hesabının açık kalmaması için bunu açık tutuyorum.
Kurulumda oluşturduğun yerel yönetici hesabını silme. AD'ye ulaşılamadığı bir gün Gitea'ya girebilen tek hesap o.
Yedek
Gitea'nın verisi iki yerde: veritabanı ve /data (repolar, eklentiler, LFS dosyaları, app.ini). İkisini aynı anda yedeklemek gerekiyor, birini dünden diğerini bugünden geri yüklersen veritabanında olup diskte olmayan commit'ler çıkabiliyor.
Gitea'nın kendi dump komutu ikisini tek bir arşivde topluyor:
docker compose exec -u git gitea \
gitea dump -c /data/gitea/conf/app.ini --file /tmp/gitea-dump.zip
docker compose cp gitea:/tmp/gitea-dump.zip "./gitea-dump-$(date +%F).zip"
docker compose exec gitea rm /tmp/gitea-dump.zipBen bunun yanına PostgreSQL'in kendi yedeğini de alıyorum, çünkü geri yükleme sırasında en çok güvendiğim araç pg_dump ve psql:
docker compose exec -T db pg_dump -U gitea -d gitea | gzip > "gitea-db-$(date +%F).sql.gz"İkisini her gece çalıştırıp başka bir makineye taşıyan bir systemd timer'ı systemd yazısındaki yapıyla kurmak birkaç dakika sürüyor. Yedeğin işe yaradığını görmenin tek yolu da onu ara sıra başka bir makinede geri yüklemek. Geri yüklenmemiş yedek, yedek olup olmadığı belli olmayan bir dosya.
Sürüm yükseltme
Etiketi sabit tuttuğum için Gitea kendi kendine güncellenmiyor. Yükseltme sırası şöyle:
- Yeni sürümün sürüm notlarında "breaking" başlığını oku.
- Yukarıdaki yedeği al.
.enviçindeGITEA_TAGdeğerini değiştir.docker compose pull gitea && docker compose up -d --wait
Gitea açılışta veritabanı şemasını yeni sürüme kendisi taşıyor, loglarda migration satırlarını görürsün. Bu geri dönüşü olmayan bir adım: yeni sürüm şemayı değiştirdikten sonra eski image ile açmaya çalışırsan Gitea veritabanının daha yeni olduğunu söyleyip açılmıyor. Loglarda göreceğin hata şu:
Your database (migration version: ...) is for a newer Gitea, you can not use the newer database for this old Gitea release (...).Geri dönüş yolu, yükseltmeden hemen önce aldığın yedek.
Arada birkaç minor sürüm atlamak da sorun değil. Migration'lar sırayla tutuluyor ve Gitea açılışta veritabanının bulunduğu noktadan son sürüme kadar hepsini arka arkaya uyguluyor. Yine de atladığın her sürümün "breaking" notlarını okumak gerekiyor, çünkü config anahtarlarındaki değişiklikler migration ile düzelmiyor.
Takılınabilecek noktalar
Clone adresi yanlış port gösteriyor. Arayüzde git@git.ornek.local:... görünüyor ama 22'ye gidip host'un SSH'ına düşüyorsa GITEA__server__SSH_PORT tanımlı değil ya da container yeniden oluşturulmamış. docker compose up -d ortam değişkenleri değişince container'ı kendisi yeniden oluşturuyor, restart ise oluşturmuyor.
SSH parola soruyor. ssh -v -T -p 2222 git@git.ornek.local çıktısında anahtarın gönderilip reddedildiğini görüyorsan anahtar arayüzde başka bir kullanıcıya eklenmiş ya da hiç eklenmemiş olabilir. Bağlantı hiç kurulmuyorsa portun sunucunun hangi IP'sine bağlandığına bak, compose dosyasında iç IP'yi yanlış yazmak kolay.
AD kullanıcısı giremiyor, log sessiz. Gitea'nın logunda ayrıntı çıkmıyorsa önce yukarıdaki ldapsearch ile aynı kullanıcıyı sorgula. En sık sebep kullanıcının arama tabanının dışında bir OU'da durması ya da memberOf filtresindeki grubun tam DN'inin yanlış yazılması. İç içe gruplar da memberOf ile yakalanmıyor, kullanıcının gruba doğrudan üye olması gerekiyor. İç içe grup gerekiyorsa filtrede memberOf:1.2.840.113556.1.4.1941:= kullanılabiliyor, ama büyük dizinlerde yavaş.
Kurulum sayfası tekrar geldi. Volume yerine yanlışlıkla boş bir dizin bağlandıysa ya da docker compose down -v ile volume silindiyse Gitea app.ini bulamıyor ve kurulumu baştan istiyor. Repolar da o volume'da olduğu için burada çözüm yedekten dönmek.
Gitea ayağa kalktıktan sonra sıradaki mantıklı adım CI. Gitea Actions, GitHub Actions'taki workflow dosyalarının büyük kısmını olduğu gibi çalıştırıyor. Ayrı bir act_runner kurmak gerekiyor, onu başka bir yazıya bırakıyorum.