Her güncellemede sunucuya SSH ile bağlanıp git pull yazmak, küçük bir projede bile zamanla hata kaynağına dönüşür: bir adım unutulur, yanlış dal çekilir, kimin neyi ne zaman yayınladığı belli olmaz. GitHub Actions ile bu adımları depodaki bir YAML dosyasına yazarsınız; main dalına her push yapıldığında GitHub'ın sanal makinesi (runner) kodu alır, SSH ile sunucunuza kopyalar ve servisi yeniden başlatır.

Bu rehberde üçüncü taraf bir action'a bağlı kalmadan, runner üzerinde zaten kurulu olan düz ssh ve rsync komutlarıyla güvenli bir deploy hattı kuruyoruz. Sunucuda yalnızca tek bir komutu sudo ile çalıştırabilen kısıtlı bir deploy kullanıcısı, depo gizli değeri olarak saklanan SSH anahtarı, önceden doğrulanmış known_hosts satırı, her yayın için ayrı sürüm klasörü ve tek komutla geri dönüş bu yapının parçaları. Örnekler Ubuntu 24.04 sunucuyu ve myapp.service adlı bir systemd servisini varsayar; servis tarafını Django, FastAPI veya Next.js rehberlerimizden kurabilirsiniz.

Yapının özeti

ParçaNeredeGörevi
deploy kullanıcısıSunucuDosyaları yükler, yalnızca servisi yeniden başlatabilir
DEPLOY_SSH_KEYGitHub secretRunner'ın sunucuya bağlandığı özel anahtar
DEPLOY_KNOWN_HOSTSGitHub secretSunucunun doğrulanmış host anahtarı; araya girme saldırısına karşı
DEPLOY_HOST, DEPLOY_PORTGitHub variableSunucu adresi ve SSH portu
/srv/myapp/releases/SunucuHer yayın için zaman damgalı ayrı klasör
/srv/myapp/currentSunucuEtkin sürümü gösteren sembolik bağ; servis buradan çalışır

1. Sunucuda kısıtlı deploy kullanıcısı

1. Sunucuda kısıtlı deploy kullanıcısı

Deploy işlemi için root veya kendi yönetici hesabınızı kullanmayın. Parolasız giriş yapamayan ayrı bir kullanıcı açın ve sürüm dizinlerini ona verin:

bash
sudo adduser --disabled-password --gecos "" deploy
sudo mkdir -p /srv/myapp/releases /srv/myapp/shared
sudo chown -R deploy:deploy /srv/myapp
sudo apt install -y rsync

Kullanıcı yüklemeleri, ortam dosyası veya SQLite veritabanı gibi sürümler arasında korunması gereken dosyaları /srv/myapp/shared altında tutun; her yeni sürüm klasörü sıfırdan oluştuğu için oraya yazılan dosyalar bir sonraki yayında kaybolur. Servis biriminizin WorkingDirectory değerini de /srv/myapp/current yapın.

Servisi yeniden başlatmak root yetkisi ister. Deploy kullanıcısına genel sudo vermek yerine yalnızca tek bir komutu parolasız çalıştırma izni verin. Dosyayı mutlaka visudo ile düzenleyin; söz dizimi hatası sudo'yu tamamen kilitleyebilir:

bash
sudo visudo -f /etc/sudoers.d/deploy-myapp
text
deploy ALL=(root) NOPASSWD: /usr/bin/systemctl restart myapp.service

sudoers kuralında komutu argümanlarıyla birlikte yazdığınızda kullanıcı yalnızca bu argümanlarla çalıştırabilir; systemctl stop veya başka bir servis için izin almaz. Workflow'daki komutun bu satırla birebir aynı olması gerekir.

2. SSH anahtarı ve authorized_keys kısıtlaması

Anahtarı kendi bilgisayarınızda, yalnızca bu iş için üretin. Parola koymuyoruz çünkü runner etkileşimli parola giremez; anahtarın güvenliğini GitHub secret deposu ve sunucu tarafındaki kısıtlamalar sağlayacak.

bash
ssh-keygen -t ed25519 -C "github-actions-deploy" -f ./gha_deploy_key -N ""

Açık anahtarı (gha_deploy_key.pub) sunucuda deploy kullanıcısının authorized_keys dosyasına, başına restrict seçeneği ekleyerek koyun. sshd(8) belgelerine göre restrict port, agent ve X11 yönlendirmesini, PTY ayırmayı ve ~/.ssh/rc çalıştırmayı kapatır. rsync ve tek satırlık komutlar PTY gerektirmediği için deploy akışı etkilenmez.

bash
sudo -u deploy mkdir -p -m 700 /home/deploy/.ssh
echo 'restrict ssh-ed25519 AAAA...ANAHTARINIZ... github-actions-deploy' | sudo -u deploy tee -a /home/deploy/.ssh/authorized_keys
sudo chmod 600 /home/deploy/.ssh/authorized_keys

3. known_hosts satırını doğrulayarak üretin

Runner sunucunuza ilk kez bağlandığında sunucunun kimliğini bilmez. StrictHostKeyChecking no ile bu kontrolü kapatmak kolaydır ama ssh-keyscan(1) belgesinin uyardığı gibi, doğrulanmadan oluşturulan bir known_hosts dosyası sizi araya girme (man in the middle) saldırılarına açık bırakır. Doğru yol, anahtarı bir kez taramak ve parmak izini sunucunun kendisinden okuduğunuz değerle karşılaştırmaktır.

bash
# Sunucuda (SSH oturumunuzda): gerçek parmak izi
ssh-keygen -lf /etc/ssh/ssh_host_ed25519_key.pub

# Kendi bilgisayarınızda: tarama ve parmak izi
ssh-keyscan -t ed25519 -p 22 SUNUCU_IP > known_hosts_satiri
ssh-keygen -lf known_hosts_satiri

İki parmak izi aynıysa known_hosts_satiri dosyasının içeriği DEPLOY_KNOWN_HOSTS secret'ı olacak. SSH 22 dışında bir portta çalışıyorsa -p değerini değiştirin; bu durumda satır [SUNUCU_IP]:PORT biçiminde üretilir ve ssh bu biçimi otomatik tanır.

4. Secret, variable ve environment tanımları

4. Secret, variable ve environment tanımları: Environment, Plan sınırları, Erişim, Fork ve Dependabot

Deponuzda Settings > Secrets and variables > Actions sayfasına gidin. GitHub CLI kullanıyorsanız değerleri dosyadan da okutabilirsiniz; belgelerde gösterilen biçim şudur:

bash
gh secret set DEPLOY_SSH_KEY < gha_deploy_key
gh secret set DEPLOY_KNOWN_HOSTS < known_hosts_satiri

DEPLOY_HOST ve DEPLOY_PORT gizli olmadığı için Variables sekmesine ekleyin; workflow'da vars bağlamıyla okunur. Ardından yerel diskteki gha_deploy_key dosyasını silin veya güvenli bir kasaya taşıyın.

  • Environment: Workflow'daki environment: production satırı işi bir ortama bağlar. Ortam secret'ları yalnızca o ortama bağlı işlerde görünür; onaylayıcı tanımlıysa onay gelene kadar iş secret'lara erişemez.
  • Plan sınırları: GitHub belgelerine göre GitHub Free planında ortamlar ve ortam secret'ları yalnızca public depolarda kullanılabilir. Private depolarda ortam ve ortam secret'ları için Pro, Team veya Enterprise gerekir; zorunlu onaylayıcı ve bekleme süresi gibi kurallar ise Free, Pro ve Team planlarında yalnızca public depolarda vardır. Private bir depoda Free plandaysanız secret'ları depo düzeyinde tanımlayın ve environment satırını kaldırın.
  • Erişim: Belgeler, depoda yazma yetkisi olan herkesin depo secret'larını okuyabildiğini hatırlatır. Deploy anahtarının yetkisini bu yüzden sunucu tarafında daraltıyoruz.
  • Fork ve Dependabot: Fork'tan tetiklenen workflow'lara ve Dependabot olaylarına secret aktarılmaz; deploy'u pull_request yerine push ile tetiklemek bu yüzden de doğrudur.

5. Workflow dosyası

Aşağıdaki dosya yalnızca GitHub'ın belgelediği söz dizimini ve GitHub'ın resmî actions/checkout action'ını kullanır. Geri kalan her şey runner imajında hazır gelen ssh, rsync ve curl komutlarıdır; GitHub'ın Ubuntu 24.04 runner imajı listesinde openssh-client ve rsync yer alır.

yaml
# .github/workflows/deploy.yml
name: Deploy

on:
  push:
    branches: [main]
  workflow_dispatch:

permissions:
  contents: read

concurrency:
  group: production-deploy
  cancel-in-progress: false

jobs:
  deploy:
    runs-on: ubuntu-24.04
    environment: production
    timeout-minutes: 15
    steps:
      - uses: actions/checkout@v6

      - name: SSH yapılandırması
        env:
          SSH_PRIVATE_KEY: ${{ secrets.DEPLOY_SSH_KEY }}
          SSH_KNOWN_HOSTS: ${{ secrets.DEPLOY_KNOWN_HOSTS }}
          DEPLOY_HOST: ${{ vars.DEPLOY_HOST }}
          DEPLOY_PORT: ${{ vars.DEPLOY_PORT }}
        run: |
          install -m 700 -d ~/.ssh
          printf '%s\n' "$SSH_PRIVATE_KEY" > ~/.ssh/deploy_key
          chmod 600 ~/.ssh/deploy_key
          printf '%s\n' "$SSH_KNOWN_HOSTS" > ~/.ssh/known_hosts
          cat > ~/.ssh/config <<EOF
          Host hedef
            HostName $DEPLOY_HOST
            Port $DEPLOY_PORT
            User deploy
            IdentityFile ~/.ssh/deploy_key
            IdentitiesOnly yes
            StrictHostKeyChecking yes
          EOF

      - name: Yeni sürümü yükle
        run: |
          RELEASE="$(date -u +%Y%m%d%H%M%S)-${GITHUB_SHA::7}"
          echo "RELEASE=$RELEASE" >> "$GITHUB_ENV"
          echo "PREV=$(ssh hedef 'readlink /srv/myapp/current || true')" >> "$GITHUB_ENV"
          rsync -az --delete --exclude='.git' --exclude='.github' ./ "hedef:/srv/myapp/releases/$RELEASE/"

      - name: Sürümü etkinleştir
        run: |
          ssh hedef "ln -sfn /srv/myapp/releases/$RELEASE /srv/myapp/current.tmp && mv -T /srv/myapp/current.tmp /srv/myapp/current && sudo systemctl restart myapp.service"

      - name: Sağlık kontrolü
        run: curl -fsS --retry 5 --retry-delay 3 https://ornek.com/saglik

      - name: Hata olursa önceki sürüme dön
        if: failure() && env.PREV != ''
        run: |
          ssh hedef "ln -sfn $PREV /srv/myapp/current.tmp && mv -T /srv/myapp/current.tmp /srv/myapp/current && sudo systemctl restart myapp.service"

      - name: Eski sürümleri temizle
        run: ssh hedef 'cd /srv/myapp/releases && ls -1t | tail -n +6 | xargs -r rm -rf --'
  • permissions: contents: read: GitHub güvenlik belgeleri GITHUB_TOKEN için varsayılanın yalnızca içerik okuma olmasını önerir. Bu workflow depoya yazmadığı için daha fazlasına gerek yok.
  • concurrency: Aynı gruptaki iki deploy aynı anda çalışmaz. cancel-in-progress: false yarıda kalmış bir yayının kesilmesini engeller; bekleyen yeni çalıştırma sıraya girer.
  • workflow_dispatch: Actions sekmesinde "Run workflow" düğmesi çıkarır. Belgelere göre bu düğme yalnızca workflow dosyası varsayılan dalda olduğunda görünür.
  • Secret'lar env üzerinden: GitHub belgeleri secret'ları komut satırına doğrudan yazmak yerine ortam değişkeni veya STDIN ile aktarmayı önerir; komut satırları ps ile görülebilir.
  • GITHUB_ENV: Bir adımda yazılan RELEASE ve PREV değerleri sonraki adımlarda ortam değişkeni olarak kullanılabilir.
  • Sembolik bağ değişimi: Yeni bağ önce current.tmp adıyla oluşturulur, sonra mv -T ile current üzerine taşınır. Böylece current hiçbir an yarım kalmaz.
  • Sağlık kontrolü: /saglik uygulamanızda 200 dönen basit bir uç olmalı. Adres yoksa kendi uç noktanızla değiştirin.

Uygulamaya özgü kurulum adımlarını (bağımlılık kurulumu, statik dosya derleme, veritabanı göçü) "Yeni sürümü yükle" ile "Sürümü etkinleştir" adımları arasına, yeni sürüm klasöründe çalışan bir ssh hedef "..." komutu olarak ekleyin. Python projelerinde sanal ortamı her sürüm klasörünün içinde oluşturmak, geri dönüşte bağımlılıkların da eski hâline dönmesini sağlar. Geri alınamayan göçlerde otomatik geri dönüşe güvenmeyin; önce yedek alın. Runner üzerindeki SSH yapılandırmasının mantığı için SSH config dosyası rehberi yazımıza bakabilirsiniz.

6. Elle geri dönüş

Workflow sağlık kontrolü başarısız olursa önceki sürüme kendisi döner. Sorunu daha sonra fark ederseniz aynı işlemi sunucuda elle yapabilirsiniz:

bash
ls -1t /srv/myapp/releases
ln -sfn /srv/myapp/releases/ONCEKI_SURUM /srv/myapp/current.tmp
mv -T /srv/myapp/current.tmp /srv/myapp/current
sudo systemctl restart myapp.service

Son adım temizlik komutu en yeni beş sürümü tutar; disk alanınıza göre bu sayıyı tail -n +6 içindeki değerle değiştirebilirsiniz. Sürüm klasörleri yedeğin yerini tutmaz; veritabanı ve shared klasörü için rsync ile sunucu yedekleme ve veritabanı yedekleme stratejileri yazılarımızı uygulayın.

7. Üçüncü taraf SSH action'ları hakkında

Marketplace'te SSH ve SCP işlerini kolaylaştıran action'lar vardır; örneğin appleboy/ssh-action GitHub'a değil bağımsız bir geliştiriciye aittir. GitHub güvenlik belgeleri, bir action'ın ele geçirilmesinin depodaki tüm secret'lara erişim anlamına gelebileceğini belirtir ve üçüncü taraf action'ları tam uzunlukta commit SHA'sına sabitlemenizi, etiketi ancak yayıncıya güveniyorsanız kullanmanızı ve kaynak kodunu incelemenizi önerir. Düz ssh ve rsync komutları bu bağımlılığı tamamen ortadan kaldırır.

8. Sık karşılaşılan hatalar

8. Sık karşılaşılan hatalar: Host key verification failed, Permission denied (publickey), sudo: a password is required, rsync: command not found
HataNedenÇözüm
Host key verification failedknown_hosts satırı yok, eski veya port uyuşmuyorTaramayı doğru port ile tekrarlayın, parmak izini yeniden karşılaştırın; sunucu yeniden kurulduysa host anahtarı değişmiştir
Permission denied (publickey)Anahtar secret'a eksik kopyalanmış veya authorized_keys izinleri yanlışBEGIN/END satırları dahil tamamını kopyalayın; .ssh 700, authorized_keys 600 ve sahibi deploy olmalı
sudo: a password is requiredWorkflow'daki komut sudoers satırıyla birebir aynı değilKomut yolunu ve servis adını eşleştirin; sudo -l -U deploy ile izinleri görün
rsync: command not foundSunucuda rsync kurulu değilsudo apt install rsync
Bağlantı zaman aşımıGüvenlik duvarı, yanlış port veya Fail2ban engeliPort değişkenini, UFW kurallarını ve fail2ban-client status sshd çıktısını kontrol edin

Sık Sorulan Sorular

Sunucuda git pull yapmak daha basit değil mi?

Daha basittir, ama sunucunun depoya erişmesi için ayrıca bir okuma anahtarı gerekir ve çalışma klasörü yarım güncellenmiş hâlde kalabilir. Runner'ın dosyaları yeni bir klasöre kopyalayıp bağı tek hamlede değiştirmesi, hem geri dönüşü hem de "şu an hangi sürüm çalışıyor" sorusunu kolaylaştırır.

Docker imajı oluşturup sunucuda çalıştırabilir miyim?

Evet; bu durumda workflow imajı bir registry'ye iter, SSH adımı da sunucuda yeni imajı çekip konteyneri yeniden başlatır. Konteyner tarafı için Docker ile uygulama deploy yazımıza bakın.

Bu yapı Laravel için de geçerli mi?

Geçerli; sürüm klasörü ve shared yaklaşımı PHP projelerinde de yaygındır. Laravel'e özgü izinler ve artisan komutları için Laravel deploy rehberimize bakın.

Deploy anahtarı sızarsa ne yapmalıyım?

Sunucudaki authorized_keys dosyasından ilgili satırı hemen silin, yeni bir anahtar üretip secret'ı güncelleyin. GitHub belgeleri, bir secret loga düşerse logu silmenizi ve secret'ı değiştirmenizi de önerir.

Yazılım ve hizmet adları sahiplerine aittir. Bu yazı bağımsız bir teknik rehberdir; adı geçen projeler veya şirketlerle ortaklık ilişkisi bulunduğu anlamına gelmez.

İlgili içerikler: Django deploy · FastAPI deploy · Next.js deploy · Laravel deploy · SSH ile sunucuya bağlanma · systemd servis dosyası · rsync ile yedekleme