Pro webovou aplikaci nebo bota na linuxovém serveru je správný dotnet publish téměř vždy ten obyčejný: framework-dependent, konfigurace Release, výstupní složka a spuštění přes dotnet App.dll. Výstup je nejmenší, bezpečnostní záplaty runtime se projeví bez nového buildu a nemá žádné z kompatibilitních pastí těch pokročilejších režimů. Self-contained je správná volba, když na runtime na serveru nemáš vliv. Single-file, trimming, ReadyToRun a Native AOT řeší vždy jeden konkrétní problém - dobu startu, počet souborů, velikost - a každý něco stojí, takže je zapínej proto, že ten problém máš, ne proto, že ten přepínač existuje.
Tento článek vysvětluje, co která volba skutečně mění ve výstupní složce a kde se rozbije.
Stručná odpověď#
| Režim | Přidá do příkazu | Potřebuje runtime na serveru | Typické použití |
|---|---|---|---|
| Framework-dependent | nic | ano, shodnou major verzi | Většina webových aplikací a botů |
| Self-contained | -r linux-x64 --self-contained | ne | Runtime na serveru je špatný nebo neznámý |
| Single-file | -p:PublishSingleFile=true | podle předchozích | Jeden soubor k přenášení |
| Trimmed | -p:PublishTrimmed=true | ne (jen self-contained) | Důležitá velikost, kód bezpečný pro trimming |
| ReadyToRun | -p:PublishReadyToRun=true | podle situace | Rychlejší studený start |
| Native AOT | -p:PublishAot=true | ne | Rychlý start, nízká paměť, omezený kód |
Volby se dají kombinovat. Self-contained, single-file build s ReadyToRun pro linux-x64 je naprosto běžná věc. Zbytek článku popisuje, co jednotlivé sloupce znamenají v praxi.
Co dotnet publish vytvoří#
Začni s výchozím nastavením a podívej se do složky:
$ dotnet publish src/Api/Api.csproj -c Release -o out$ ls outApi Api.deps.json Api.dll Api.pdb Api.runtimeconfig.jsonappsettings.json appsettings.Development.json web.config ...Každý z těchto souborů má svůj úkol:
Api.dllje tvůj kód zkompilovaný do IL - mezijazyka, který JIT runtime překládá do strojového kódu při prvním volání metod.Api.deps.jsonvypisuje všechny závislosti a kde je najít. Smaž ho a host se vrátí k hledání ve složce, což většinou funguje, dokud nefunguje.Api.runtimeconfig.jsonříká, jaký sdílený framework načíst, třebaMicrosoft.AspNetCore.App10.0, a obsahuje nastavení runtime, například režim GC.Api(na WindowsApi.exe) je apphost - malý nativní spouštěč pro platformu, na které jsi build dělal../Apiadotnet Api.dlldělají totéž. Pokud aplikaci vždy spouštíš přesdotnet, vypni ho pomocí-p:UseAppHost=false.- Závislosti z NuGetu se kopírují jako samostatné DLL. Framework samotný ne.
web.configje pro IIS a všude jinde se ignoruje.
Od .NET 8 používá dotnet publish u projektů cílících na .NET 8 a novější ve výchozím stavu konfiguraci Release. Psát -c Release explicitně je ale pořád dobrý zvyk u starších projektů i ve build skriptech, které si někdo později přečte.
Framework-dependent a jak funguje roll-forward#
Výchozí výstup závisí na tom, že je tam, kde běží, nainstalovaný sdílený runtime .NET. Požadavek na verzi je v runtimeconfig.json:
{ "runtimeOptions": { "tfm": "net10.0", "framework": { "name": "Microsoft.AspNetCore.App", "version": "10.0.0" } }}version je minimum a runtime podle politiky roll-forward hledá, kterou verzi použije. Výchozí politika Minor dělá dvě věci: vždy vybere nejnovější nainstalovanou záplatu požadované verze (takže 10.0.0 poběží na 10.0.7, pokud je k dispozici) a pokud neexistuje vůbec žádné 10.0.x, přijme novější minor verzi. Přes hranici major verze nepřeskočí: aplikace sestavená pro .NET 8 se nespustí na stroji, který má jen .NET 10, a skončí hláškou „You must install or update .NET to run this application“.
Můžeš to změnit pomocí <RollForward>Major</RollForward> v projektu nebo proměnné prostředí DOTNET_ROLL_FORWARD a aplikace pro 8 pak poběží na 10. Obvykle to funguje, protože zásadní změny mezi major verzemi jsou zdokumentované a většinou malé, jenže pak běžíš na runtime, který jsi nikdy netestoval. Poctivé řešení je změnit cílovou verzi a znovu sestavit.
Skutečná výhoda framework-dependent výstupu je záplatování. Když hostitel nainstaluje bezpečnostní aktualizaci .NET, aplikace ji převezme při dalším restartu bez nového buildu. Self-contained aplikace nese vlastní kopii runtime a drží si úroveň záplat, se kterou byla sestavena, dokud znovu nepublikuješ.
Self-contained a runtime identifikátory#
Self-contained build zkopíruje runtime do výstupní složky, takže server nepotřebuje nainstalované .NET vůbec:
$ dotnet publish -c Release -r linux-x64 --self-contained -o outSelf-contained aplikace je svázaná s jednou platformou, kterou určuje runtime identifikátor (RID):
| RID | Platforma |
|---|---|
linux-x64 | 64bitový Intel/AMD Linux s glibc - většina serverů |
linux-arm64 | 64bitový ARM Linux s glibc |
linux-musl-x64 | Alpine a další distribuce založené na musl |
win-x64 | 64bitové Windows |
osx-arm64 | macOS na Apple silicon |
Od .NET 8 používá SDK ve výchozím stavu přenosné RID jako tyto; RID specifické pro distribuci, například ubuntu.22.04-x64, už cílit nemáš. Také od .NET 8 už samotné -r neznamená self-contained - dostaneš framework-dependent build pro danou platformu. Napiš --self-contained (nebo --self-contained false) explicitně, aby si nikdo nemusel pamatovat, které SDK co změnilo.
Self-contained nezahrnuje nativní knihovny operačního systému. Runtime pořád potřebuje glibc (u musl RID musl), OpenSSL pro TLS a ICU pro zpracování řetězců podle kultury. Na minimálním linuxovém image bez ICU se aplikace .NET zastaví při startu s hláškou „Couldn't find a valid ICU package installed on the system“. Pokud tvoje aplikace kulturně specifické formátování opravdu nepotřebuje - většina API, která mluví JSON a ukládají časy v UTC, ho nepotřebuje - zapni invariantní globalizaci:
<PropertyGroup> <InvariantGlobalization>true</InvariantGlobalization></PropertyGroup>Pak se všechny kultury chovají jako invariantní kultura: ToUpper na tureckém textu a formáty data v de-DE nebudou dodržovat místní pravidla. Některé databázové ovladače mají k tomu také své výhrady; starší verze Microsoft.Data.SqlClient odmítaly v invariantním režimu otevřít spojení, takže to vyzkoušej se svým skutečným stackem.
Cenou za self-contained je velikost: runtime a u webové aplikace i ASP.NET Core přidají zhruba 70 až 100 MB podle verze, oproti několika megabajtům u framework-dependent buildu. Na plánu s 5 GB disku to tolik nevadí, jak to zní, ale vadí to, pokud si necháváš víc buildů najednou.
Single-file#
PublishSingleFile zabalí aplikaci do jednoho spustitelného souboru. Vyžaduje RID a funguje u framework-dependent i self-contained buildů:
$ dotnet publish -c Release -r linux-x64 --self-contained \ -p:PublishSingleFile=true -o outNa tři detaily lidé narážejí.
Některé soubory zůstávají zvlášť. Spravované assembly jdou do balíčku. Nativní knihovny zůstanou vedle spustitelného souboru, pokud nepřidáš -p:IncludeNativeLibrariesForSelfExtract=true; v tom případě se při prvním spuštění rozbalí do dočasného adresáře (řídí ho DOTNET_BUNDLE_EXTRACT_BASE_DIR). Soubory s obsahem jako appsettings.json a wwwroot se publikují vedle spustitelného souboru, ne dovnitř něj, pokud nenastavíš IncludeAllContentForSelfExtract, což u konfiguračního souboru, který chceš upravovat, málokdy chceš.
`Assembly.Location` je prázdné. Kód, který si hledá vlastní složku přes typeof(Program).Assembly.Location, dostane uvnitř balíčku prázdný řetězec. Použij místo toho AppContext.BaseDirectory; kompilátor na to upozorní varováním IL3000, pokud máš zapnuté analyzátory.
Není menší ani rychlejší. Jeden soubor jsou stejné bajty v jedné nádobě. Hodí se pro nástroj příkazové řádky, který předáváš dalším lidem. U serverové aplikace nasazované z Gitu se složka spouští stejně snadno jako soubor, takže single-file řeší problém, který nemáš.
Trimming#
Trimming odstraní z frameworku a tvých závislostí kód, který aplikace nepoužívá. Funguje jen u self-contained buildů, protože sdílený framework nejde oříznout pro jednu aplikaci.
<PropertyGroup> <PublishTrimmed>true</PublishTrimmed></PropertyGroup>Trimmer pracuje statickou analýzou: sleduje volání od vstupního bodu a odstraní, k čemu se nedostane. Reflexe ho oklame. Cokoli, co objevuje typy až za běhu - klasické objevování controllerů v MVC, Razor views, serializace System.Text.Json bez source generation, většina dependency injection podle konvencí, starší ORM - může přijít o kód, který potřebuje, a chyba se projeví za běhu jako MissingMethodException nebo typ, který se tiše deserializuje na nic.
SDK ukazuje, kde je riziko, pomocí varování trimmeru (IL2026, IL2070 a další). Ber je jako chyby. Pokud build vyprodukuje desítky takových varování z knihovny, kterou neovládáš, trimming pro tu aplikaci není.
V praxi: minimální API se source-generated JSON se trimuje dobře. Aplikace MVC s Razor views nebo cokoli, co hodně používá EF Core, je špatný kandidát a dokumentace Microsoftu to u těchto frameworků říká výslovně. Úspora je skutečná - často polovina velikosti self-contained buildu i víc - ale na serveru málokdy disk představuje omezení, na kterém záleží.
ReadyToRun a tiered compilation#
Běžný kód .NET překládá do strojového kódu JIT za běhu. Start proto tráví čas kompilací, což na pomalém CPU tvoří velkou část prvních sekund. ReadyToRun (R2R) kompiluje předem a ukládá nativní kód vedle IL:
$ dotnet publish -c Release -r linux-x64 -p:PublishReadyToRun=true -o outPotřebuje RID, protože nativní kód je specifický pro platformu. Assembly se zvětší, často na dvoj- až trojnásobek velikosti IL. JIT tam pořád je: tiered compilation bere kód z R2R jako výchozí úroveň a horké metody překompiluje s plnou optimalizací podle dynamických profilových dat (dynamic PGO, od .NET 8 ve výchozím stavu zapnuté), takže výkon v ustáleném stavu vyjde stejně jako bez R2R.
R2R přináší studený start. Na serveru s půl vCPU, kde je limit CPU tvrdé omezení a ne doporučení, je práce JIT při startu znatelně pomalá a R2R může ubrat sekundy z doby mezi „proces se spustil“ a „první požadavek je obsloužen“. Pokud se aplikace restartuje při každém nasazení a na té prodlevě ti záleží, je to nejlevnější výhra na tomto seznamu a nestojí nic na kompatibilitě. Pokud se aplikace spouští jednou týdně, disk za to nestojí.
Native AOT#
Native AOT zkompiluje celou aplikaci včetně runtime do jednoho nativního spustitelného souboru, bez JIT:
<PropertyGroup> <PublishAot>true</PublishAot></PropertyGroup>Výsledky jsou tam, kde to jde použít, dramatické: start v desítkách milisekund, menší binárka než u trimmed self-contained a nižší paměť v klidu. Omezení jsou ta od trimmingu, jen absolutní: žádné generování kódu za běhu, žádné dynamické načítání assembly, reflexe jen tam, kam kompilátor dohlédne. ASP.NET Core to podporuje pro minimální API a gRPC, s WebApplication.CreateSlimBuilder a source-generated JSON; šablona webapiaot je na to připravená. MVC, Razor Pages a Blazor Server podporované nejsou. Podpora AOT v EF Core je stále omezená a v posledních verzích označená jako experimentální.
Dvě praktická omezení: Native AOT potřebuje při buildu nativní toolchain platformy (na Linuxu clang a vývojové hlavičky zlib) a nekompiluje napříč operačními systémy - linuxové binárky sestavuj na Linuxu. Build na malém plánu přímo na serveru je pomalý a hladový po paměti; to je práce pro CI.
Native AOT je opravdu dobrý pro malé, úzce zaměřené služby a nástroje příkazové řádky. Pro typickou firemní webovou aplikaci stojí omezení víc, než kolik ušetří zrychlení startu.
Volba pro malý server#
Takhle volby dopadnou na reálném případě: ASP.NET Core API s EF Core, nasazené z GitHubu na plán s 1 GB a půl vCPU.
- Začni s framework-dependent.
dotnet publish -c Release -o outadotnet out/Api.dll. Nejmenší výstup, záplatovaný spolu s runtime, žádná překvapení. - Pokud má server špatnou major verzi runtime, publikuj místo bojování s roll-forward self-contained pro
linux-x64. Pamatuj, že záplatování runtime je teď na tobě: znovu sestav, když vyjde bezpečnostní aktualizace .NET. - Pokud jsou restarty pomalé, přidej jako první ReadyToRun. Nemá žádné riziko pro kompatibilitu.
- Trimming a AOT nech u této aplikace vypnuté. EF Core a stack postavený na reflexi vyprodukují varování, která nelze bezpečně ignorovat.
- Single-file vynech. Nic nepřidá, když je nasazení git pull.
Na tom, kde se build dělá, záleží stejně jako na tom, jak. Build na serveru znamená, že SDK, MSBuild a kompilátor při každém spuštění běží uvnitř limitu paměti plánu, a na RE:NODE se proces, který dosáhne limitu paměti, zastaví a kontejner se čistě restartuje místo swapování - takže velký build na nejmenším plánu může spadnout v půlce. Build v GitHub Actions a nasazení výstupu (z větve, kterou server sleduje) tuto práci ze serveru úplně přesune. Nasazení aplikace ASP.NET Core z GitHubu popisuje obě varianty a paměť a garbage collection v .NET vysvětluje, kolik bude aplikace sama používat, až poběží.
Stejná rozhodnutí platí i mimo web: worker služba nebo bot se publikuje úplně stejně a worker služby a úlohy na pozadí v .NET popisují hostitelský model pro ně.
FAQ#
Je self-contained rychlejší než framework-dependent?
Ne. Je to stejný runtime a stejný JIT; mění se jen to, odkud se soubory berou. Rychlost startu přináší ReadyToRun nebo Native AOT, ne přibalení runtime.
Proč moje publikovaná aplikace chce verzi .NET, o které jsem myslel, že ji mám?
Server má nainstalovanou jinou major verzi, než je v runtimeconfig.json, a výchozí politika roll-forward přes hranici major verze nepřeskočí. Změň cílovou verzi, publikuj self-contained, nebo nastav RollForward záměrně.
Můžu publikovat na Windows a spouštět na Linuxu?
Ano, u všeho kromě Native AOT. IL ve framework-dependent výstupu je nezávislé na platformě a self-contained build pro linux-x64 jde vytvořit i na Windows. Liší se jen apphost a aplikaci můžeš místo něj spouštět přes dotnet App.dll.
Potřebuju SDK na serveru?
Jen pokud tam buildíš. Ke spuštění publikované aplikace stačí runtime, nebo vůbec nic, pokud je self-contained. Držet SDK mimo běžící prostředí je jeden z důvodů, proč buildovat v CI.
Mám výstup publish commitovat do Gitu?
Ne do hlavní větve. Pokud nasazuješ předem sestavený výstup, drž ho na samostatné větvi zapisované z CI, aby historie zdrojů zůstala čitelná. Verze .NET a podpora LTS popisuje udržování aktuální cílové verze frameworku, což je druhá půlka stabilního buildu.




Komentáře
Zcela anonymně: žádný účet, žádný e-mail, žádné cookie. Ukládáme jen jméno, které napíšeš, text a čas - nic dalšího. Počet odkazů je omezený a značkování se nevykresluje.