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_deploydosyası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:
.envdosyası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
mainbranch’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.
