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ça | Nerede | Görevi |
|---|---|---|
deploy kullanıcısı | Sunucu | Dosyaları yükler, yalnızca servisi yeniden başlatabilir |
DEPLOY_SSH_KEY | GitHub secret | Runner'ın sunucuya bağlandığı özel anahtar |
DEPLOY_KNOWN_HOSTS | GitHub secret | Sunucunun doğrulanmış host anahtarı; araya girme saldırısına karşı |
DEPLOY_HOST, DEPLOY_PORT | GitHub variable | Sunucu adresi ve SSH portu |
/srv/myapp/releases/ | Sunucu | Her yayın için zaman damgalı ayrı klasör |
/srv/myapp/current | Sunucu | Etkin sürümü gösteren sembolik bağ; servis buradan çalışır |
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:
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 rsyncKullanı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:
sudo visudo -f /etc/sudoers.d/deploy-myappdeploy ALL=(root) NOPASSWD: /usr/bin/systemctl restart myapp.servicesudoers 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.
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.
# 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ı

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:
gh secret set DEPLOY_SSH_KEY < gha_deploy_key
gh secret set DEPLOY_KNOWN_HOSTS < known_hosts_satiriDEPLOY_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: productionsatı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
environmentsatı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_requestyerinepushile 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.
# .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: falseyarı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ı
psile görülebilir. - GITHUB_ENV: Bir adımda yazılan
RELEASEvePREVdeğerleri sonraki adımlarda ortam değişkeni olarak kullanılabilir. - Sembolik bağ değişimi: Yeni bağ önce
current.tmpadıyla oluşturulur, sonramv -Tilecurrentüzerine taşınır. Böylececurrenthiçbir an yarım kalmaz. - Sağlık kontrolü:
/saglikuygulamanı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:
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.serviceSon 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

| Hata | Neden | Çözüm |
|---|---|---|
| Host key verification failed | known_hosts satırı yok, eski veya port uyuşmuyor | Taramayı 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 required | Workflow'daki komut sudoers satırıyla birebir aynı değil | Komut yolunu ve servis adını eşleştirin; sudo -l -U deploy ile izinleri görün |
| rsync: command not found | Sunucuda rsync kurulu değil | sudo apt install rsync |
| Bağlantı zaman aşımı | Güvenlik duvarı, yanlış port veya Fail2ban engeli | Port 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