Docker Build ve Deploy Pipeline: Jenkins ile CI/CD Rehberi

Üretim ortamında Docker imajlarını elle build edip deploy etmek, bir süre sonra insanı delirtecek kadar yorucu bir hal alıyor. Bir dosyayı değiştirdin, sunucuya SSH’ladın, docker build çalıştırdın, tag’ledin, registry’ye push ettin, sonra her sunucuda pull yaptın… Bu döngü günde onlarca kez tekrarlanabilir. Jenkins tam da bu noktada devreye giriyor ve tüm bu süreci otomatize ederek seni hem zamandan hem de insan hatasından kurtarıyor. Bu yazıda sıfırdan bir Jenkins kurulumu yapıp, Docker build ve deploy pipeline’ını production-ready şekilde nasıl kuracağını anlatacağım.

Jenkins Kurulumu: Docker ile Başlayalım

Jenkins’i bare-metal’a kurmak yerine Docker üzerinde çalıştırmak çok daha mantıklı. Hem taşınabilirlik açısından hem de yönetim kolaylığı açısından bu yaklaşım önerilen yol.

Önce Jenkins için gerekli dizinleri oluşturalım:

mkdir -p /opt/jenkins/data
chown -R 1000:1000 /opt/jenkins/data

Jenkins’i Docker Compose ile ayağa kaldırmak için bir docker-compose.yml dosyası oluşturalım:

cat > /opt/jenkins/docker-compose.yml << 'EOF'
version: '3.8'

services:
  jenkins:
    image: jenkins/jenkins:lts-jdk17
    container_name: jenkins
    restart: unless-stopped
    privileged: true
    user: root
    ports:
      - "8080:8080"
      - "50000:50000"
    volumes:
      - /opt/jenkins/data:/var/jenkins_home
      - /var/run/docker.sock:/var/run/docker.sock
      - /usr/bin/docker:/usr/bin/docker
    environment:
      - JAVA_OPTS=-Djenkins.install.runSetupWizard=false
    networks:
      - jenkins-net

networks:
  jenkins-net:
    driver: bridge
EOF

/var/run/docker.sock mount etmek, Jenkins container’ının host üzerindeki Docker daemon’ını kullanmasını sağlıyor. Bu yaklaşım “Docker-in-Docker” (DinD) değil, “Docker-outside-of-Docker” (DooD) olarak geçiyor ve production’da çok daha stabil çalışıyor.

cd /opt/jenkins
docker compose up -d
docker compose logs -f jenkins

İlk kurulumda admin şifresini şöyle alabilirsin:

docker exec jenkins cat /var/jenkins_home/secrets/initialAdminPassword

Jenkins arayüzüne http://sunucu-ip:8080 adresinden ulaştıktan sonra önerilen plugin’leri kur. Ek olarak şu plugin’leri mutlaka yüklemen gerekiyor:

  • Docker Pipeline: Pipeline içinde Docker komutlarını kullanmak için
  • Docker Commons: Docker entegrasyonu için temel kütüphane
  • Blue Ocean: Görsel pipeline takibi için
  • Credentials Binding: Hassas bilgileri güvenli saklamak için
  • Git: Repository entegrasyonu için
  • Pipeline: Temel pipeline desteği

Jenkins Credentials Yapılandırması

Pipeline’da Docker Hub veya private registry’ye push yapabilmek için credentials’ları Jenkins’e tanıtman gerekiyor. Manage Jenkins > Credentials > System > Global credentials yolunu takip et.

Aşağıdaki credential’ları ekle:

  • docker-hub-credentials: Docker Hub kullanıcı adı ve şifresi, tip “Username with password”
  • github-credentials: GitHub token veya SSH key, tip “Username with password” veya “SSH Username with private key”
  • registry-url: Private registry adresi, tip “Secret text”

İlk Jenkinsfile: Temel Docker Build Pipeline

Projenin root dizininde bir Jenkinsfile oluşturarak başlayalım. Bu örnek bir Node.js uygulaması için ama aynı yapıyı herhangi bir uygulama için kullanabilirsin:

pipeline {
    agent any

    environment {
        DOCKER_IMAGE = 'kullanici-adi/uygulama-adi'
        DOCKER_TAG = "${BUILD_NUMBER}"
        REGISTRY_CREDENTIALS = 'docker-hub-credentials'
    }

    stages {
        stage('Checkout') {
            steps {
                checkout scm
                sh 'git log --oneline -5'
            }
        }

        stage('Build Docker Image') {
            steps {
                script {
                    dockerImage = docker.build("${DOCKER_IMAGE}:${DOCKER_TAG}")
                }
            }
        }

        stage('Test') {
            steps {
                script {
                    dockerImage.inside {
                        sh 'npm test'
                    }
                }
            }
        }

        stage('Push to Registry') {
            steps {
                script {
                    docker.withRegistry('https://registry.hub.docker.com', REGISTRY_CREDENTIALS) {
                        dockerImage.push("${DOCKER_TAG}")
                        dockerImage.push('latest')
                    }
                }
            }
        }

        stage('Cleanup') {
            steps {
                sh "docker rmi ${DOCKER_IMAGE}:${DOCKER_TAG} || true"
            }
        }
    }

    post {
        success {
            echo "Build ${BUILD_NUMBER} basariyla tamamlandi!"
        }
        failure {
            echo "Build ${BUILD_NUMBER} basarisiz oldu!"
        }
    }
}

Multi-Environment Deploy Pipeline

Gerçek dünyada genellikle development, staging ve production ortamları oluyor. Her ortam için farklı konfigürasyonlar ve deploy stratejileri gerekiyor. Aşağıdaki pipeline bu senaryoyu ele alıyor:

pipeline {
    agent any

    parameters {
        choice(
            name: 'DEPLOY_ENV',
            choices: ['dev', 'staging', 'production'],
            description: 'Deploy edilecek ortami secin'
        )
        booleanParam(
            name: 'SKIP_TESTS',
            defaultValue: false,
            description: 'Testleri atla (onerilen degil!)'
        )
    }

    environment {
        APP_NAME = 'myapp'
        DOCKER_REGISTRY = 'registry.sirketim.com:5000'
        IMAGE_TAG = "${BUILD_NUMBER}-${GIT_COMMIT.take(7)}"
        FULL_IMAGE = "${DOCKER_REGISTRY}/${APP_NAME}:${IMAGE_TAG}"
    }

    stages {
        stage('Kod Cek') {
            steps {
                checkout scm
                script {
                    env.GIT_COMMIT_MSG = sh(
                        script: 'git log -1 --pretty=%B',
                        returnStdout: true
                    ).trim()
                }
                echo "Son commit: ${env.GIT_COMMIT_MSG}"
            }
        }

        stage('Docker Build') {
            steps {
                script {
                    sh """
                        docker build 
                            --build-arg BUILD_DATE=$(date -u +'%Y-%m-%dT%H:%M:%SZ') 
                            --build-arg VCS_REF=${GIT_COMMIT} 
                            --build-arg VERSION=${IMAGE_TAG} 
                            -t ${FULL_IMAGE} 
                            -f Dockerfile .
                    """
                }
            }
        }

        stage('Guvenik Tarama') {
            when {
                expression { params.DEPLOY_ENV == 'production' }
            }
            steps {
                sh "docker run --rm -v /var/run/docker.sock:/var/run/docker.sock aquasec/trivy image --exit-code 1 --severity HIGH,CRITICAL ${FULL_IMAGE} || true"
            }
        }

        stage('Test') {
            when {
                expression { !params.SKIP_TESTS }
            }
            steps {
                sh """
                    docker run --rm 
                        --network=host 
                        -e NODE_ENV=test 
                        ${FULL_IMAGE} npm test
                """
            }
        }

        stage('Registry Push') {
            steps {
                withCredentials([usernamePassword(
                    credentialsId: 'registry-credentials',
                    usernameVariable: 'REGISTRY_USER',
                    passwordVariable: 'REGISTRY_PASS'
                )]) {
                    sh """
                        echo $REGISTRY_PASS | docker login ${DOCKER_REGISTRY} -u $REGISTRY_USER --password-stdin
                        docker push ${FULL_IMAGE}
                        docker logout ${DOCKER_REGISTRY}
                    """
                }
            }
        }

        stage('Deploy') {
            steps {
                script {
                    if (params.DEPLOY_ENV == 'production') {
                        input message: "Production deploy onayi bekleniyor. Devam edilsin mi?", ok: 'Evet, deploy et'
                    }
                }
                sh "./scripts/deploy.sh ${params.DEPLOY_ENV} ${FULL_IMAGE}"
            }
        }
    }

    post {
        always {
            sh "docker rmi ${FULL_IMAGE} || true"
        }
        success {
            slackSend(
                color: 'good',
                message: "Basarili deploy: ${APP_NAME} v${IMAGE_TAG} -> ${params.DEPLOY_ENV}"
            )
        }
        failure {
            slackSend(
                color: 'danger',
                message: "BASARISIZ: ${APP_NAME} build/deploy islemi hata verdi! Build: ${BUILD_URL}"
            )
        }
    }
}

Deploy Script’i

Yukardaki pipeline’da çağrılan scripts/deploy.sh dosyasını da oluşturmamız gerekiyor. Bu script, hedef sunuculara SSH ile bağlanıp container’ı güncelliyor:

#!/bin/bash
set -euo pipefail

ENVIRONMENT=$1
IMAGE=$2

# Ortama gore hedef sunucuları belirle
case "$ENVIRONMENT" in
    dev)
        HOSTS=("dev-server-01")
        COMPOSE_FILE="docker-compose.dev.yml"
        ;;
    staging)
        HOSTS=("staging-server-01")
        COMPOSE_FILE="docker-compose.staging.yml"
        ;;
    production)
        HOSTS=("prod-server-01" "prod-server-02" "prod-server-03")
        COMPOSE_FILE="docker-compose.prod.yml"
        ;;
    *)
        echo "Hata: Gecersiz ortam '$ENVIRONMENT'"
        exit 1
        ;;
esac

echo "=== Deploy basladi: $ENVIRONMENT ortamina $IMAGE deploy ediliyor ==="

for HOST in "${HOSTS[@]}"; do
    echo "-> $HOST sunucusuna deploy ediliyor..."

    ssh -o StrictHostKeyChecking=no deploy@$HOST << EOF
        # Yeni imaji cek
        docker pull $IMAGE

        # Eski container'i durdur ve yenisini baslat
        export APP_IMAGE=$IMAGE
        docker compose -f /opt/app/$COMPOSE_FILE pull
        docker compose -f /opt/app/$COMPOSE_FILE up -d --no-deps app

        # Health check
        sleep 10
        if curl -sf http://localhost:8080/health > /dev/null; then
            echo "Health check basarili: $HOST"
        else
            echo "HATA: Health check basarisiz: $HOST"
            docker compose -f /opt/app/$COMPOSE_FILE rollback
            exit 1
        fi

        # Eski imajlari temizle
        docker image prune -f
EOF

    echo "-> $HOST deploy tamamlandi"
done

echo "=== Tum sunuculara deploy tamamlandi ==="

Private Registry Kurulumu

Docker Hub yerine kendi registry’nizi kurmak hem maliyet hem de güvenlik açısından avantajlı. Basit bir private registry kurulumu:

cat > /opt/registry/docker-compose.yml << 'EOF'
version: '3.8'

services:
  registry:
    image: registry:2
    container_name: docker-registry
    restart: unless-stopped
    ports:
      - "5000:5000"
    volumes:
      - /opt/registry/data:/var/lib/registry
      - /opt/registry/config.yml:/etc/docker/registry/config.yml
      - /opt/registry/certs:/certs
    environment:
      REGISTRY_HTTP_TLS_CERTIFICATE: /certs/domain.crt
      REGISTRY_HTTP_TLS_KEY: /certs/domain.key
      REGISTRY_AUTH: htpasswd
      REGISTRY_AUTH_HTPASSWD_PATH: /auth/htpasswd
      REGISTRY_AUTH_HTPASSWD_REALM: Registry Realm

  registry-ui:
    image: joxit/docker-registry-ui:latest
    container_name: registry-ui
    restart: unless-stopped
    ports:
      - "8888:80"
    environment:
      - REGISTRY_URL=https://registry.sirketim.com:5000
      - REGISTRY_TITLE=Sirket Registry
    depends_on:
      - registry
EOF

Registry için htpasswd dosyası oluştur:

mkdir -p /opt/registry/auth
docker run --rm --entrypoint htpasswd httpd:2 -Bbn kullanici sifre > /opt/registry/auth/htpasswd

Jenkins Agent ile Paralel Build

Büyük projelerde tek bir Jenkins master üzerinde build yapmak dar boğaz yaratır. Jenkins agent’ları devreye alarak paralel build yapabiliriz:

pipeline {
    agent none

    stages {
        stage('Paralel Build') {
            parallel {
                stage('Build AMD64') {
                    agent {
                        docker {
                            image 'docker:24-dind'
                            args '--privileged -v /var/run/docker.sock:/var/run/docker.sock'
                        }
                    }
                    steps {
                        sh "docker buildx build --platform linux/amd64 -t ${DOCKER_IMAGE}:${BUILD_NUMBER}-amd64 --push ."
                    }
                }

                stage('Build ARM64') {
                    agent {
                        label 'arm64-agent'
                    }
                    steps {
                        sh "docker buildx build --platform linux/arm64 -t ${DOCKER_IMAGE}:${BUILD_NUMBER}-arm64 --push ."
                    }
                }

                stage('Unit Tests') {
                    agent {
                        docker {
                            image 'node:20-alpine'
                        }
                    }
                    steps {
                        sh 'npm ci'
                        sh 'npm run test:unit'
                    }
                }
            }
        }

        stage('Manifest Olustur') {
            agent any
            steps {
                script {
                    withCredentials([usernamePassword(credentialsId: 'docker-hub-credentials', usernameVariable: 'USER', passwordVariable: 'PASS')]) {
                        sh """
                            echo $PASS | docker login -u $USER --password-stdin
                            docker manifest create ${DOCKER_IMAGE}:${BUILD_NUMBER} 
                                ${DOCKER_IMAGE}:${BUILD_NUMBER}-amd64 
                                ${DOCKER_IMAGE}:${BUILD_NUMBER}-arm64
                            docker manifest push ${DOCKER_IMAGE}:${BUILD_NUMBER}
                        """
                    }
                }
            }
        }
    }
}

Webhook ile Otomatik Tetikleme

Pipeline’ı her commit’te otomatik çalıştırmak için GitHub/GitLab webhook’u kurmak gerekiyor.

GitHub tarafında:

  • Repository Settings > Webhooks > Add webhook
  • Payload URL: http://jenkins-sunucusu:8080/github-webhook/
  • Content type: application/json
  • Events: “Just the push event” seç

Jenkins tarafında Multibranch Pipeline kullanıyorsan şu konfigürasyonu Jenkinsfile‘a ekle:

pipeline {
    agent any

    triggers {
        githubPush()
    }

    options {
        buildDiscarder(logRotator(numToKeepStr: '10'))
        timeout(time: 30, unit: 'MINUTES')
        disableConcurrentBuilds()
    }

    stages {
        stage('Branch Kontrol') {
            steps {
                script {
                    if (env.BRANCH_NAME == 'main') {
                        env.DEPLOY_TARGET = 'production'
                    } else if (env.BRANCH_NAME == 'develop') {
                        env.DEPLOY_TARGET = 'staging'
                    } else {
                        env.DEPLOY_TARGET = 'dev'
                    }
                    echo "Branch: ${env.BRANCH_NAME} -> Deploy: ${env.DEPLOY_TARGET}"
                }
            }
        }
    }
}

Sık Karşılaşılan Sorunlar ve Çözümleri

Docker socket izin hatası:

# Jenkins kullanicisini docker grubuna ekle
usermod -aG docker jenkins
# Veya container icin
docker exec -u root jenkins chmod 666 /var/run/docker.sock

Build cache’ini etkin kullanmak:

# Dockerfile'da layer sirasini optimize et
# Once bagimlilik dosyalarini kopyala, sonra kaynak kodu
COPY package*.json ./
RUN npm ci --only=production
COPY . .

# Jenkins'te build cache kullanimi
docker build --cache-from ${DOCKER_IMAGE}:latest -t ${DOCKER_IMAGE}:${BUILD_NUMBER} .

Pipeline’da environment variable’ları güvenli kullanmak:

# Asla boyle yapma:
# sh "docker login -u kullanici -p sifre"

# Her zaman boyle yap:
withCredentials([usernamePassword(credentialsId: 'my-creds', usernameVariable: 'USER', passwordVariable: 'PASS')]) {
    sh 'echo $PASS | docker login -u $USER --password-stdin'
}

Pipeline Optimizasyonu

Uzun süren build’leri hızlandırmak için birkaç pratik ipucu:

  • .dockerignore dosyası oluştur: node_modules, .git, *.log gibi gereksiz dosyaların image’a girmesini engelle. Build context küçülür, build hızlanır.
  • Multi-stage build kullan: Development bağımlılıklarını production image’ına taşıma.
  • Layer cache’ini koru: Sık değişmeyen dosyaları (bağımlılıklar) Dockerfile’ın başına, sık değişenleri (kaynak kodu) sonuna koy.
  • Paralel stage’leri kullan: Birbirinden bağımsız test süitlerini aynı anda çalıştır.
  • Build agent’larını ayrıştır: Test ve build işlemlerini farklı agent’larda yürüt.

Sonuç

Jenkins ile Docker pipeline kurulumu başta karmaşık görünse de doğru yapılandırıldığında son derece güçlü ve güvenilir bir CI/CD altyapısı oluşturuyor. Bu yazıda anlattıklarımızı özetleyecek olursam: Jenkins’i Docker üzerinde çalıştırmak yönetimi kolaylaştırıyor, Jenkinsfile‘ı versiyon kontrol sistemine koymak pipeline’ı kod gibi yönetmeyi sağlıyor, multi-environment yapısı staging-production ayrımını netleştiriyor ve paralel build’ler ciddi zaman kazandırıyor.

Başlangıç için en basit pipeline’ı kur, çalıştır ve sonra ihtiyaçlarına göre genişlet. Production ortamında güvenlik tarama adımını, input onayını ve health check mekanizmalarını kesinlikle atlatma. İnsan hatası en çok bu adımların atlandığı anlarda ortaya çıkıyor.

Bir sonraki adım olarak Kubernetes ile entegrasyon, ArgoCD ile GitOps yaklaşımı veya Jenkins X’e geçiş incelenebilir. Ama bunların hepsi sağlam bir temel üzerine kurulabiliyor, o yüzden burada anlattıklarını sağlamlaştırmak her zaman öncelikli olmalı.

Bir yanıt yazın

E-posta adresiniz yayınlanmayacak. Gerekli alanlar * ile işaretlenmişlerdir