Kıvanç Ertürk
Blog Claude Code ile yapıldı EN

Tüm yazılar · Geliştirme

SSH'sız Windows hostinge otomatik deploy

SSH'sız Windows hostinge otomatik deploy

Bu site paylaşımlı bir Windows hostingde duruyor: Plesk paneli, arkada IIS. Elimdeki erişim sadece FTP ve Web Deploy. SSH yok, panelde Git entegrasyonu yok. Yine de istediğim şey basitti: main’e push ettiğimde site kendiliğinden güncellensin.

Bunu GitHub Actions ve FTPS ile kurdum. Sunucuda hiçbir şey kurmak gerekmedi. Aşağıda kurulum ve beni yakalayan tuzaklar var.

Neden FTPS

Seçenekler şunlardı:

YolSorun
SSH ile rsyncSSH yok
Panelin Git entegrasyonuBu planda yok. Olsa da build adımı sunucuda koşmalı
Web DeployÇalışır ama Windows odaklı araç zinciri ister
FTPS ile senkronHer planda var, sunucu tarafında kurulum gerekmez

Build GitHub’ın Linux runner’ında çalışıyor, sunucuya sadece çıktı klasörü gidiyor. Bunun için SamKirkland/FTP-Deploy-Action kullanıyorum. Sunucuda bir durum dosyası tutuyor ve her deploy’da sadece değişen dosyaları yüklüyor. Yerelde silinen dosyalar sunucudan da siliniyor.

Kurulum

1. Ayrı bir FTP hesabı. Panelin ana kullanıcısını kullanmıyorum. Panelde deploy için ayrı bir FTP hesabı açtım. Şifresi uzun ve rastgele, sadece GitHub’da duruyor.

2. Secrets. Repo ayarlarında üç secret var: FTP_HOST, FTP_USERNAME, FTP_PASSWORD.

3. Workflow. İki job var. test her push’ta ve PR’da çalışıyor. deploy sadece main’e push’ta ve testler geçerse çalışıyor.

on:
  push:
    branches: [main]
  pull_request:
    branches: [main]
  workflow_dispatch:

permissions:
  contents: read

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

jobs:
  test:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - uses: actions/setup-node@v4
        with: { node-version: 22, cache: npm }
      - run: npm ci
      - run: npm run build
      - run: npm test

  deploy:
    needs: test
    if: github.event_name != 'pull_request'
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - uses: actions/setup-node@v4
        with: { node-version: 22, cache: npm }
      - run: npm ci
      - run: npm run build
      - uses: SamKirkland/FTP-Deploy-Action@v4.3.5
        with:
          server: ${{ secrets.FTP_HOST }}
          username: ${{ secrets.FTP_USERNAME }}
          password: ${{ secrets.FTP_PASSWORD }}
          protocol: ftps
          local-dir: dist/
          server-dir: httpdocs/
          state-name: .ftp-deploy-sync-state.json

İki ayrıntı önemli. concurrency ile cancel-in-progress: false, yarıda kesilmiş bir FTP senkronunu önlüyor. permissions: contents: read ise workflow token’ını salt okunur tutuyor.

4. web.config. IIS’in davranışını repodan yönetmenin yolu bu dosya. Build çıktısının kökünde duruyor ve siteyle birlikte yükleniyor.

<configuration>
  <system.webServer>
    <rewrite>
      <rules>
        <rule name="https" stopProcessing="true">
          <match url="(.*)" />
          <conditions><add input="{HTTPS}" pattern="off" ignoreCase="true" /></conditions>
          <action type="Redirect" url="https://example.com/{R:1}" redirectType="Permanent" />
        </rule>
      </rules>
    </rewrite>
    <staticContent>
      <remove fileExtension=".json" />
      <mimeMap fileExtension=".json" mimeType="application/json" />
      <remove fileExtension=".woff2" />
      <mimeMap fileExtension=".woff2" mimeType="font/woff2" />
      <clientCache cacheControlMode="DisableCache" />
    </staticContent>
    <security>
      <requestFiltering>
        <hiddenSegments>
          <add segment=".ftp-deploy-sync-state.json" />
        </hiddenSegments>
      </requestFiltering>
    </security>
  </system.webServer>
  <location path="assets">
    <system.webServer>
      <staticContent>
        <clientCache cacheControlMode="UseMaxAge" cacheControlMaxAge="365.00:00:00" cacheControlCustom="immutable" />
      </staticContent>
    </system.webServer>
  </location>
</configuration>

Beni yakalayan tuzaklar

FTP hesabının ana klasörü. Ek FTP hesapları kendi ana klasörlerine kapatılıyor. Hesabın kökünü httpdocs yaparsan, workflow’daki server-dir: httpdocs/ aslında httpdocs/httpdocs/ olur. Deploy yeşil biter ama site değişmez. Ya hesabın kökünü üst klasör yap, ya da server-dir’i ./ olarak ayarla. İkisini birbirine uygun seç.

İlk deploy eski dosyaları silmiyor. Senkron, sunucudaki durum dosyasıyla karşılaştırarak çalışıyor. İlk koşuda bu dosya olmadığı için sunucudaki eski dosyaları tanımıyor ve silmiyor. Temizlik ikinci koşudan itibaren başlıyor. İlk deploy’dan sonra klasöre bir kez elle bakmak gerekiyor.

Durum dosyası web’de yayınlanıyor. .ftp-deploy-sync-state.json sitenin köküne yazılıyor ve IIS nokta ile başlayan dosyaları gizlemiyor. Dosya, sitedeki tüm dosyaların listesini veriyor. hiddenSegments ile 404 döndürmek gerekiyor.

Eksik MIME tipleri. IIS varsayılan olarak .json ya da .woff2 dosyalarını sunmayabiliyor. Sayfa açılıyor ama dil dosyası ya da font yüklenmiyor. staticContent altında tanımlamak gerekiyor. Aynı uzantı zaten tanımlıysa hata vermesin diye önce remove kullanılıyor.

Tek bir cache ayarı. Başta tüm siteye 24 saatlik bir clientCache koymuştum. Bu HTML’i de kapsıyordu. Deploy sonrası dönen ziyaretçi bir gün eski sayfayı görebilirdi. Doğrusu: HTML için cache kapalı, isimleri hash’li ya da hiç değişmeyen assets klasörü için bir yıl ve immutable.

Runner alınamadı. Bir deploy “job was not started because it repeatedly failed to be acquired” hatasıyla düştü. Bu GitHub tarafında bir sorun, koddan kaynaklanmıyor. gh run rerun --failed yeterli oldu.

Aynı yoldan .NET 8

Aynı hostingde ASP.NET Core Hosting Bundle kuruluysa, aynı mekanizmayla .NET uygulaması da deploy edilebiliyor. Denemek için bir alt alan adı açıp minimal bir API yükledim:

- uses: actions/setup-dotnet@v4
  with: { dotnet-version: "8.0.x" }
- run: dotnet publish MyApp.csproj -c Release -r win-x64 --self-contained false -o publish
- uses: SamKirkland/FTP-Deploy-Action@v4.3.5
  with:
    # ... aynı secret'lar
    local-dir: publish/
    server-dir: app.example.com/

Linux runner’da win-x64 için publish sorunsuz çalışıyor. dotnet publish IIS için gereken web.config dosyasını kendisi üretiyor. Uygulama ilk denemede açıldı. Bir not: çalışan bir uygulamayı güncellerken IIS DLL’leri kilitleyebilir. Klasik çözüm, yüklemeden önce köke bir app_offline.htm koymak ve yükleme bitince silmek.

Doğrulama

Yeşil bir deploy, dosyaların doğru yere gittiğini kanıtlamaz. Her büyük değişiklikten sonra birkaç curl ile kontrol ediyorum:

curl -sI http://example.com/ | head -3
curl -sI https://example.com/assets/fonts/inter.woff2 | grep -iE "content-type|cache-control"
curl -sI https://example.com/.ftp-deploy-sync-state.json | head -1

İlki https’e 301 dönmeli. İkincisi doğru MIME tipini ve uzun cache’i göstermeli. Üçüncüsü 404 dönmeli.

Sonuç

SSH’sız bir paylaşımlı hosting, otomatik deploy için engel değil. Build’i CI’da yap, sadece çıktıyı FTPS ile senkronla, sunucu davranışını repodaki web.config ile yönet. Geriye kalan iş, ilk seferde bir kez elle kontrol etmek.