VPS’e Otomatik Deployment: GitHub Actions ile CI/CD Rehberi

Kod yazmaya başlamadan önce sunucuya her değişikliği elle göndermekten sıkıldıysanız, doğru yere geldiniz. GitHub Actions ile bu süreci tamamen otomatize edebilir, bir git push ile deploymentı tetikleyebilirsiniz. Bu rehberde gerçek bir VPS üzerinde, sıfırdan çalışan bir CI/CD pipeline kuruyoruz.

GitHub Actions Nedir ve Neden Kullanmalısınız

GitHub Actions, GitHub’ın kendi bünyesindeki CI/CD servisidir. Özel bir araç kurmanıza gerek yoktur, repository’nizin içinde .github/workflows/ klasörü oluşturmanız yeterlidir. Her push, pull request veya schedule tetikleyicisinde otomatik olarak devreye girer.

Klasik deployment akışı şöyle işler: Siz kodu yazar, GitHub’a push edersiniz. GitHub Actions bunu algılar, testleri çalıştırır, ardından SSH üzerinden VPS’inize bağlanarak yeni kodu çeker ve servisi yeniden başlatır. Tüm bu adımlar siz kahvenizi içerken gerçekleşir.

Neden başka araçlar yerine GitHub Actions? Jenkins kurmak için ayrı bir sunucu, bakım ve zaman ister. GitLab CI zaten GitLab kullanıyorsanız mantıklıdır. Ama projeniz GitHub’daysa, Actions en az sürtüşmeli seçenektir. Ücretsiz plan ayda 2000 dakika sunar, küçük ve orta ölçekli projeler için bu fazlasıyla yeterlidir.

VPS Tarafında Ön Hazırlıklar

Pipeline’ı yazmadan önce sunucu tarafını hazırlamanız gerekiyor. Bu adımları atlarsanız sonradan hata ayıklamakla saatler geçirirsiniz.

Deployment Kullanıcısı Oluşturma

Root ile deployment yapmak kötü bir pratiktir. Bunun yerine yalnızca gerekli izinlere sahip ayrı bir kullanıcı oluşturun.

# Sunucuda çalıştırın
sudo adduser deployer
sudo usermod -aG sudo deployer

# Eğer nginx veya apache ile çalışıyorsanız
sudo usermod -aG www-data deployer

SSH Anahtar Çifti Oluşturma

GitHub Actions, sunucunuza parola sormadan bağlanabilmek için SSH anahtarına ihtiyaç duyar. Bu anahtarı yerel makinenizde oluşturun, private key GitHub’a, public key sunucuya gidecek.

# Yerel makinenizde çalıştırın
ssh-keygen -t ed25519 -C "github-actions-deploy" -f ~/.ssh/github_actions_deploy

# Bu komut iki dosya üretir:
# ~/.ssh/github_actions_deploy      -> Private key (GitHub'a eklenecek)
# ~/.ssh/github_actions_deploy.pub  -> Public key (Sunucuya eklenecek)

Public key’i sunucuya ekleyin:

# Sunucuda deployer kullanıcısı olarak çalıştırın
mkdir -p ~/.ssh
chmod 700 ~/.ssh
nano ~/.ssh/authorized_keys
# Yukarıda üretilen .pub dosyasının içeriğini buraya yapıştırın
chmod 600 ~/.ssh/authorized_keys

Bağlantıyı test edin:

ssh -i ~/.ssh/github_actions_deploy deployer@sunucu_ip_adresiniz

Eğer bağlantı başarılıysa bir sonraki adıma geçebilirsiniz.

GitHub Secrets Ayarlama

Private key ve diğer hassas bilgileri asla workflow dosyasına yazmayın. Bunlar için GitHub Secrets kullanın.

Repository sayfanızda Settings > Secrets and variables > Actions yolunu takip edin. Şu secret’ları ekleyin:

  • SSH_PRIVATE_KEY: ~/.ssh/github_actions_deploy dosyasının tüm içeriği (—–BEGIN ile başlayan satırlar dahil)
  • VPS_HOST: Sunucunuzun IP adresi veya domain adı
  • VPS_USER: deployer
  • VPS_PORT: Genellikle 22, özel port kullanıyorsanız onu yazın

İlk Workflow Dosyası: Basit Deployment

Artık temel bir workflow yazabiliriz. Bu örnek, main branch’e her push yapıldığında çalışır.

# .github/workflows/deploy.yml
name: Deploy to VPS

on:
  push:
    branches:
      - main

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

      - name: Setup SSH
        env:
          SSH_PRIVATE_KEY: ${{ secrets.SSH_PRIVATE_KEY }}
        run: |
          mkdir -p ~/.ssh
          echo "$SSH_PRIVATE_KEY" > ~/.ssh/deploy_key
          chmod 600 ~/.ssh/deploy_key
          ssh-keyscan -H ${{ secrets.VPS_HOST }} >> ~/.ssh/known_hosts

      - name: Deploy to server
        run: |
          ssh -i ~/.ssh/deploy_key 
              -p ${{ secrets.VPS_PORT }} 
              ${{ secrets.VPS_USER }}@${{ secrets.VPS_HOST }} 
              'cd /var/www/uygulamam && git pull origin main && npm install && pm2 restart uygulamam'

Bu workflow birkaç şey yapıyor: SSH bağlantısını kuruyor, sunucuya bağlanıyor, proje klasörüne gidiyor, yeni kodu çekiyor ve servisi yeniden başlatıyor. Node.js uygulaması için PM2 kullanan bir senaryo bu, ama aynı mantığı Python veya PHP için de uygulayabilirsiniz.

Gerçek Dünya Senaryosu: Node.js Uygulaması

Daha kapsamlı bir örneğe geçelim. Bu sefer testleri çalıştırıyor, başarısız olursa deploy etmiyoruz.

# .github/workflows/deploy-nodejs.yml
name: Test and Deploy Node.js App

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

jobs:
  test:
    runs-on: ubuntu-latest
    
    steps:
      - 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 linter
        run: npm run lint

  deploy:
    needs: test
    runs-on: ubuntu-latest
    if: github.ref == 'refs/heads/main' && github.event_name == 'push'
    
    steps:
      - uses: actions/checkout@v4
      
      - name: Setup SSH
        run: |
          mkdir -p ~/.ssh
          echo "${{ secrets.SSH_PRIVATE_KEY }}" > ~/.ssh/deploy_key
          chmod 600 ~/.ssh/deploy_key
          ssh-keyscan -H ${{ secrets.VPS_HOST }} >> ~/.ssh/known_hosts
      
      - name: Deploy application
        run: |
          ssh -i ~/.ssh/deploy_key 
              ${{ secrets.VPS_USER }}@${{ secrets.VPS_HOST }} << 'ENDSSH'
            set -e
            cd /var/www/uygulamam
            git fetch origin main
            git reset --hard origin/main
            npm ci --production
            pm2 reload uygulamam --update-env
            echo "Deployment tamamlandi: $(date)"
          ENDSSH

Burada dikkat edilmesi gereken birkaç nokta var. needs: test satırı, deploy job’ının test job’ı başarıyla tamamlanmadan çalışmamasını sağlıyor. if koşulu ise yalnızca main branch’e yapılan push’larda deploy’ın tetiklenmesini garanti ediyor. Pull request açıldığında testler çalışır ama deploy yapılmaz.

Sunucuda git reset --hard kullanmak git pull‘dan daha güvenlidir. Sunucuda birileri manuel değişiklik yapmışsa git pull çakışma çıkarabilir, reset --hard ise her zaman repository’deki duruma döner.

Docker ile Deployment

Eğer uygulamanızı Docker ile çalıştırıyorsanız, workflow biraz farklı şekilleniyor. İki yaklaşım var: image’ı GitHub Container Registry’ye push edip sunucuda pull etmek, ya da doğrudan sunucuda build etmek. İlk yaklaşım daha temizdir.

# .github/workflows/deploy-docker.yml
name: Build and Deploy Docker Container

on:
  push:
    branches:
      - main

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

jobs:
  build-and-push:
    runs-on: ubuntu-latest
    permissions:
      contents: read
      packages: write
    
    outputs:
      image_tag: ${{ steps.meta.outputs.tags }}
    
    steps:
      - uses: actions/checkout@v4
      
      - name: Login to 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=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 }}
          cache-from: type=gha
          cache-to: type=gha,mode=max

  deploy:
    needs: build-and-push
    runs-on: ubuntu-latest
    
    steps:
      - name: Setup SSH
        run: |
          mkdir -p ~/.ssh
          echo "${{ secrets.SSH_PRIVATE_KEY }}" > ~/.ssh/deploy_key
          chmod 600 ~/.ssh/deploy_key
          ssh-keyscan -H ${{ secrets.VPS_HOST }} >> ~/.ssh/known_hosts
      
      - name: Deploy with Docker Compose
        run: |
          ssh -i ~/.ssh/deploy_key 
              ${{ secrets.VPS_USER }}@${{ secrets.VPS_HOST }} << 'ENDSSH'
            cd /opt/uygulamam
            echo "${{ secrets.GITHUB_TOKEN }}" | docker login ghcr.io -u ${{ github.actor }} --password-stdin
            docker compose pull
            docker compose up -d --remove-orphans
            docker image prune -f
          ENDSSH

Bu yaklaşımın güzel tarafı, image build işleminin ağır kısmı GitHub’ın runner’larında gerçekleşiyor. Sunucunuz sadece pull ve restart yapıyor, bu da deployment süresini ve sunucu yükünü önemli ölçüde düşürüyor.

Zero Downtime Deployment

Uygulamanız production’daysa birkaç saniyelik bile kesinti kabul edilemez olabilir. Blue-green deployment veya rolling update mantığıyla sıfır kesintili geçiş yapabilirsiniz.

#!/bin/bash
# /opt/deploy.sh - Sunucuda bu scripti oluşturun

set -e

APP_DIR="/var/www/uygulamam"
BACKUP_DIR="/var/www/uygulamam_backup"

echo "=== Deployment Basliyor: $(date) ==="

# Mevcut versiyonu yedekle
if [ -d "$APP_DIR" ]; then
    rm -rf "$BACKUP_DIR"
    cp -r "$APP_DIR" "$BACKUP_DIR"
fi

# Yeni kodu çek
cd "$APP_DIR"
git fetch origin main
git reset --hard origin/main
npm ci --production

# Health check sonrası servisi yeniden başlat
pm2 reload uygulamam --update-env

# Uygulama ayağa kalktı mı kontrol et
sleep 5
if curl -f http://localhost:3000/health > /dev/null 2>&1; then
    echo "=== Deployment Basarili ==="
    rm -rf "$BACKUP_DIR"
else
    echo "=== Health Check Basarisiz, Rollback Yapiliyor ==="
    rm -rf "$APP_DIR"
    mv "$BACKUP_DIR" "$APP_DIR"
    cd "$APP_DIR"
    pm2 reload uygulamam --update-env
    exit 1
fi

Workflow tarafında bu scripti çağırmanız yeterli:

      - name: Run deployment script
        run: |
          ssh -i ~/.ssh/deploy_key 
              ${{ secrets.VPS_USER }}@${{ secrets.VPS_HOST }} 
              'bash /opt/deploy.sh'

Environment Variables ve .env Yönetimi

Production ortamında .env dosyası genellikle git’e eklenmez ve sunucuda manuel olarak oluşturulur. Ama bazı durumlarda deployment sırasında env dosyasını da güncellemek istersiniz.

Bunun için GitHub Secrets kullanabilirsiniz. Tüm env içeriğini tek bir secret olarak saklayın:

  • APP_ENV_PRODUCTION: .env dosyasının tüm içeriği

Workflow’da bunu kullanmak için:

      - name: Update environment file
        run: |
          ssh -i ~/.ssh/deploy_key 
              ${{ secrets.VPS_USER }}@${{ secrets.VPS_HOST }} 
              "echo '${{ secrets.APP_ENV_PRODUCTION }}' > /var/www/uygulamam/.env"

Dikkat: Bu yaklaşımda secret içinde tek tırnak işareti varsa kaçış karakteri gerekebilir. Daha güvenli alternatif, base64 encode ederek göndermektir:

      - name: Update environment file safely
        run: |
          echo "${{ secrets.APP_ENV_PRODUCTION }}" | base64 -d > /tmp/app_env
          scp -i ~/.ssh/deploy_key /tmp/app_env 
              ${{ secrets.VPS_USER }}@${{ secrets.VPS_HOST }}:/var/www/uygulamam/.env
          rm /tmp/app_env

Deployment Bildirimleri

Başarılı ya da başarısız deployment’lardan haberdar olmak için bildirim ekleyebilirsiniz. Slack veya Discord en yaygın tercihlerdir.

      - name: Notify Slack on success
        if: success()
        run: |
          curl -X POST ${{ secrets.SLACK_WEBHOOK_URL }} 
          -H 'Content-type: application/json' 
          --data '{
            "text": "✅ Deployment basarili!nBranch: ${{ github.ref_name }}nCommit: ${{ github.sha }}nYapan: ${{ github.actor }}"
          }'
      
      - name: Notify Slack on failure
        if: failure()
        run: |
          curl -X POST ${{ secrets.SLACK_WEBHOOK_URL }} 
          -H 'Content-type: application/json' 
          --data '{
            "text": "❌ Deployment BASARISIZ!nBranch: ${{ github.ref_name }}nCommit: ${{ github.sha }}nLog icin Actions sekmesini kontrol edin."
          }'

if: success() ve if: failure() koşulları sayesinde her duruma göre farklı mesaj gönderebilirsiniz.

Yaygın Hatalar ve Çözümleri

“Host key verification failed” hatası: ssh-keyscan adımını atlamışsınızdır veya IP adresi yanlıştır. Workflow’da ssh-keyscan -H ${{ secrets.VPS_HOST }} >> ~/.ssh/known_hosts satırının olduğundan emin olun.

“Permission denied (publickey)” hatası: Private key’in formatı bozulmuş olabilir. Secret’ı eklerken dosyanın tüm içeriğini kopyaladığınızdan, başında ve sonunda boşluk bırakmadığınızdan emin olun. -----BEGIN OPENSSH PRIVATE KEY----- satırı dahil her şey olmalı.

“sudo: no tty present” hatası: SSH üzerinden sudo komutları çalıştırıyorsanız, deployer kullanıcısına parolasız sudo yetkisi vermeniz gerekir. Sunucuda şu satırı /etc/sudoers.d/deployer dosyasına ekleyin:

deployer ALL=(ALL) NOPASSWD: /bin/systemctl restart uygulamam

Tüm komutlara değil, yalnızca ihtiyaç duyduğunuz komutlara bu yetkiyi verin.

Workflow çok uzun sürüyor: npm ci her seferinde tüm paketleri indiriyordur. Actions cache kullanarak bunu hızlandırabilirsiniz:

      - name: Cache node modules
        uses: actions/cache@v4
        with:
          path: ~/.npm
          key: ${{ runner.os }}-node-${{ hashFiles('**/package-lock.json') }}

Güvenlik Önerileri

Deployment pipeline’ı kurarken güvenliği de göz önünde bulundurmak gerekiyor.

  • SSH portunu değiştirin: Varsayılan 22 portu sürekli taranır. 2222 veya başka bir port kullanmayı düşünün. VPS_PORT secret’ına yeni portu yazmanız yeterli.
  • Deployer kullanıcısını kısıtlayın: Bu kullanıcı yalnızca deployment için gereken komutları çalıştırabilmeli, sisteme geniş erişimi olmamalı.
  • Secret’ları düzenli rotasyon yapın: Özellikle ekip üyeleri değişiyorsa SSH anahtarlarını yenileyin.
  • Workflow dosyalarını review edin: .github/workflows/ klasöründeki değişiklikler dikkatli incelenmelidir. Kötü niyetli bir katkıda bulunan kişi buraya zararlı komut ekleyebilir.
  • Environment’a göre branch koruyun: GitHub’ın branch protection kuralları ile main branch’e doğrudan push’u engelleyin, her şey pull request üzerinden geçsin.

Sonuç

GitHub Actions ile VPS deployment, bir kez kurulduğunda geliştirici deneyimini köklü biçimde değiştiriyor. Artık sunucuya bağlanmak, kodu elle çekmek, servisi yeniden başlatmak gibi tekrarlayan işlemlerle zaman harcamıyorsunuz. Kodunuzu yazıyor, push ediyorsunuz ve pipeline geri kalanını hallediyor.

Bu rehberde anlattığımız adımları sırayla takip ederseniz, çalışan bir CI/CD pipeline’ı birkaç saat içinde kurabilirsiniz. Basit deployment ile başlayın, sistemi tanıdıkça testler, Docker entegrasyonu ve zero downtime stratejileri ekleyerek ilerleyin.

Bir noktada mutlaka bir şeyler ters gidecek, workflow çalışmayacak veya deployment başarısız olacaktır. GitHub Actions’ın log sistemi oldukça ayrıntılıdır, Actions sekmesinden her adımın çıktısını görebilirsiniz. Hata ayıklama sürecinde bu loglar en iyi rehberiniz olacak. Merak etmeyin, ilk birkaç denemede sorun yaşamak tamamen normaldir.

Bir yanıt yazın

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