GitHub Actions ile Docker İmajı Otomatik Build ve Push

Her sabah ofise gelip “acaba gece build bozuldu mu?” diye endişeyle Jenkins’e bakmak zorunda kalmak… O dönemi hatırlıyorum. Sonra GitHub Actions ile tanıştım ve hayatım değişti. Özellikle Docker imajı build edip registry’e push etme sürecini otomatikleştirdiğimde, “neden bunu daha önce yapmadım?” diye kendime sordum. Bu yazıda, GitHub Actions kullanarak Docker imajlarını nasıl build edip push edeceğinizi, gerçek dünya senaryolarıyla ve bol bol kod örneğiyle anlatacağım.

Neden GitHub Actions + Docker?

Çoğu ekip şu döngüde sıkışıp kalıyor: geliştirici kodu yazar, lokal ortamda test eder, sonra “bende çalışıyor abi” der ve production’a çıkmak için manuel adımlar başlar. Docker bu problemi büyük ölçüde çözdü ama imaj build edip push etmek hala manuel bir süreçti. GitHub Actions bu süreci tamamen otomatize ediyor.

GitHub Actions’ın öne çıkan avantajları:

  • Kodla birlikte yaşayan pipeline tanımları (YAML dosyaları repo’da)
  • GitHub ile native entegrasyon (PR, branch, tag olaylarını dinleyebilme)
  • Ücretsiz tier ile birçok küçük-orta proje için yeterli dakika
  • Docker Hub, GHCR (GitHub Container Registry), AWS ECR, GCP Artifact Registry gibi her major registry ile entegrasyon
  • Marketplace’te hazır action’ların bolluğu

Temel Yapı: Workflow Dosyası

GitHub Actions workflow’ları .github/workflows/ dizini altında YAML dosyaları olarak tanımlanır. Önce en basit haliyle başlayalım, sonra üzerine ekleyeceğiz.

mkdir -p .github/workflows
touch .github/workflows/docker-build-push.yml

İlk workflow dosyamız:

name: Docker Build and Push

on:
  push:
    branches:
      - main
  pull_request:
    branches:
      - main

jobs:
  build-and-push:
    runs-on: ubuntu-latest
    
    steps:
      - name: Checkout repository
        uses: actions/checkout@v4

      - name: Log in to Docker Hub
        uses: docker/login-action@v3
        with:
          username: ${{ secrets.DOCKERHUB_USERNAME }}
          password: ${{ secrets.DOCKERHUB_TOKEN }}

      - name: Build and push Docker image
        uses: docker/build-push-action@v5
        with:
          context: .
          push: true
          tags: kullaniciadiniz/uygulamaniz:latest

Bu en minimal hali. Ama “latest” tag’iyle çalışmak production’da ciddi sorunlara yol açar. Şimdi bunu düzeltelim.

Secret’ları Doğru Yapılandırmak

Workflow’a geçmeden önce secret’ları ayarlamak gerekiyor. GitHub repository sayfasında Settings > Secrets and variables > Actions yolunu izliyorsunuz.

Docker Hub için iki şey gerekiyor:

  • DOCKERHUB_USERNAME: Docker Hub kullanıcı adınız
  • DOCKERHUB_TOKEN: Docker Hub’dan oluşturduğunuz Access Token (şifrenizi değil, token kullanın!)

Docker Hub Access Token oluşturmak için: Hub.docker.com > Account Settings > Security > New Access Token.

GHCR (GitHub Container Registry) kullanıyorsanız işiniz daha kolay çünkü GITHUB_TOKEN otomatik olarak mevcut:

      - name: Log in to GitHub Container Registry
        uses: docker/login-action@v3
        with:
          registry: ghcr.io
          username: ${{ github.actor }}
          password: ${{ secrets.GITHUB_TOKEN }}

Akıllı Tag Stratejisi

“latest” tag’i kullanmak neden sorunlu? Çünkü hangi imajın production’da çalıştığını bilemezsiniz. Bir şeyler bozulduğunda “hangi versiyona döneceğiz?” sorusu cevaplanamaz hale gelir. Doğru yaklaşım şu:

name: Docker Build and Push

on:
  push:
    branches:
      - main
      - develop
    tags:
      - 'v*.*.*'
  pull_request:
    branches:
      - main

env:
  REGISTRY: ghcr.io
  IMAGE_NAME: ${{ github.repository }}

jobs:
  build-and-push:
    runs-on: ubuntu-latest
    permissions:
      contents: read
      packages: write

    steps:
      - name: Checkout repository
        uses: actions/checkout@v4

      - 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=semver,pattern={{version}}
            type=semver,pattern={{major}}.{{minor}}
            type=sha,prefix=sha-,format=short

      - name: Log in to GitHub Container Registry
        if: github.event_name != 'pull_request'
        uses: docker/login-action@v3
        with:
          registry: ${{ env.REGISTRY }}
          username: ${{ github.actor }}
          password: ${{ secrets.GITHUB_TOKEN }}

      - name: Build and push Docker image
        uses: docker/build-push-action@v5
        with:
          context: .
          push: ${{ github.event_name != 'pull_request' }}
          tags: ${{ steps.meta.outputs.tags }}
          labels: ${{ steps.meta.outputs.labels }}

docker/metadata-action bu işi inanılmaz kolaylaştırıyor. Yukarıdaki konfigürasyonla:

  • main branch’e push atıldığında: ghcr.io/orgadi/repo:main tag’i oluşur
  • v1.2.3 tag’i push edildiğinde: v1.2.3 ve 1.2 tag’leri oluşur
  • PR açıldığında: imaj build edilir ama push edilmez (test amaçlı)
  • Her commit için kısa SHA tag’i oluşur (örn: sha-abc1234)

Build Cache ile Süreyi Düşürmek

GitHub Actions’da her workflow çalışması temiz bir ortamda başlar. Bu güzel bir şey ama Docker build için dezavantaj: her seferinde her layer yeniden build edilir. Büyük projelerde bu 10-15 dakikayı bulabiliyor.

Cache kullanmak için:

      - name: Set up Docker Buildx
        uses: docker/setup-buildx-action@v3

      - name: Build and push Docker image
        uses: docker/build-push-action@v5
        with:
          context: .
          push: ${{ github.event_name != 'pull_request' }}
          tags: ${{ steps.meta.outputs.tags }}
          labels: ${{ steps.meta.outputs.labels }}
          cache-from: type=gha
          cache-to: type=gha,mode=max

type=gha (GitHub Actions Cache) kullanmak için docker/setup-buildx-action şart. Bu sayede layer cache’i GitHub Actions’ın kendi cache mekanizmasında tutuyor. İlk build uzun sürebilir ama sonraki build’ler dramatik biçimde kısalıyor.

Alternatif olarak registry cache de kullanılabilir:

          cache-from: type=registry,ref=ghcr.io/orgadi/repo:buildcache
          cache-to: type=registry,ref=ghcr.io/orgadi/repo:buildcache,mode=max

Multi-Platform Build (ARM + AMD64)

Apple Silicon Mac’lerin yaygınlaşmasıyla birlikte ARM imajlarına olan ihtiyaç arttı. Ayrıca AWS Graviton gibi ARM tabanlı sunucular maliyet avantajı sunuyor. GitHub Actions ile multi-platform build yapmak düşündüğünüzden kolay:

      - name: Set up QEMU
        uses: docker/setup-qemu-action@v3

      - name: Set up Docker Buildx
        uses: docker/setup-buildx-action@v3

      - name: Build and push multi-platform image
        uses: docker/build-push-action@v5
        with:
          context: .
          platforms: linux/amd64,linux/arm64
          push: ${{ github.event_name != 'pull_request' }}
          tags: ${{ steps.meta.outputs.tags }}
          labels: ${{ steps.meta.outputs.labels }}
          cache-from: type=gha
          cache-to: type=gha,mode=max

QEMU, QEMU emülatörünü kuruyor ve farklı mimarileri simüle ediyor. Native ARM runner’ınız yoksa emülasyon yavaş olabilir ama küçük-orta imajlar için kabul edilebilir.

Gerçek Dünya Senaryosu: Node.js API Projesi

Şimdi gerçek bir senaryo üzerinden gidelim. Bir Node.js API projeniz var, staging ve production ortamlarınız var, ve şu kuralları uygulamak istiyorsunuz:

  • develop branch’e her push’ta staging registry’ye imaj gönder
  • main branch’e merge olduğunda production registry’ye gönder
  • Semantic versioning tag’i push edildiğinde production’a “release” imajı gönder
  • Test geçmeden push yapma
name: CI/CD Pipeline

on:
  push:
    branches:
      - main
      - develop
    tags:
      - 'v*.*.*'
  pull_request:
    branches:
      - main
      - develop

env:
  REGISTRY: ghcr.io
  IMAGE_NAME: ${{ github.repository }}

jobs:
  test:
    runs-on: ubuntu-latest
    steps:
      - name: Checkout
        uses: actions/checkout@v4

      - name: Setup Node.js
        uses: actions/setup-node@v4
        with:
          node-version: '20'
          cache: 'npm'

      - name: Install dependencies
        run: npm ci

      - name: Run tests
        run: npm test

      - name: Run linting
        run: npm run lint

  build-and-push:
    needs: test
    runs-on: ubuntu-latest
    if: github.event_name != 'pull_request'
    permissions:
      contents: read
      packages: write

    steps:
      - name: Checkout repository
        uses: actions/checkout@v4

      - name: Set up Docker Buildx
        uses: docker/setup-buildx-action@v3

      - name: Log in to GitHub Container Registry
        uses: docker/login-action@v3
        with:
          registry: ${{ env.REGISTRY }}
          username: ${{ github.actor }}
          password: ${{ secrets.GITHUB_TOKEN }}

      - name: Extract metadata
        id: meta
        uses: docker/metadata-action@v5
        with:
          images: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}
          tags: |
            type=ref,event=branch
            type=semver,pattern={{version}}
            type=semver,pattern={{major}}.{{minor}}
            type=sha,prefix=git-

      - name: Build and push
        uses: docker/build-push-action@v5
        with:
          context: .
          push: true
          tags: ${{ steps.meta.outputs.tags }}
          labels: ${{ steps.meta.outputs.labels }}
          build-args: |
            BUILD_DATE=${{ github.event.repository.updated_at }}
            GIT_COMMIT=${{ github.sha }}
          cache-from: type=gha
          cache-to: type=gha,mode=max

needs: test satırı kritik. Bu sayede test job’u başarısız olursa build job’u hiç başlamıyor. Kırık imaj registry’ye gitmemiş oluyor.

Build Arguments ve Secrets

Dockerfile içine build zamanında değer geçirmek çok yaygın bir ihtiyaç. Özellikle versiyon numarası, git commit hash veya ortam bilgisi gibi şeyler için:

# Dockerfile örneği
FROM node:20-alpine

ARG BUILD_DATE
ARG GIT_COMMIT
ARG APP_VERSION=unknown

LABEL org.opencontainers.image.created=$BUILD_DATE
LABEL org.opencontainers.image.revision=$GIT_COMMIT
LABEL org.opencontainers.image.version=$APP_VERSION

WORKDIR /app
COPY package*.json ./
RUN npm ci --only=production
COPY . .

ENV APP_VERSION=$APP_VERSION
EXPOSE 3000
CMD ["node", "server.js"]

Workflow’da build-args kullanımı:

      - name: Build and push
        uses: docker/build-push-action@v5
        with:
          context: .
          push: true
          tags: ${{ steps.meta.outputs.tags }}
          build-args: |
            BUILD_DATE=${{ github.event.repository.updated_at }}
            GIT_COMMIT=${{ github.sha }}
            APP_VERSION=${{ steps.meta.outputs.version }}
          secrets: |
            NPM_TOKEN=${{ secrets.NPM_TOKEN }}

Build sırasında secret kullanmak için secrets parametresi var. Bu secret’lar imaj içine gömülmüyor, sadece build zamanında kullanılıyor. Dockerfile’da şöyle kullanıyorsunuz:

RUN --mount=type=secret,id=NPM_TOKEN 
    NPM_TOKEN=$(cat /run/secrets/NPM_TOKEN) 
    npm install --save-dev @ozel/paket

Güvenlik Taraması Eklemek

İmajı push etmeden önce güvenlik açığı taraması yapmak production ortamları için neredeyse zorunlu. Trivy ile bunu pipeline’a entegre etmek çok kolay:

  security-scan:
    needs: build-and-push
    runs-on: ubuntu-latest
    permissions:
      security-events: write

    steps:
      - name: Run Trivy vulnerability scanner
        uses: aquasecurity/trivy-action@master
        with:
          image-ref: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}:main
          format: 'sarif'
          output: 'trivy-results.sarif'
          severity: 'CRITICAL,HIGH'
          exit-code: '1'

      - name: Upload Trivy scan results to GitHub Security tab
        uses: github/codeql-action/upload-sarif@v3
        if: always()
        with:
          sarif_file: 'trivy-results.sarif'

exit-code: '1' ayarı kritik. Bu sayede CRITICAL veya HIGH bir güvenlik açığı bulunursa pipeline başarısız oluyor. Sonuçlar GitHub’ın Security sekmesinde görünüyor, PR’larda warning olarak çıkıyor.

AWS ECR ile Entegrasyon

Bazı ekipler Docker Hub veya GHCR yerine AWS ECR kullanıyor. Bu durumda authentication biraz farklı işliyor:

      - name: Configure AWS credentials
        uses: aws-actions/configure-aws-credentials@v4
        with:
          aws-access-key-id: ${{ secrets.AWS_ACCESS_KEY_ID }}
          aws-secret-access-key: ${{ secrets.AWS_SECRET_ACCESS_KEY }}
          aws-region: eu-west-1

      - name: Login to Amazon ECR
        id: login-ecr
        uses: aws-actions/amazon-ecr-login@v2

      - name: Build and push to ECR
        uses: docker/build-push-action@v5
        with:
          context: .
          push: true
          tags: |
            ${{ steps.login-ecr.outputs.registry }}/uygulamaniz:latest
            ${{ steps.login-ecr.outputs.registry }}/uygulamaniz:${{ github.sha }}
          cache-from: type=gha
          cache-to: type=gha,mode=max

AWS IAM için en az yetkili kullanıcı oluşturmayı unutmayın. ECR push için gereken minimum yetkiler:

  • ecr:GetAuthorizationToken: ECR token almak için
  • ecr:BatchCheckLayerAvailability: Layer kontrolü
  • ecr:PutImage: İmaj push etmek
  • ecr:InitiateLayerUpload: Layer upload başlatmak
  • ecr:UploadLayerPart: Layer parçası yüklemek
  • ecr:CompleteLayerUpload: Layer upload tamamlamak

Matrix Build ile Farklı Versiyonlar

Birden fazla Node.js veya Python versiyonu için imaj build etmek gerektiğinde matrix stratejisi kullanabilirsiniz:

jobs:
  build-matrix:
    runs-on: ubuntu-latest
    strategy:
      matrix:
        node-version: ['18', '20', '21']
        fail-fast: false

    steps:
      - name: Checkout
        uses: actions/checkout@v4

      - name: Set up Docker Buildx
        uses: docker/setup-buildx-action@v3

      - name: Login to GHCR
        uses: docker/login-action@v3
        with:
          registry: ghcr.io
          username: ${{ github.actor }}
          password: ${{ secrets.GITHUB_TOKEN }}

      - name: Build and push
        uses: docker/build-push-action@v5
        with:
          context: .
          push: true
          build-args: |
            NODE_VERSION=${{ matrix.node-version }}
          tags: |
            ghcr.io/${{ github.repository }}:node${{ matrix.node-version }}
            ghcr.io/${{ github.repository }}:node${{ matrix.node-version }}-${{ github.sha }}
          cache-from: type=gha,scope=node-${{ matrix.node-version }}
          cache-to: type=gha,scope=node-${{ matrix.node-version }},mode=max

Matrix build’de cache scope’u belirtmek önemli. Aksi halde farklı versiyonların cache’leri birbirini eziyor.

Yaygın Sorunlar ve Çözümleri

Pratikte karşılaşılan sorunlar ve çözümleri:

Build çok uzun sürüyor:

  • cache-from/cache-to ekleyin
  • Dockerfile’ınızı optimize edin (sık değişen COPY komutlarını sona alın)
  • Multi-stage build kullanın

“denied: requested access to the resource is denied” hatası:

  • Secret değerlerini kontrol edin
  • GHCR için packages: write permission’ı ekleyin
  • Docker Hub için şifre yerine Access Token kullanın

“no space left on device” hatası:

  • docker/setup-buildx-action‘a driver-opts: image=moby/buildkit:latest ekleyin
  • Büyük base image kullanıyorsanız runner’ın disk kapasitesini kontrol edin

PR’larda imaj push edilmek istenmiyor ama edilmesi gerekiyor:

  • if: github.event_name != 'pull_request' koşulunu kullanın
  • Ya da PR’lar için farklı bir registry veya tag yapısı belirleyin

Bildirimler ve Monitoring

Pipeline başarısız olduğunda haberdar olmak için Slack veya Teams entegrasyonu ekleyebilirsiniz:

      - name: Slack Bildirimi
        if: failure()
        uses: slackapi/[email protected]
        with:
          payload: |
            {
              "text": "Docker build basarisiz oldu!",
              "blocks": [
                {
                  "type": "section",
                  "text": {
                    "type": "mrkdwn",
                    "text": "*${{ github.repository }}* - Build BASARISIZnBranch: `${{ github.ref_name }}`nCommit: `${{ github.sha }}`n<${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }}|Workflow'u Goruntule>"
                  }
                }
              ]
            }
        env:
          SLACK_WEBHOOK_URL: ${{ secrets.SLACK_WEBHOOK_URL }}
          SLACK_WEBHOOK_TYPE: INCOMING_WEBHOOK

Sonuç

GitHub Actions ile Docker build ve push sürecini otomatikleştirmek başlangıçta karmaşık görünebilir ama bir kez kurduğunuzda “nasıl bu olmadan çalışıyordum?” diyeceksiniz. Özetlemek gerekirse:

  • “latest” yerine anlamlı tag stratejisi kullanın (semver, branch adı, git SHA kombinasyonu)
  • Build cache eklemek build sürelerini dramatik biçimde düşürür
  • Test job’u başarısız olursa build job’un çalışmamasını sağlayın (needs kullanın)
  • PR’larda build yapın ama push etmeyin, böylece kırık kod registry’ye gitmez
  • Trivy veya benzeri bir araçla güvenlik taraması ekleyin
  • Secret’lar için her zaman GitHub Secrets kullanın, workflow dosyasına asla hardcode etmeyin

Multi-platform build, matrix stratejisi ve güvenlik taraması gibi ileri seviye konulara ihtiyaç duyup duymadığınız ekip büyüklüğünüze ve projenizin kritikliğine göre değişir. Ama temel yapıyı doğru kurmak, üzerine her şeyi inşa etmeyi kolaylaştırıyor. Küçük başlayın, giderek iyileştirin. Bu işin püf noktası bu zaten.

Bir yanıt yazın

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