Linux'ta .NET Uygulamasını systemd Service Olarak Çalıştırmak
Kestrel'i systemd altında ayrı bir kullanıcıyla, Type=notify ve AddSystemd ile, KillSignal=SIGTERM ve hardening ayarlarıyla çalıştır; KillSignal=SIGINT açık istekleri keser ve systemd bunu yine success olarak raporlar. Publish seçeneklerinin boyutlarını, Ubuntu 24.04'te runtime'ın dağıtım deposundan gelmesini, journald çıktısını, çökme sonrası restart'ı, exposure skorunun 9.2'den 3.8'e inmesini ve releases/current symlink'iyle 0,2 saniyelik deploy ve geri dönüşü ölçüyoruz.
Sürüm notu: Bu yazıdaki örnekler .NET 10 SDK (Software Development Kit) 10.0.402, Ubuntu 24.04 paket deposundaki aspnetcore-runtime-10.0 10.0.14, Microsoft.Extensions.Hosting.Systemd 10.0.14 ve systemd 255 ile, Docker 29 üzerinde systemd'yi PID (process ID) 1 olarak çalıştıran bir Ubuntu 24.04 container'ında denenmiştir.
Önce Tahmin Et
Bookstore.Api, bir Linux VM'de (virtual machine) systemd service olarak çalışıyor. Birinin internetten bulduğu eski bir unit dosyasında KillSignal=SIGINT yazıyor; başka biri bunu KillSignal=SIGTERM yapmak istiyor. Test için 5 saniye süren bir rapor isteği başlatılıyor ve bir saniye sonra systemctl stop bookstore-api çalıştırılıyor.
Tahmin et: iki ayarda rapor isteği tamamlanır mı? systemctl stop ne kadar sürer? Ve ikisi arasında fark yoksa, Microsoft'un dokümanı neden SIGINT'ten (interrupt sinyali, terminalde Ctrl+C'nin gönderdiği) açıkça kaçınmanı söylüyor?
Publish Seçenekleri: Framework-Dependent, Self-Contained, Single-File
Sunucuya ne kopyalayacağına dotnet publish'in seçenekleri karar veriyor. Aynı Bookstore.Api için dört çıktı:
dotnet publish -c Release -o out/pub-fdd
dotnet publish -c Release -r linux-arm64 --self-contained false -o out/pub-fdd-rid
dotnet publish -c Release -r linux-arm64 --self-contained true -o out/pub-scd
dotnet publish -c Release -r linux-arm64 --self-contained true -p:PublishSingleFile=true -o out/pub-single
| Çıktı | Boyut | Dosya | Sunucuda gereken |
|---|---|---|---|
| Framework-dependent, RID (Runtime Identifier) yok | 232K | 10 | ASP.NET Core runtime; dotnet Bookstore.Api.dll ile çalışır |
Framework-dependent, linux-arm64 | 180K | 9 | Runtime; ayrıca ./Bookstore.Api executable'ı |
| Self-contained | 114M | 337 | Hiçbir şey; runtime çıktının içinde |
| Self-contained, single-file | 106M | 5 | Hiçbir şey; tek bir 111 MB'lık executable |
Framework-dependent çıktı küçük ve sunucudaki runtime'ı kullanıyor; runtime'ın güvenlik yamaları apt upgrade ile gelip bütün uygulamalara uygulanıyor. Self-contained çıktı runtime'ı kendi içinde taşıyor: sunucuda hiçbir şey kurmana gerek yok, farklı .NET sürümlerindeki uygulamalar yan yana çalışıyor, ama runtime'ın yaması ancak uygulamayı yeniden publish ettiğinde geliyor. RID'li framework-dependent çıktıda web.config yok, çünkü RID Windows değil; RID'siz çıktıda IIS (Internet Information Services) için üretilen web.config de var, Linux'ta zararsız ama gereksiz. Bu yazıda framework-dependent, RID'siz çıktıyı kullanıyorum.
Runtime'ı Kurmak: Ubuntu Deposu mu, Microsoft Deposu mu?
Ubuntu 24.04'te .NET 10 doğrudan dağıtımın kendi deposunda:
$ apt-cache policy aspnetcore-runtime-10.0 aspnetcore-runtime-8.0 | grep -E "^[a-z]|Candidate"
aspnetcore-runtime-10.0:
Candidate: 10.0.14-0ubuntu1~24.04.1
aspnetcore-runtime-8.0:
Candidate: 8.0.33-0ubuntu1~24.04.1
$ sudo apt-get install -y aspnetcore-runtime-10.0
$ dotnet --list-runtimes
Microsoft.AspNetCore.App 10.0.14 [/usr/lib/dotnet/shared/Microsoft.AspNetCore.App]
Microsoft.NETCore.App 10.0.14 [/usr/lib/dotnet/shared/Microsoft.NETCore.App]
Microsoft Learn'deki "Install .NET on Ubuntu" sayfasının tablosuna göre Ubuntu 24.04 için .NET 10 ve 8 dağıtımın kendi deposunda, 9.0 Ubuntu'nun backports deposunda; Microsoft'un paket deposunda (packages.microsoft.com) bu sürüm için .NET yok. Eski rehberlerdeki "önce Microsoft deposunu ekle" adımı yeni Ubuntu sürümlerinde gereksiz ve iki deponun birlikte eklenmesi dokümanın ayrıca uyardığı paket karışıklığına yol açıyor. Kurulum /usr/lib/dotnet'e gidiyor ve /usr/bin/dotnet oraya link; unit dosyasında yolun bu olduğunu bil, /usr/local/bin/dotnet yazan eski örnekler bu kurulumda çalışmaz.
Unit Dosyası Satır Satır
Uygulama için ayrı, login yetkisi olmayan bir sistem kullanıcısı, release klasörleri ve Data Protection key'leri için yazılabilir bir klasör:
sudo useradd --system --no-create-home --shell /usr/sbin/nologin bookstore
sudo mkdir -p /var/www/bookstore/releases/2026-11-08-1 /var/lib/bookstore/keys /etc/bookstore
sudo cp -r out/pub-fdd/. /var/www/bookstore/releases/2026-11-08-1/
sudo ln -sfn /var/www/bookstore/releases/2026-11-08-1 /var/www/bookstore/current
sudo chown -R bookstore:bookstore /var/lib/bookstore
/etc/systemd/system/bookstore-api.service:
[Unit]
Description=Bookstore API (ASP.NET Core on Kestrel)
After=network.target
[Service]
Type=notify
User=bookstore
Group=bookstore
WorkingDirectory=/var/www/bookstore/current
ExecStart=/usr/bin/dotnet /var/www/bookstore/current/Bookstore.Api.dll
Restart=always
RestartSec=10
KillSignal=SIGTERM
TimeoutStopSec=30
SyslogIdentifier=bookstore-api
Environment=ASPNETCORE_ENVIRONMENT=Production
Environment=ASPNETCORE_URLS=http://127.0.0.1:5000
Environment=DOTNET_NOLOGO=true
Environment=DataProtection__KeysDirectory=/var/lib/bookstore/keys
EnvironmentFile=-/etc/bookstore/api.env
[Install]
WantedBy=multi-user.target
Type=notify: systemd, process başladı diye değil, uygulama "hazırım" dediğinde service'i çalışıyor sayıyor. Bunun için uygulamadaAddSystemd()gerekiyor; aşağıda.UserveGroup: root olarak çalışmamak için. Bu satır yoksa service root ile çalışır.WorkingDirectoryveExecStart:currentsymlink'i üzerinden; deploy'da yalnız link değişiyor.Restart=alwaysveRestartSec=10: process hangi sebeple biterse bitsin, 10 saniye sonra yeniden başlat.KillSignalveTimeoutStopSec: durdururken hangi sinyalin gönderileceği ve SIGKILL'den (yakalanamayan, process'i anında öldüren sinyal) önce ne kadar bekleneceği. Microsoft'un dokümanı varsayılanın 90 saniye olduğunu yazıyor; bir sonraki bölümün konusu.Environment: ikinci yazıdaki binding kuralları burada da geçerli; Kestrel yalnız loopback'i dinliyor, önündeki Nginx'e. İç içe configuration anahtarlarında:yerine__.EnvironmentFile=-/etc/bookstore/api.env: secret'lar ve sunucuya özel değerler unit dosyasının dışında, yalnız root'un okuyabildiği bir dosyada. Baştaki-, dosya yoksa hata verme demek.
AddSystemd: Type=notify ve journald
Microsoft.Extensions.Hosting.Systemd paketi ve tek satır:
builder.Services.AddSystemd();
Uygulama systemd altında çalışmıyorsa bu satır hiçbir şey yapmıyor; aynı kod Docker'da ve geliştirici makinesinde değişmeden çalışıyor. systemd altındaysa üç şey değişiyor: host hazır olunca systemd'ye READY=1 bildirimi gidiyor, durdurma sinyali için systemd'ye uygun bir lifetime kullanılıyor ve console log'u journald'ın anlayacağı biçime dönüyor. Başlatıp bakalım:
$ sudo systemctl daemon-reload
$ sudo systemctl enable --now bookstore-api
Created symlink /etc/systemd/system/multi-user.target.wants/bookstore-api.service → /etc/systemd/system/bookstore-api.service.
$ journalctl -u bookstore-api --no-pager
Nov 08 04:32:12 bookstore-vm systemd[1]: Starting bookstore-api.service - Bookstore API (ASP.NET Core on Kestrel)...
Nov 08 04:32:12 bookstore-vm bookstore-api[396]: Microsoft.AspNetCore.DataProtection.KeyManagement.XmlKeyManager[35] No XML encryptor configured. Key {ea7719a5-ce79-4c02-aa6b-98753116d198} may be persisted to storage in unencrypted form.
Nov 08 04:32:12 bookstore-vm bookstore-api[396]: Microsoft.Hosting.Lifetime[14] Now listening on: http://127.0.0.1:5000
Nov 08 04:32:12 bookstore-vm bookstore-api[396]: Microsoft.Hosting.Lifetime[0] Application started. Hosting environment: Production; Content root path: /var/www/bookstore/releases/2026-11-08-1
Nov 08 04:32:12 bookstore-vm systemd[1]: Started bookstore-api.service - Bookstore API (ASP.NET Core on Kestrel).
Üç ayrıntı. "Started" satırı, uygulamanın "Application started" satırından sonra geliyor: Type=notify sayesinde systemd, Kestrel portu açana kadar bekledi. Bu, systemctl start ya da restart döndüğünde uygulamanın gerçekten istek kabul ettiği anlamına geliyor; deploy script'indeki smoke test bu yüzden hemen çalışabiliyor. İkincisi, log satırları tek satır ve renk kodu yok; her satırın journald priority'si log seviyesinden geliyor, bu yüzden journalctl -p warning yalnız uyarıları süzüyor:
$ journalctl -u bookstore-api -p warning --no-pager
Nov 08 04:32:12 bookstore-vm bookstore-api[396]: Microsoft.AspNetCore.DataProtection.KeyManagement.XmlKeyManager[35] No XML encryptor configured. Key {ea7719a5-ce79-4c02-aa6b-98753116d198} may be persisted to storage in unencrypted form.
Üçüncüsü, "Content root path" symlink'i değil gerçek release klasörünü gösteriyor; hangi sürümün çalıştığını log'dan okumanın en kolay yolu. Uygulama gerçekten bookstore kullanıcısıyla mı çalışıyor:
$ ps -o user,pid,cmd -C dotnet
USER PID CMD
booksto+ 396 /usr/bin/dotnet /var/www/bookstore/current/Bookstore.Api.dll
Graceful Shutdown ve Çökme Sonrası Restart
Bookstore.Api, host durdurulurken iki log satırı yazıyor (ApplicationStopping ve ApplicationStopped callback'leri). systemctl stop:
Nov 08 04:32:22 bookstore-vm systemd[1]: Stopping bookstore-api.service - Bookstore API (ASP.NET Core on Kestrel)...
Nov 08 04:32:22 bookstore-vm bookstore-api[396]: Microsoft.Hosting.Lifetime[0] Application is shutting down...
Nov 08 04:32:22 bookstore-vm bookstore-api[396]: Bookstore.Api[0] Stopping: draining in-flight requests on vm1
Nov 08 04:32:22 bookstore-vm bookstore-api[396]: Bookstore.Api[0] Stopped cleanly on vm1
Nov 08 04:32:22 bookstore-vm systemd[1]: bookstore-api.service: Deactivated successfully.
Nov 08 04:32:22 bookstore-vm systemd[1]: Stopped bookstore-api.service - Bookstore API (ASP.NET Core on Kestrel).
Aynısı KillSignal=SIGINT ile:
Nov 08 04:32:24 bookstore-vm systemd[1]: Stopping bookstore-api.service - Bookstore API (ASP.NET Core on Kestrel)...
Nov 08 04:32:24 bookstore-vm systemd[1]: bookstore-api.service: Deactivated successfully.
Nov 08 04:32:24 bookstore-vm systemd[1]: Stopped bookstore-api.service - Bookstore API (ASP.NET Core on Kestrel).
$ systemctl show bookstore-api -p Result -p ExecMainStatus -p ExecMainCode
Result=success
ExecMainCode=2
ExecMainStatus=2
Uygulamanın tek bir satırı yok. ExecMainCode=2 "sinyalle öldürüldü", ExecMainStatus=2 de sinyalin numarası: SIGINT. Uygulama hiçbir shutdown kodu çalıştırmadan bitti, ama systemd bunu Result=success olarak raporluyor, çünkü kendi gönderdiği sinyalle biten process'i başarılı sayıyor. Tahminin cevabı, 5 saniyelik rapor isteğiyle:
=== KillSignal=SIGTERM
in-flight request: 200 after 5.034589s
curl exit=0
systemctl stop took 4.045s
=== KillSignal=SIGINT
in-flight request: 000 after 1.006153s
curl exit=52
systemctl stop took 0.010s
SIGTERM'de (terminate sinyali, "kapanmaya hazırlan") Kestrel yeni bağlantı almayı bıraktı, açık isteğin bitmesini bekledi (stop 4 saniye sürdü) ve client 200 aldı. SIGINT'te process anında gitti, client boş yanıt aldı. Sebep AddSystemd()'nin kullandığı lifetime: systemd altında yalnız SIGTERM'i graceful shutdown sinyali olarak dinliyor; terminalde Ctrl+C ile gelen SIGINT'i yakalayan console lifetime devrede değil. Microsoft'un Nginx dokümanı da bunu açıkça yazıyor: .NET'in lifetime entegrasyonları graceful shutdown için SIGTERM'i dinliyor, KillSignal=SIGINT'ten kaçın. SIGTERM zaten systemd'nin varsayılanı; satırı yazmak yalnız niyeti belgeliyor. TimeoutStopSec uzun istekler ve background job'lar için yeterli olmalı; bu süre dolunca systemd SIGKILL gönderiyor. Uygulama tarafındaki shutdown süresi ve BackgroundService'lerin bu sırada nasıl durduğu Background Job'lar ve Scheduled Job'lar serisinin "IHostedService ve BackgroundService" yazısında.
Process çökerse Restart=always devreye giriyor. kill -9 ile simüle ettim:
Nov 08 04:33:04 bookstore-vm systemd[1]: bookstore-api.service: Main process exited, code=killed, status=9/KILL
Nov 08 04:33:04 bookstore-vm systemd[1]: bookstore-api.service: Failed with result 'signal'.
Nov 08 04:33:14 bookstore-vm systemd[1]: bookstore-api.service: Scheduled restart job, restart counter is at 1.
Nov 08 04:33:14 bookstore-vm systemd[1]: Starting bookstore-api.service - Bookstore API (ASP.NET Core on Kestrel)...
Nov 08 04:33:15 bookstore-vm bookstore-api[770]: Microsoft.Hosting.Lifetime[14] Now listening on: http://127.0.0.1:5000
Nov 08 04:33:15 bookstore-vm bookstore-api[770]: Microsoft.Hosting.Lifetime[0] Application started. Hosting environment: Production; Content root path: /var/www/bookstore/releases/2026-11-08-1
Nov 08 04:33:15 bookstore-vm systemd[1]: Started bookstore-api.service - Bookstore API (ASP.NET Core on Kestrel).
$ systemctl show bookstore-api -p NRestarts
NRestarts=1
10 saniyelik bekleme, sürekli çöken bir uygulamanın CPU'yu (Central Processing Unit) yakmasını engelliyor. NRestarts değerini monitoring'e bağla: sessizce yeniden başlayan bir service, kimsenin fark etmediği bir hata demek.
Hardening: systemd-analyze security
Ayrı bir kullanıcıyla çalışmak ilk adım. systemd, process'in dosya sistemine, kernel'a ve network'e erişimini çok daha fazla daraltabiliyor; ne kadar açık olduğunu da kendisi puanlıyor:
$ systemd-analyze security bookstore-api --no-pager | tail -1
→ Overall exposure level for bookstore-api.service: 9.2 UNSAFE :-{
Ayarları unit dosyasına dokunmadan bir drop-in dosyasıyla ekledim (/etc/systemd/system/bookstore-api.service.d/hardening.conf):
[Service]
NoNewPrivileges=true
ProtectSystem=strict
ProtectHome=true
PrivateTmp=true
PrivateDevices=true
ProtectKernelTunables=true
ProtectKernelModules=true
ProtectControlGroups=true
RestrictAddressFamilies=AF_UNIX AF_INET AF_INET6
RestrictNamespaces=true
LockPersonality=true
CapabilityBoundingSet=
ReadWritePaths=/var/lib/bookstore
$ sudo systemctl daemon-reload && sudo systemctl restart bookstore-api
$ systemctl is-active bookstore-api
active
$ curl -s -o /dev/null -w "%{http_code}\n" http://127.0.0.1:5000/api/books
200
$ curl -s "http://127.0.0.1:5000/api/dp/protect?text=cart-42" | cut -c1-20
CfDJ8KUZd-p5zgJMqmuY
$ systemd-analyze security bookstore-api --no-pager | tail -1
→ Overall exposure level for bookstore-api.service: 3.8 OK :-)
ProtectSystem=strict bütün dosya sistemini salt okunur yapıyor; uygulamanın yazması gereken tek yer Data Protection key'leri, o da ReadWritePaths ile açık. NoNewPrivileges process'in ve çocuklarının yetki kazanmasını, PrivateTmp başka service'lerin /tmp'sini görmesini, boş CapabilityBoundingSet bütün Linux capability'lerini engelliyor. Uygulama 5000 portunu dinlediği için (1024'ün altı değil) hiçbir capability'ye ihtiyacı yok. Her ayardan sonra uygulamanın yazmaya çalıştığı bir yer olup olmadığına bak: log dosyası, upload klasörü, cache. "Read-only file system" hatası, eksik bir ReadWritePaths demek. Aynı unit yapısı Worker Service'ler için de geçerli; Background Job'lar serisinin "Worker Service ve In-Process Job Queue" yazısı oradan devam ediyor.
Deploy: releases/ Klasörü ve current Symlink'i
Yeni sürümü çalışan klasörün üstüne kopyalamak iki sorun yaratıyor: kopyalama sırasında uygulama yarısı eski yarısı yeni dosyalarla çalışabilir ve geri dönmek için eski dosyalar artık yok. Her sürüm kendi klasörüne gidiyor, current symlink'i yalnız çalışanı gösteriyor:
#!/bin/bash
# deploy.sh <release-adı> <publish-klasörü>: kopyala, symlink'i çevir, restart, smoke test
set -euo pipefail
release=$1; src=$2
base=/var/www/bookstore
mkdir -p "$base/releases/$release"
cp -r "$src/." "$base/releases/$release/"
ln -sfn "$base/releases/$release" "$base/current.tmp"
mv -T "$base/current.tmp" "$base/current"
systemctl restart bookstore-api
curl -sf -o /dev/null http://127.0.0.1:5000/health && echo "deployed $release: healthy"
Symlink'i doğrudan ln -sfn ile değiştirmek yerine geçici bir link oluşturup mv -T ile yerine taşımak, değişikliği tek bir atomik rename işlemine çeviriyor; hiçbir an current'ın olmadığı bir durum yok.
$ time deploy.sh 2026-11-08-2 /mnt/out/pub-fdd
deployed 2026-11-08-2: healthy
took 0.200s
$ readlink /var/www/bookstore/current
/var/www/bookstore/releases/2026-11-08-2
# geri dönüş: link'i eski klasöre çevir, restart
$ time (ln -sfn /var/www/bookstore/releases/2026-11-08-1 /var/www/bookstore/current.tmp && mv -T /var/www/bookstore/current.tmp /var/www/bookstore/current && systemctl restart bookstore-api)
took 0.168s
$ journalctl -u bookstore-api --no-pager | grep "Content root" | tail -1
Nov 08 04:33:46 bookstore-vm bookstore-api[952]: Microsoft.Hosting.Lifetime[0] Application started. Hosting environment: Production; Content root path: /var/www/bookstore/releases/2026-11-08-1
Geri dönüş 0,17 saniye; eski sürüm diskte hazır bekliyor. Type=notify sayesinde systemctl restart uygulama hazır olana kadar döndü, smoke test hemen geçti. Ama restart'ın kendisi kısa bir kesinti: o arada gelen istekleri Nginx 502 ile karşılıyor. Kesintisiz deploy için iki instance ve Nginx'te sırayla drain, ya da blue-green; bunlar ilerideki Deployment Stratejileri serisinin konusu. Eski release klasörlerini birkaç sürüm tutup gerisini silen bir temizlik adımını da script'e ekle.
Üretimde Dikkat
Secret'ları unit dosyasına yazma. systemctl show bookstore-api -p Environment unit'teki Environment satırlarını herkese gösterir; EnvironmentFile ile verilenler ise dosyanın izinleriyle korunur (chmod 600, sahibi root). Değerde boşluk ya da özel karakter varsa Microsoft'un önerdiği systemd-escape'i kullan.
Log'ların nereye gittiğini bil. journald varsayılan olarak disk kotasıyla döndürüyor ve eski kayıtları siliyor; uzun saklama ve arama için log'ları merkezi bir yere göndermen gerekiyor. .NET'te Logging serisinin "Serilog Sink'leri" yazısı bu yolu anlatıyor; journald'ın kendisi yalnız yerel tampon.
Runtime'ı güncellediğinde uygulamayı yeniden başlat. Microsoft'un dokümanındaki son not tam olarak bu: shared framework güncellendiğinde çalışan process eski dosyaları kullanmaya devam ediyor. apt upgrade sonrası systemctl restart bookstore-api bakım prosedürünün parçası olmalı.
Sık Düşülen Tuzaklar
appsettings.Production.json'un adını yanlış yazmak. Linux'un dosya sistemi büyük/küçük harfe duyarlı:
appsettings.production.json -> "instance":"bookstore-vm"
appsettings.Production.json -> "instance":"from-appsettings"
Küçük harfli dosya Windows'ta okunuyor, Linux'ta sessizce yok sayılıyor. Uygulama hata vermiyor; production ayarları yerine varsayılanlarla çalışıyor.
Service'i root ile çalıştırmak. User= satırı yoksa systemd service'i root ile başlatır; uygulamadaki bir açık, sunucunun tamamına erişim demek olur.
KillSignal=SIGINT. Eski rehberlerden kalan bu satır her deploy'da açık istekleri kesiyor ve systemd bunu "success" olarak raporluyor.
Unix socket'i /tmp ya da elle oluşturulan bir klasörde tutmak. Process çökünce socket dosyası kalıyor ve bir sonraki başlatma "address already in use" ile düşüyor; serinin ikinci yazısındaki container deneyinde bunu yaşadım. RuntimeDirectory=bookstore ile systemd /run/bookstore'u service başlarken service kullanıcısına ait olarak oluşturuyor ve durduğunda siliyor; bu container'da process'i SIGKILL ile öldürdüğümde klasör kaldırıldı ve yeniden başlatma sorunsuz oldu.
Yeni sürümü çalışan klasörün üstüne kopyalamak. Geri dönüş yolu yok ve kopyalama sırasında uygulama karışık dosyalarla çalışabilir.
Küçük Bir Deneme
★☆☆ Unit dosyasından User= ve Group= satırlarını kaldır, systemd-analyze security puanına ve ps çıktısına bak. Sonra satırları DynamicUser=yes ile değiştir; Data Protection key'leri için StateDirectory=bookstore kullan ve key'lerin nereye yazıldığını bul.
★★☆ TimeoutStopSec=3 yap ve 10 saniyelik bir rapor isteği sırasında systemctl stop çalıştır. journal'da ne görüyorsun? Uygulama tarafında HostOptions.ShutdownTimeout'u da değiştirip iki sürenin birbiriyle ilişkisini açıkla.
★★★ Self-contained single-file çıktıyla aynı unit'i kur (ExecStart=/var/www/bookstore/current/Bookstore.Api), sunucudan aspnetcore-runtime-10.0'ı kaldır ve hardening ayarlarıyla çalıştığını doğrula. Single-file executable'ın başlangıçta bir şey çıkardığı (extract) bir klasör var mı, ProtectSystem=strict buna izin veriyor mu?
Linux tarafı tamam: Nginx önde, Kestrel systemd altında, deploy ve geri dönüş birkaç komut. Bir sonraki yazıda aynı uygulamayı Windows Server'a taşıyoruz: IIS, Hosting Bundle, ASP.NET Core Module, in-process ve out-of-process hosting ve 500.30 gibi hataların ne anlattığı.