Modern yazılım geliştirme dünyasında “bende çalışıyor” problemi artık mazeret sayılmıyor. Ekipler büyüdükçe, deployment sıklığı arttıkça ve mikroservis mimarileri yaygınlaştıkça, tutarlı build ve test ortamlarına olan ihtiyaç da katlanarak artıyor. İşte tam bu noktada Docker ile CI/CD entegrasyonu devreye giriyor. Bu yazıda, production’da gerçekten kullandığım pipeline yapılarını, karşılaştığım sorunları ve çözüm yollarını paylaşacağım.
CI/CD Pipeline’ında Docker’ın Rolü
Docker’ı CI/CD süreçlerine dahil etmenin temel mantığı şu: her build, test ve deployment adımı izole, tekrarlanabilir ve taşınabilir bir ortamda çalışsın. Geliştirici makinesindeki Python versiyonu ile CI sunucusundaki Python versiyonunun farklı olması gibi klasik problemleri kalıcı olarak çözmek istiyorsanız Docker doğru araç.
Bir pipeline’da Docker’ı genellikle iki farklı şekilde kullanırsınız:
- Docker as a build tool: Uygulamanızı Docker içinde build edip test edersiniz, çıktı olarak bir Docker image üretirsiniz.
- Docker as a runtime: CI/CD sisteminin kendisi (runner, agent) Docker container içinde çalışır.
Çoğu gerçek dünya senaryosunda bu iki yaklaşım iç içe geçer. Hadi adım adım inceleyelim.
Temel Dockerfile Yapısı: CI Dostu Tasarım
CI/CD için optimize edilmiş bir Dockerfile yazmak, local development için yazmaktan biraz farklı. En önemli konu layer cache‘i verimli kullanmak. Gereksiz yere cache’i bozan bir Dockerfile, her commit’te sıfırdan build başlatır ve pipeline sürelerinizi katlayabilir.
İşte Node.js uygulaması için CI dostu bir Dockerfile örneği:
# Multi-stage build - production image'ı küçük tutar
FROM node:18-alpine AS dependencies
WORKDIR /app
COPY package.json package-lock.json ./
RUN npm ci --only=production
FROM node:18-alpine AS build
WORKDIR /app
COPY package.json package-lock.json ./
RUN npm ci
COPY . .
RUN npm run build
FROM node:18-alpine AS production
WORKDIR /app
ENV NODE_ENV=production
COPY --from=dependencies /app/node_modules ./node_modules
COPY --from=build /app/dist ./dist
COPY package.json ./
EXPOSE 3000
USER node
CMD ["node", "dist/index.js"]
Bu yapının avantajları şunlar:
- dependencies stage’i sadece production bağımlılıklarını yükler, node_modules’ün gereksiz şişmesini önler
- build stage’i dev bağımlılıklarıyla birlikte build alır ama bu katman production image’ına geçmez
- production stage’i minimal ve güvenli bir image üretir
package.jsonvepackage-lock.jsondosyaları kaynak koddan önce kopyalandığı için, sadece kod değiştiğinde npm install yeniden çalışmaz
GitLab CI ile Docker Pipeline Kurulumu
GitLab CI, Docker entegrasyonu açısından en olgun platformlardan biri. .gitlab-ci.yml dosyanızı doğru yapılandırmak kritik.
# .gitlab-ci.yml
variables:
DOCKER_DRIVER: overlay2
DOCKER_TLS_CERTDIR: "/certs"
IMAGE_NAME: $CI_REGISTRY_IMAGE
IMAGE_TAG: $CI_COMMIT_SHORT_SHA
stages:
- build
- test
- security
- deploy
# Docker-in-Docker için servis tanımı
build:
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 $IMAGE_NAME:latest
--build-arg BUILDKIT_INLINE_CACHE=1
-t $IMAGE_NAME:$IMAGE_TAG
-t $IMAGE_NAME:latest .
- docker push $IMAGE_NAME:$IMAGE_TAG
- docker push $IMAGE_NAME:latest
only:
- main
- develop
Burada dikkat edilmesi gereken bir nokta var: --cache-from parametresi ile önceki build’in cache’ini kullanıyoruz. BUILDKIT_INLINE_CACHE=1 flag’i ise cache metadata’sını image içine gömiyor ki sonraki build’ler bu bilgiyi kullanabilsin.
Test Stage’i: Servis Bağımlılıklarıyla Çalışmak
Gerçek projelerde test aşaması genellikle veritabanı, cache veya message broker gibi harici servislere ihtiyaç duyar. GitLab CI’ın services özelliği burada hayat kurtarıyor.
test:unit:
stage: test
image: $IMAGE_NAME:$IMAGE_TAG
services:
- name: postgres:15-alpine
alias: db
- name: redis:7-alpine
alias: cache
variables:
POSTGRES_DB: testdb
POSTGRES_USER: testuser
POSTGRES_PASSWORD: testpass
DATABASE_URL: "postgresql://testuser:testpass@db:5432/testdb"
REDIS_URL: "redis://cache:6379"
script:
- npm run test:unit
- npm run test:integration
coverage: '/Liness*:s*(d+.?d*)%/'
artifacts:
reports:
coverage_report:
coverage_format: cobertura
path: coverage/cobertura-coverage.xml
paths:
- coverage/
expire_in: 1 week
Bu yapıda testler, production image’ının kendisi üzerinde koşuyor. Bu yaklaşım şu soruyu ortadan kaldırır: “Test ortamı production ortamına ne kadar benziyor?” Cevap: Aynısı.
GitHub Actions ile Docker Pipeline
GitHub Actions kullanıyorsanız, Docker Hub veya GitHub Container Registry ile entegrasyon biraz farklı ama bir o kadar güçlü.
# .github/workflows/docker-pipeline.yml
name: Docker CI/CD Pipeline
on:
push:
branches: [main, develop]
pull_request:
branches: [main]
env:
REGISTRY: ghcr.io
IMAGE_NAME: ${{ github.repository }}
jobs:
build-and-test:
runs-on: ubuntu-latest
permissions:
contents: read
packages: write
security-events: write
steps:
- name: Checkout repository
uses: actions/checkout@v4
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v3
- name: Log in to Container Registry
uses: docker/login-action@v3
with:
registry: ${{ env.REGISTRY }}
username: ${{ github.actor }}
password: ${{ secrets.GITHUB_TOKEN }}
- name: Extract metadata for Docker
id: meta
uses: docker/metadata-action@v5
with:
images: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}
tags: |
type=ref,event=branch
type=ref,event=pr
type=sha,prefix=sha-
type=raw,value=latest,enable=${{ github.ref == 'refs/heads/main' }}
- name: Build and push Docker image
uses: docker/build-push-action@v5
with:
context: .
push: true
tags: ${{ steps.meta.outputs.tags }}
labels: ${{ steps.meta.outputs.labels }}
cache-from: type=gha
cache-to: type=gha,mode=max
build-args: |
BUILD_DATE=${{ github.event.head_commit.timestamp }}
VCS_REF=${{ github.sha }}
GitHub Actions’ın cache-from: type=gha özelliği, GitHub’ın kendi cache altyapısını kullanıyor ve son derece etkili. Özellikle mode=max ile tüm intermediate layer’ları cache’liyorsunuz.
Docker Compose ile Entegrasyon Test Ortamı
Birden fazla mikroservisin birlikte test edilmesi gerektiğinde Docker Compose vazgeçilmez oluyor. CI ortamında Compose kullanımı için bazı püf noktalar var.
# docker-compose.test.yml
version: '3.8'
services:
app:
build:
context: .
target: build
environment:
- NODE_ENV=test
- DATABASE_URL=postgresql://postgres:password@postgres:5432/testdb
- REDIS_URL=redis://redis:6379
- RABBITMQ_URL=amqp://guest:guest@rabbitmq:5672
depends_on:
postgres:
condition: service_healthy
redis:
condition: service_healthy
command: npm run test:e2e
postgres:
image: postgres:15-alpine
environment:
POSTGRES_PASSWORD: password
POSTGRES_DB: testdb
healthcheck:
test: ["CMD-SHELL", "pg_isready -U postgres"]
interval: 5s
timeout: 5s
retries: 5
redis:
image: redis:7-alpine
healthcheck:
test: ["CMD", "redis-cli", "ping"]
interval: 5s
timeout: 3s
retries: 5
rabbitmq:
image: rabbitmq:3.12-management-alpine
healthcheck:
test: ["CMD", "rabbitmq-diagnostics", "ping"]
interval: 10s
timeout: 5s
retries: 5
Bu Compose dosyasını CI pipeline’ında şöyle kullanırsınız:
# CI script içinde
script:
- docker compose -f docker-compose.test.yml up --build --abort-on-container-exit
- EXIT_CODE=$(docker compose -f docker-compose.test.yml ps -q app | xargs docker inspect -f '{{ .State.ExitCode }}')
- docker compose -f docker-compose.test.yml down -v
- exit $EXIT_CODE
--abort-on-container-exit parametresi önemli: herhangi bir container durduğunda tüm stack’i durdurur. app container’ı testleri tamamlayıp çıktığında diğerleri de durur ve exit code’u alırsınız.
Security Scanning Pipeline’a Entegrasyon
Production’a çıkan her image’ı taramak artık bir best practice değil, zorunluluk. Trivy, açık kaynak ve son derece etkili bir araç.
security:scan:
stage: security
image: aquasec/trivy:latest
variables:
TRIVY_NO_PROGRESS: "true"
TRIVY_CACHE_DIR: ".trivycache/"
cache:
paths:
- .trivycache/
script:
# HIGH ve CRITICAL vulnerability varsa pipeline'ı durdur
- trivy image
--exit-code 1
--severity HIGH,CRITICAL
--ignore-unfixed
--format sarif
--output trivy-results.sarif
$IMAGE_NAME:$IMAGE_TAG
artifacts:
when: always
paths:
- trivy-results.sarif
expire_in: 30 days
allow_failure: false
--ignore-unfixed parametresi pratikte çok önemli. Henüz fix’i olmayan zafiyetleri rapordan çıkarmak pipeline’ı gereksiz şekilde bloke etmekten kurtarır. Ancak bu parametreyi kullanıyorsanız, düzenli aralıklarla tüm zafiyetleri (unfixed dahil) listeleyen ayrı bir rapor üretmenizi öneririm.
Deployment Stage: Kubernetes’e Rolling Update
Image build ve test aşamaları tamamlandıktan sonra deployment adımına geliyoruz. Kubernetes ortamına deployment için kubectl veya helm kullanabilirsiniz.
deploy:production:
stage: deploy
image: bitnami/kubectl:latest
environment:
name: production
url: https://app.example.com
before_script:
- kubectl config set-cluster production
--server=$KUBE_URL
--certificate-authority=$KUBE_CA_FILE
- kubectl config set-credentials ci-user
--token=$KUBE_TOKEN
- kubectl config set-context production
--cluster=production
--user=ci-user
- kubectl config use-context production
script:
# Image tag'ini güncelle
- kubectl set image deployment/app
app=$IMAGE_NAME:$IMAGE_TAG
-n production
# Rollout'u bekle, timeout: 5 dakika
- kubectl rollout status deployment/app
-n production
--timeout=300s
after_script:
# Başarısız olursa otomatik rollback
- |
if [ $CI_JOB_STATUS == 'failed' ]; then
kubectl rollout undo deployment/app -n production
echo "Deployment başarısız, rollback yapıldı"
fi
only:
- main
when: manual
when: manual ile production deployment’ını otomatik değil, manuel onaya bağlayabilirsiniz. Bu, özellikle kritik servisler için önemli bir güvenlik katmanı.
BuildKit ve Cache Optimizasyonu
Pipeline sürelerini kısaltmak için BuildKit’i tam anlamıyla kullanmak gerekiyor. Birkaç pratik optimizasyon:
# BuildKit'i aktif et
export DOCKER_BUILDKIT=1
# Registry cache kullanımı - en etkili yöntem
docker build
--cache-from type=registry,ref=myregistry.com/myapp:buildcache
--cache-to type=registry,ref=myregistry.com/myapp:buildcache,mode=max
-t myregistry.com/myapp:$VERSION
.
# Paralel stage build için build argümanları
docker build
--build-arg BUILDKIT_INLINE_CACHE=1
--progress=plain
-t myapp:latest .
Registry-based cache, özellikle ephemeral runner’lar (her pipeline için temiz bir makine başlatan sistemler) kullandığınızda disk cache’e göre çok daha etkili. GitLab Runners veya GitHub Actions’ın self-hosted runner’ları ephemeral olabilir; bu durumda cache’i registry’ye gömmek pipeline sürelerinizi yarıya indirebilir.
Pratik Sorun: Flaky Tests ve Docker
Containerized test ortamlarında en sık karşılaşılan sorunlardan biri, servisler hazır olmadan testlerin başlaması. depends_on ile healthcheck kombinasyonu bu sorunu çözer ama bazen yetmez. İşte production’da kullandığım bir wait script:
#!/bin/bash
# wait-for-services.sh
set -e
TIMEOUT=${TIMEOUT:-60}
SERVICES=("$@")
wait_for_service() {
local service=$1
local host=$(echo $service | cut -d: -f1)
local port=$(echo $service | cut -d: -f2)
local elapsed=0
echo "Bekleniyor: $host:$port"
while ! nc -z $host $port 2>/dev/null; do
if [ $elapsed -ge $TIMEOUT ]; then
echo "TIMEOUT: $host:$port $TIMEOUT saniyede hazır olmadı"
exit 1
fi
sleep 2
elapsed=$((elapsed + 2))
done
echo "Hazır: $host:$port (${elapsed}s)"
}
for service in "${SERVICES[@]}"; do
wait_for_service $service
done
echo "Tüm servisler hazır, testler başlıyor..."
exec "$@"
Bu scripti Dockerfile’ınıza kopyalayıp test command’inizde kullanabilirsiniz:
COPY wait-for-services.sh /usr/local/bin/wait-for-services
RUN chmod +x /usr/local/bin/wait-for-services
# Kullanım
CMD ["wait-for-services", "db:5432", "cache:6379", "--", "npm", "run", "test"]
Image Tagging Stratejisi
Production ortamında image tagging karmaşık bir konu. Önerdiğim strateji:
latest: Sadece main/master branch’ten üretilen son stable imagedevelop: Develop branch’inin son hali, QA ortamı içinsha-abc1234: Her commit için unique tag, traceability için kritikv1.2.3: Semantic versioning tag’leri, release süreçleri içinmain-20240115: Branch + tarih kombinasyonu, rollback senaryoları için
latest tag’ini production deployment’ında asla kullanmayın. Deployment sırasında image değişmiş olabilir ve tam olarak neyi deploy ettiğinizi bilmezsiniz. Her zaman commit SHA veya semantic version ile tag’lenmiş bir image kullanın.
Monitoring ve Observability
Pipeline’larınızın sağlığını izlemek de en az pipeline’ı kurmak kadar önemli. Birkaç pratik metrik takip etmenizi öneririm:
- Pipeline duration: Build süresi trend olarak artıyorsa Dockerfile veya cache optimizasyonu gerekiyor
- Failure rate by stage: Hangi stage en çok başarısız oluyor? Flaky test mi, environment sorunu mu?
- Image size over time: Image boyutu commit’ler arasında dramatik değişiyor mu?
GitLab CI için pipeline analytics built-in geliyor. GitHub Actions için ise Datadog veya custom metrics with Prometheus kullanabilirsiniz.
Güvenlik: Secret Yönetimi
CI/CD pipeline’ında Docker kullanırken en kritik konulardan biri secret yönetimi. Dockerfile’a asla secret gömmeyln. ARG ile build-time secret geçmek de log’larda görünebilir.
BuildKit’in secret mounting özelliği bu sorunu çözüyor:
# Dockerfile içinde
RUN --mount=type=secret,id=npmrc,target=/root/.npmrc
npm install
# Build sırasında
docker build
--secret id=npmrc,src=$HOME/.npmrc
-t myapp:latest .
Bu yöntemle .npmrc dosyası final image’a gömülmez, sadece RUN komutunun çalışması sırasında kullanılabilir durumda olur.
Sonuç
Docker ile CI/CD entegrasyonu, başta karmaşık görünse de doğru temeller atıldığında son derece güçlü ve güvenilir bir yapı ortaya çıkıyor. Bu yazıda ele aldığım konuları özetlemek gerekirse:
- Multi-stage build ile hem build sürelerini hem de final image boyutunu optimize edebilirsiniz
- Layer cache’i doğru kullanmak, pipeline sürelerinizi dramatik ölçüde kısaltır
- Test aşamasında servis bağımlılıkları için
healthcheckve wait script kombinasyonu şart - Security scanning pipeline’ın bir parçası olmalı, sonradan eklenen bir adım değil
- Image tagging stratejisi traceability için kritik,
latesttag’ini production deployment’ında kullanmayın - Secret yönetimi için BuildKit’in mount özelliğini kullanın
En sık yapılan hata, bu kurulumları bir kere yapıp unutmak. Pipeline’larınızı düzenli olarak gözden geçirin, build sürelerini takip edin ve image boyutlarını monitör edin. CI/CD pipeline’ı da bir uygulama gibi bakım istiyor.
Sorularınız veya farklı yaklaşımlarınız varsa yorumlarda buluşalım.