Docker İmajı Build ve Registry’e Push: GitLab CI/CD
GitLab CI/CD pipeline’larından bahsedince akla ilk gelen şeylerden biri Docker imaj build süreci. Uygulamanı containerize etmek güzel, ama her commit’te otomatik olarak imajı build edip registry’e push etmek işte asıl o zaman güçlü oluyor. Bu yazıda GitLab’ın kendi container registry’sini ve harici registry’leri kullanarak Docker imajlarını nasıl otomatik build edip push edeceğini detaylıca anlatacağım.
GitLab Container Registry Nedir?
GitLab, her proje için ücretsiz olarak built-in bir container registry sunuyor. Ayrıca Docker Hub, AWS ECR, Google Container Registry gibi harici servisleri de pipeline’larına entegre edebilirsin. GitLab CI/CD ile bu entegrasyonu kurmak düşündüğünden çok daha kolay, ama bazı ince noktaları bilmezsen kafan karışabilir.
Öncelikle şunu netleştirelim: GitLab Runner’ın Docker build yapabilmesi için ya Docker-in-Docker (DinD) yöntemini ya da Kaniko gibi alternatif bir araç kullanman gerekiyor. Her iki yöntemi de ele alacağız.
Ön Gereksinimler
Başlamadan önce şunların hazır olduğundan emin ol:
- GitLab hesabı ve bir proje
- GitLab Runner kurulu ve projeye register edilmiş
- Projende bir
Dockerfilemevcut - Runner’ın privileged modda çalışıyor olması (DinD için)
Runner’ın privileged modda olup olmadığını kontrol etmek için runner’ın kurulu olduğu sunucuda şu komuta bak:
cat /etc/gitlab-runner/config.toml
Çıktıda privileged = true görmüyorsan, config.toml dosyasını düzenleyip runner’ı yeniden başlatman gerekecek:
# config.toml içindeki ilgili runner bloğunda
[[runners]]
name = "docker-runner"
url = "https://gitlab.com/"
token = "TOKEN_BURAYA"
executor = "docker"
[runners.docker]
tls_verify = false
image = "docker:24.0.5"
privileged = true # Bu satır kritik
disable_entrypoint_overwrite = false
oom_kill_disable = false
disable_cache = false
volumes = ["/cach"]
shm_size = 0
Değişikliği yaptıktan sonra runner’ı restart et:
sudo gitlab-runner restart
GitLab CI/CD Variable’larını Ayarlamak
Pipeline’a şifre veya token gibi hassas bilgileri direkt .gitlab-ci.yml dosyasına yazmamalısın. Bunun için GitLab’ın CI/CD Variables özelliğini kullanman gerekiyor.
GitLab arayüzünde Settings > CI/CD > Variables bölümüne gidip şu değişkenleri ekle:
- CI_REGISTRY_USER: Docker registry kullanıcı adı
- CI_REGISTRY_PASSWORD: Docker registry şifresi veya access token
- CI_REGISTRY: Registry adresi (örn. registry.gitlab.com)
Güzel haber şu: GitLab Container Registry kullanırken bu değişkenler zaten otomatik olarak tanımlı geliyor. $CI_REGISTRY, $CI_REGISTRY_IMAGE, $CI_REGISTRY_USER ve $CI_REGISTRY_PASSWORD değişkenlerini ekstra bir şey yapmadan doğrudan kullanabilirsin.
Temel Pipeline: GitLab Container Registry’e Push
En basit haliyle bir Docker build ve push pipeline’ı şöyle görünür:
# .gitlab-ci.yml
stages:
- build
variables:
DOCKER_DRIVER: overlay2
DOCKER_TLS_CERTDIR: "/certs"
build-image:
stage: build
image: docker:24.0.5
services:
- docker:24.0.5-dind
before_script:
- docker login -u $CI_REGISTRY_USER -p $CI_REGISTRY_PASSWORD $CI_REGISTRY
script:
- docker build -t $CI_REGISTRY_IMAGE:$CI_COMMIT_SHORT_SHA .
- docker build -t $CI_REGISTRY_IMAGE:latest .
- docker push $CI_REGISTRY_IMAGE:$CI_COMMIT_SHORT_SHA
- docker push $CI_REGISTRY_IMAGE:latest
only:
- main
Bu pipeline main branch’e her push yapıldığında tetiklenecek ve imajı hem commit hash’iyle hem de latest tag’iyle registry’e push edecek. $CI_COMMIT_SHORT_SHA değişkeni GitLab tarafından otomatik sağlanıyor ve her build’i takip etmeni kolaylaştırıyor.
Çoklu Stage ile Daha Kapsamlı Pipeline
Gerçek dünya senaryolarında genellikle build öncesi testler ve sonrasında deploy adımları da olur. Şöyle bir yapı çok daha yaygın:
# .gitlab-ci.yml
stages:
- test
- build
- push
- deploy
variables:
DOCKER_DRIVER: overlay2
DOCKER_TLS_CERTDIR: "/certs"
IMAGE_TAG: $CI_REGISTRY_IMAGE:$CI_COMMIT_SHORT_SHA
# Test aşaması
unit-tests:
stage: test
image: python:3.11-slim
script:
- pip install -r requirements.txt
- pytest tests/ -v --junitxml=report.xml
artifacts:
when: always
reports:
junit: report.xml
# Build aşaması - imajı oluştur ama henüz push etme
build-docker:
stage: build
image: docker:24.0.5
services:
- docker:24.0.5-dind
before_script:
- docker login -u $CI_REGISTRY_USER -p $CI_REGISTRY_PASSWORD $CI_REGISTRY
script:
- docker build --cache-from $CI_REGISTRY_IMAGE:latest
--tag $IMAGE_TAG
--tag $CI_REGISTRY_IMAGE:latest .
- docker push $IMAGE_TAG
- docker push $CI_REGISTRY_IMAGE:latest
only:
- main
- develop
# Deploy aşaması (örnek)
deploy-staging:
stage: deploy
image: alpine:latest
before_script:
- apk add --no-cache curl
script:
- echo "Staging ortamına deploy ediliyor..."
- curl -X POST $DEPLOY_WEBHOOK_URL
only:
- develop
environment:
name: staging
Burada dikkat çeken bir detay var: --cache-from parametresi. Bu sayede önceki build’den kalan katmanları cache olarak kullanıyor ve build süresini ciddi şekilde kısaltıyorsun. Özellikle bağımlılıkların yoğun olduğu imajlarda bu fark 10 dakikadan 2 dakikaya kadar düşebilir.
Docker Hub’a Push Yapmak
GitLab Container Registry yerine Docker Hub kullanmak isteyebilirsin. Bu durumda Variable’larını farklı tanımlaman ve login komutunu değiştirmen gerekiyor.
Önce GitLab’da şu değişkenleri ekle:
- DOCKERHUB_USERNAME: Docker Hub kullanıcı adın
- DOCKERHUB_TOKEN: Docker Hub’dan oluşturduğun access token (şifre değil, token kullan!)
# .gitlab-ci.yml - Docker Hub versiyonu
stages:
- build
variables:
DOCKER_DRIVER: overlay2
DOCKER_TLS_CERTDIR: "/certs"
DOCKERHUB_IMAGE: kullaniciadi/uygulama-adi
build-and-push:
stage: build
image: docker:24.0.5
services:
- docker:24.0.5-dind
before_script:
- echo "$DOCKERHUB_TOKEN" | docker login -u "$DOCKERHUB_USERNAME" --password-stdin
script:
- docker build -t $DOCKERHUB_IMAGE:$CI_COMMIT_SHORT_SHA -t $DOCKERHUB_IMAGE:latest .
- docker push $DOCKERHUB_IMAGE:$CI_COMMIT_SHORT_SHA
- docker push $DOCKERHUB_IMAGE:latest
only:
- main
--password-stdin kullanımına dikkat et. docker login -p PASSWORD formatında şifreyi komut satırı argümanı olarak geçmek güvenli değil çünkü process listesinde görünebilir. --password-stdin ile stdin üzerinden okutmak daha güvenli bir pratik.
Multi-Platform İmaj Build Etmek (ARM + AMD64)
Günümüzde Apple Silicon’ın yaygınlaşması ve ARM tabanlı sunucuların artmasıyla birlikte multi-platform imaj build etmek önem kazandı. GitLab CI/CD’de buildx kullanarak bunu yapabilirsin:
build-multiplatform:
stage: build
image: docker:24.0.5
services:
- docker:24.0.5-dind
before_script:
- docker login -u $CI_REGISTRY_USER -p $CI_REGISTRY_PASSWORD $CI_REGISTRY
- docker buildx create --use --name multiplatform-builder
- docker buildx inspect --bootstrap
script:
- docker buildx build
--platform linux/amd64,linux/arm64
--tag $CI_REGISTRY_IMAGE:$CI_COMMIT_SHORT_SHA
--tag $CI_REGISTRY_IMAGE:latest
--push .
only:
- main
Burada --push flag’ini doğrudan buildx build komutuna eklediğimize dikkat et. buildx ile önce build sonra push yapmak yerine build sırasında direkt push etmek daha kolay çünkü multi-platform imajların manifest listelerini sonradan yönetmek karmaşıklaşıyor.
Kaniko ile Docker-in-Docker’sız Build
Bazı ortamlarda privileged mode kullanmak güvenlik politikaları nedeniyle mümkün olmayabiliyor. Bu durumda Google’ın geliştirdiği Kaniko aracı kurtarıcı oluyor. Kaniko, Docker daemon gerektirmeden container içinden imaj build edebiliyor:
# .gitlab-ci.yml - Kaniko versiyonu
stages:
- build
build-with-kaniko:
stage: build
image:
name: gcr.io/kaniko-project/executor:v1.9.0-debug
entrypoint: [""]
script:
- mkdir -p /kaniko/.docker
- echo "{"auths":{"$CI_REGISTRY":{"auth":"$(printf "%s:%s" "$CI_REGISTRY_USER" "$CI_REGISTRY_PASSWORD" | base64 | tr -d 'n')"}}}" > /kaniko/.docker/config.json
- /kaniko/executor
--context "${CI_PROJECT_DIR}"
--dockerfile "${CI_PROJECT_DIR}/Dockerfile"
--destination "${CI_REGISTRY_IMAGE}:${CI_COMMIT_SHORT_SHA}"
--destination "${CI_REGISTRY_IMAGE}:latest"
--cache=true
--cache-repo "${CI_REGISTRY_IMAGE}/cache"
only:
- main
Kaniko’nun --cache ve --cache-repo parametreleri sayesinde önceki build layer’larını registry’de saklayıp sonraki build’lerde kullanabiliyorsun. Bu DinD’deki yerel cache’e benzer bir işlev görüyor.
Branch ve Tag’e Göre Dinamik İmaj Etiketleme
Profesyonel bir pipeline’da imajları akıllıca etiketlemek çok önemli. Feature branch’lerden latest‘i kirletmemek, release tag’lerini düzgün yönetmek gerekiyor:
# .gitlab-ci.yml - Akıllı tagging
stages:
- build
variables:
DOCKER_DRIVER: overlay2
DOCKER_TLS_CERTDIR: "/certs"
.build-template: &build-template
image: docker:24.0.5
services:
- docker:24.0.5-dind
before_script:
- docker login -u $CI_REGISTRY_USER -p $CI_REGISTRY_PASSWORD $CI_REGISTRY
build-feature-branch:
<<: *build-template
stage: build
script:
- BRANCH_TAG=$(echo $CI_COMMIT_REF_NAME | tr '/' '-' | tr '[:upper:]' '[:lower:]')
- docker build -t $CI_REGISTRY_IMAGE:$BRANCH_TAG .
- docker push $CI_REGISTRY_IMAGE:$BRANCH_TAG
except:
- main
- tags
build-main:
<<: *build-template
stage: build
script:
- docker build -t $CI_REGISTRY_IMAGE:latest -t $CI_REGISTRY_IMAGE:$CI_COMMIT_SHORT_SHA .
- docker push $CI_REGISTRY_IMAGE:latest
- docker push $CI_REGISTRY_IMAGE:$CI_COMMIT_SHORT_SHA
only:
- main
build-release:
<<: *build-template
stage: build
script:
- docker build -t $CI_REGISTRY_IMAGE:$CI_COMMIT_TAG -t $CI_REGISTRY_IMAGE:stable .
- docker push $CI_REGISTRY_IMAGE:$CI_COMMIT_TAG
- docker push $CI_REGISTRY_IMAGE:stable
only:
- tags
Branch adlarındaki slash karakterlerini tire’ye çeviriyoruz çünkü Docker tag’lerinde slash kullanılamıyor. feature/login-page gibi bir branch adı feature-login-page olarak etiketleniyor.
Build Arguments ve Secrets Kullanımı
Bazen Dockerfile’a build-time değişkenler geçmen gerekebilir. Örneğin private npm registry adresi veya internal API endpoint gibi şeyler:
build-with-args:
stage: build
image: docker:24.0.5
services:
- docker:24.0.5-dind
before_script:
- docker login -u $CI_REGISTRY_USER -p $CI_REGISTRY_PASSWORD $CI_REGISTRY
script:
- docker build
--build-arg APP_VERSION=$CI_COMMIT_TAG
--build-arg BUILD_DATE=$(date -u +'%Y-%m-%dT%H:%M:%SZ')
--build-arg GIT_COMMIT=$CI_COMMIT_SHA
--build-arg NPM_TOKEN=$NPM_REGISTRY_TOKEN
--tag $CI_REGISTRY_IMAGE:$CI_COMMIT_SHORT_SHA
--tag $CI_REGISTRY_IMAGE:latest .
- docker push $CI_REGISTRY_IMAGE:$CI_COMMIT_SHORT_SHA
- docker push $CI_REGISTRY_IMAGE:latest
only:
- main
Karşılığındaki Dockerfile’da bu arg’ları şöyle kullanırsın:
FROM node:18-alpine
ARG APP_VERSION=dev
ARG BUILD_DATE
ARG GIT_COMMIT
ARG NPM_TOKEN
# NPM token'ı kullan ama imajda bırakma
RUN echo "//registry.npmjs.org/:_authToken=${NPM_TOKEN}" > ~/.npmrc
&& npm ci --production
&& rm -f ~/.npmrc
# Build metadata label olarak ekle
LABEL version="$APP_VERSION"
build-date="$BUILD_DATE"
git-commit="$GIT_COMMIT"
COPY . .
RUN npm run build
EXPOSE 3000
CMD ["node", "dist/server.js"]
Burada önemli bir nokta var: NPM_TOKEN gibi hassas değerleri ARG ile alıp kullandıktan hemen sonra temizliyoruz. Aksi takdirde bu değer Docker layer geçmişinde kalabilir ve docker history ile görülebilir.
Pipeline Optimizasyonu: Build Süresini Kısaltmak
Büyük projelerde her commit’te 15-20 dakika Docker build beklemek verimliliği öldürür. Birkaç pratik optimizasyon:
Layer caching ile cache from kullanımı:
variables:
DOCKER_BUILDKIT: 1
build-optimized:
stage: build
image: docker:24.0.5
services:
- docker:24.0.5-dind
before_script:
- docker login -u $CI_REGISTRY_USER -p $CI_REGISTRY_PASSWORD $CI_REGISTRY
script:
# Önce mevcut imajı pull et (cache için)
- docker pull $CI_REGISTRY_IMAGE:latest || true
# Cache kullanarak build et
- docker build
--cache-from $CI_REGISTRY_IMAGE:latest
--build-arg BUILDKIT_INLINE_CACHE=1
--tag $CI_REGISTRY_IMAGE:$CI_COMMIT_SHORT_SHA
--tag $CI_REGISTRY_IMAGE:latest .
- docker push $CI_REGISTRY_IMAGE:$CI_COMMIT_SHORT_SHA
- docker push $CI_REGISTRY_IMAGE:latest
only:
- main
BUILDKIT_INLINE_CACHE=1 build argument’ı, cache metadata’sını imajın içine gömer. Böylece başka bir makinede veya temiz bir runner’da bu imajı pull edip cache olarak kullanabilirsin.
Sadece değişen dosyalarda pipeline tetikleme:
build-image:
stage: build
script:
- docker build -t $CI_REGISTRY_IMAGE:latest .
- docker push $CI_REGISTRY_IMAGE:latest
rules:
- changes:
- Dockerfile
- src/**/*
- package.json
- requirements.txt
changes kuralı sayesinde Dockerfile veya kaynak kodunda değişiklik olmadığında bu job atlaniyor. Sadece readme güncellediysen Docker build tetiklenmiyor.
Yaygın Hatalar ve Çözümleri
Sysadmin olarak bu pipeline’ları kurarken en çok şu hatalarla karşılaşacaksın:
“Cannot connect to the Docker daemon” hatası: Bu hata genellikle DinD servisinin hazır olmadan build’in başlamasından kaynaklanıyor. DOCKER_TLS_CERTDIR variable’ını set etmek ve Docker’ın hazır olmasını beklemek gerekiyor. Runner config’inde privileged = true ayarını da kontrol et.
“unauthorized: authentication required” hatası: CI_REGISTRY_PASSWORD variable’ının Masked ve Protected olarak işaretlenip işaretlenmediğini kontrol et. Protected branch’lerde çalışmayan bir variable protected olmayan runner’da görünmeyebilir.
“no space left on device” hatası: Runner’ın diskinde yer bitti. Eski imajları temizlemek için runner’a periyodik cleanup ekle:
# Crontab'a ekle
0 2 * * * docker system prune -af --filter "until=24h" >> /var/log/docker-cleanup.log 2>&1
Build çok yavaş çalışıyor: Dockerfile’ının layer sırasını optimize et. Sık değişen şeyleri (uygulama kodu) en sona, nadir değişenleri (sistem paketleri, bağımlılıklar) en üste koy. Cache mekanizması bu sıraya göre çalışıyor.
Güvenlik Taraması Entegrasyonu
Build edilen imajları push etmeden önce güvenlik taramasından geçirmek iyi bir pratik. GitLab kendi Container Scanning özelliğini sunuyor ama ücretsiz tier’da sınırlı. Trivy ile ücretsiz yapabilirsin:
scan-image:
stage: build
image:
name: aquasec/trivy:latest
entrypoint: [""]
variables:
TRIVY_NO_PROGRESS: "true"
TRIVY_CACHE_DIR: ".trivycache/"
script:
- trivy image --exit-code 0 --severity LOW,MEDIUM $CI_REGISTRY_IMAGE:$CI_COMMIT_SHORT_SHA
- trivy image --exit-code 1 --severity HIGH,CRITICAL $CI_REGISTRY_IMAGE:$CI_COMMIT_SHORT_SHA
cache:
paths:
- .trivycache/
needs:
- build-docker
only:
- main
--exit-code 1 ile HIGH ve CRITICAL zaafiyetler bulunduğunda pipeline başarısız oluyor. LOW ve MEDIUM için exit code 0 yani pipeline devam ediyor ama bulguları görüyorsun. Bu dengeyi kendi risk iştahına göre ayarlayabilirsin.
Sonuç
GitLab CI/CD ile Docker imaj build ve push sürecini otomatize etmek, DevOps pratiklerinin temel taşlarından biri. Anlattığım yöntemleri özetleyecek olursam:
- GitLab Container Registry ile başlamak en kolay yol, değişkenler otomatik geliyor
- DinD yaygın ama privileged mode gerektiriyor; güvenlik kısıtlamaları varsa Kaniko iyi bir alternatif
- Cache-from ve BuildKit kullanımı build sürelerini dramatik şekilde kısaltıyor
- Akıllı tagging stratejisi ile branch, commit ve release imajlarını düzenli tutabilirsin
- Build args ile hassas değerleri runtime’da geçip imajda iz bırakmamak güvenlik açısından kritik
- Trivy gibi araçlarla güvenlik taramasını pipeline’a dahil etmek production’a güvenli imajlar çıkarmana yardımcı oluyor
Bu pipeline yapısını kurduktan sonra geliştiriciler sadece kod yazıp push yapıyor, geri kalanını CI/CD hallediyor. İlk kurulum biraz zaman alıyor ama sonrasında günde onlarca build otomatik çalışırken o zamanın değdiğini göreceksin.
