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:
.dockerignoredosyası oluştur:node_modules,.git,*.loggibi 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ı.
