[{"data":1,"prerenderedAt":1378},["ShallowReactive",2],{"blog-\u002Fblog\u002Fdotnet-deployment":3},{"id":4,"title":5,"body":6,"description":1364,"difficulty":1365,"extension":1366,"framework":1367,"frameworkSlug":53,"meta":1368,"navigation":76,"order":67,"path":1369,"qaPath":1370,"seo":1371,"stem":1372,"subtopic":1373,"topic":1374,"topicSlug":1375,"updated":1376,"__hash__":1377},"blog\u002Fblog\u002Fdotnet-deployment.md","Deploying ASP.NET Core to Docker, Kubernetes, and Azure",{"type":7,"value":8,"toc":1352},"minimark",[9,14,18,22,29,178,185,189,196,247,250,283,287,290,438,441,448,452,455,477,525,528,585,610,614,617,696,738,749,753,756,819,826,851,855,1020,1027,1031,1034,1185,1188,1192,1195,1339,1342,1348],[10,11,13],"h2",{"id":12},"why-deployment-knowledge-matters-in-net-interviews","Why deployment knowledge matters in .NET interviews",[15,16,17],"p",{},"Senior .NET interviews regularly include infrastructure and operational questions:\n\"How would you containerise this?\" \"What's the difference between liveness and\nreadiness probes?\" \"How do you manage secrets in production?\" Deployment knowledge\ndistinguishes engineers who can take code all the way to production from those\nwho hand it off at a PR. This article covers the full deployment surface.",[10,19,21],{"id":20},"dotnet-publish-choosing-the-right-output-mode","dotnet publish: choosing the right output mode",[15,23,24,28],{},[25,26,27],"code",{},"dotnet publish"," has three deployment models, each trading binary size for\nportability:",[30,31,36],"pre",{"className":32,"code":33,"language":34,"meta":35,"style":35},"language-bash shiki shiki-themes github-light github-dark","# Framework-dependent (default) — requires .NET runtime on host; small output:\ndotnet publish -c Release\n# Output: ~500 KB of app DLLs\n\n# Self-contained — includes the runtime; works on bare hosts:\ndotnet publish -c Release --self-contained -r linux-x64\n# Output: ~70 MB\n\n# Single-file — everything in one executable:\ndotnet publish -c Release --self-contained -r linux-x64 -p:PublishSingleFile=true\n# Output: one ~70 MB file\n\n# ReadyToRun — ahead-of-time compilation for faster startup:\ndotnet publish -c Release -r linux-x64 -p:PublishReadyToRun=true\n","bash","",[25,37,38,47,65,71,78,84,105,111,116,122,143,149,154,160],{"__ignoreMap":35},[39,40,43],"span",{"class":41,"line":42},"line",1,[39,44,46],{"class":45},"sJ8bj","# Framework-dependent (default) — requires .NET runtime on host; small output:\n",[39,48,50,54,58,62],{"class":41,"line":49},2,[39,51,53],{"class":52},"sScJk","dotnet",[39,55,57],{"class":56},"sZZnC"," publish",[39,59,61],{"class":60},"sj4cs"," -c",[39,63,64],{"class":56}," Release\n",[39,66,68],{"class":41,"line":67},3,[39,69,70],{"class":45},"# Output: ~500 KB of app DLLs\n",[39,72,74],{"class":41,"line":73},4,[39,75,77],{"emptyLinePlaceholder":76},true,"\n",[39,79,81],{"class":41,"line":80},5,[39,82,83],{"class":45},"# Self-contained — includes the runtime; works on bare hosts:\n",[39,85,87,89,91,93,96,99,102],{"class":41,"line":86},6,[39,88,53],{"class":52},[39,90,57],{"class":56},[39,92,61],{"class":60},[39,94,95],{"class":56}," Release",[39,97,98],{"class":60}," --self-contained",[39,100,101],{"class":60}," -r",[39,103,104],{"class":56}," linux-x64\n",[39,106,108],{"class":41,"line":107},7,[39,109,110],{"class":45},"# Output: ~70 MB\n",[39,112,114],{"class":41,"line":113},8,[39,115,77],{"emptyLinePlaceholder":76},[39,117,119],{"class":41,"line":118},9,[39,120,121],{"class":45},"# Single-file — everything in one executable:\n",[39,123,125,127,129,131,133,135,137,140],{"class":41,"line":124},10,[39,126,53],{"class":52},[39,128,57],{"class":56},[39,130,61],{"class":60},[39,132,95],{"class":56},[39,134,98],{"class":60},[39,136,101],{"class":60},[39,138,139],{"class":56}," linux-x64",[39,141,142],{"class":60}," -p:PublishSingleFile=true\n",[39,144,146],{"class":41,"line":145},11,[39,147,148],{"class":45},"# Output: one ~70 MB file\n",[39,150,152],{"class":41,"line":151},12,[39,153,77],{"emptyLinePlaceholder":76},[39,155,157],{"class":41,"line":156},13,[39,158,159],{"class":45},"# ReadyToRun — ahead-of-time compilation for faster startup:\n",[39,161,163,165,167,169,171,173,175],{"class":41,"line":162},14,[39,164,53],{"class":52},[39,166,57],{"class":56},[39,168,61],{"class":60},[39,170,95],{"class":56},[39,172,101],{"class":60},[39,174,139],{"class":56},[39,176,177],{"class":60}," -p:PublishReadyToRun=true\n",[15,179,180,181,184],{},"For containers, self-contained is often unnecessary because the runtime is\nalready in the base image (",[25,182,183],{},"mcr.microsoft.com\u002Fdotnet\u002Faspnet","). Framework-dependent\nin a container gives a small, reproducible image.",[10,186,188],{"id":187},"kestrel-and-reverse-proxies","Kestrel and reverse proxies",[15,190,191,195],{},[192,193,194],"strong",{},"Kestrel"," is ASP.NET Core's built-in HTTP server. It handles raw TCP, TLS,\nand HTTP\u002F2 and is fast enough for production. In most deployments it sits behind\na reverse proxy (Nginx, Apache, IIS) that handles TLS termination, static files,\nand connection management.",[30,197,201],{"className":198,"code":199,"language":200,"meta":35,"style":35},"language-csharp shiki shiki-themes github-light github-dark","\u002F\u002F When behind a reverse proxy, trust forwarded headers:\napp.UseForwardedHeaders(new ForwardedHeadersOptions\n{\n    ForwardedHeaders = ForwardedHeaders.XForwardedFor | ForwardedHeaders.XForwardedProto,\n});\n\u002F\u002F Call before UseAuthentication and UseRouting\n\n\u002F\u002F Without this, Request.Scheme returns \"http\" even for HTTPS requests.\n\u002F\u002F Cookie Secure flags and redirect URLs will be wrong.\n","csharp",[25,202,203,208,213,218,223,228,233,237,242],{"__ignoreMap":35},[39,204,205],{"class":41,"line":42},[39,206,207],{},"\u002F\u002F When behind a reverse proxy, trust forwarded headers:\n",[39,209,210],{"class":41,"line":49},[39,211,212],{},"app.UseForwardedHeaders(new ForwardedHeadersOptions\n",[39,214,215],{"class":41,"line":67},[39,216,217],{},"{\n",[39,219,220],{"class":41,"line":73},[39,221,222],{},"    ForwardedHeaders = ForwardedHeaders.XForwardedFor | ForwardedHeaders.XForwardedProto,\n",[39,224,225],{"class":41,"line":80},[39,226,227],{},"});\n",[39,229,230],{"class":41,"line":86},[39,231,232],{},"\u002F\u002F Call before UseAuthentication and UseRouting\n",[39,234,235],{"class":41,"line":107},[39,236,77],{"emptyLinePlaceholder":76},[39,238,239],{"class":41,"line":113},[39,240,241],{},"\u002F\u002F Without this, Request.Scheme returns \"http\" even for HTTPS requests.\n",[39,243,244],{"class":41,"line":118},[39,245,246],{},"\u002F\u002F Cookie Secure flags and redirect URLs will be wrong.\n",[15,248,249],{},"Kestrel limits should also be tuned in production:",[30,251,253],{"className":198,"code":252,"language":200,"meta":35,"style":35},"builder.WebHost.ConfigureKestrel(opts =>\n{\n    opts.Limits.MaxConcurrentConnections = 1000;\n    opts.Limits.MaxRequestBodySize       = 10 * 1024 * 1024;\n    opts.Limits.RequestHeadersTimeout    = TimeSpan.FromSeconds(30);\n});\n",[25,254,255,260,264,269,274,279],{"__ignoreMap":35},[39,256,257],{"class":41,"line":42},[39,258,259],{},"builder.WebHost.ConfigureKestrel(opts =>\n",[39,261,262],{"class":41,"line":49},[39,263,217],{},[39,265,266],{"class":41,"line":67},[39,267,268],{},"    opts.Limits.MaxConcurrentConnections = 1000;\n",[39,270,271],{"class":41,"line":73},[39,272,273],{},"    opts.Limits.MaxRequestBodySize       = 10 * 1024 * 1024;\n",[39,275,276],{"class":41,"line":80},[39,277,278],{},"    opts.Limits.RequestHeadersTimeout    = TimeSpan.FromSeconds(30);\n",[39,280,281],{"class":41,"line":86},[39,282,227],{},[10,284,286],{"id":285},"multi-stage-dockerfile","Multi-stage Dockerfile",[15,288,289],{},"A multi-stage build uses the SDK image to compile and the smaller runtime image\nto run. The final image contains no build tools, reducing the attack surface and\nimage size.",[30,291,295],{"className":292,"code":293,"language":294,"meta":35,"style":35},"language-dockerfile shiki shiki-themes github-light github-dark","# Stage 1: build\nFROM mcr.microsoft.com\u002Fdotnet\u002Fsdk:8.0 AS build\nWORKDIR \u002Fsrc\n\n# Copy project file first — Docker caches the restore layer until .csproj changes:\nCOPY [\"MyApp\u002FMyApp.csproj\", \"MyApp\u002F\"]\nRUN dotnet restore \"MyApp\u002FMyApp.csproj\"\n\nCOPY . .\nWORKDIR \u002Fsrc\u002FMyApp\nRUN dotnet publish \"MyApp.csproj\" -c Release -o \u002Fapp\u002Fpublish --no-restore\n\n# Stage 2: runtime\nFROM mcr.microsoft.com\u002Fdotnet\u002Faspnet:8.0 AS final\nWORKDIR \u002Fapp\n\n# Run as non-root user:\nRUN useradd --uid 1001 --no-create-home appuser\nUSER appuser\n\nCOPY --from=build \u002Fapp\u002Fpublish .\n\nENV ASPNETCORE_ENVIRONMENT=Production\nENV ASPNETCORE_URLS=http:\u002F\u002F+:8080\n\nEXPOSE 8080\nENTRYPOINT [\"dotnet\", \"MyApp.dll\"]\n","dockerfile",[25,296,297,302,307,312,316,321,326,331,335,340,345,350,354,359,364,370,375,381,387,393,398,404,409,415,421,426,432],{"__ignoreMap":35},[39,298,299],{"class":41,"line":42},[39,300,301],{},"# Stage 1: build\n",[39,303,304],{"class":41,"line":49},[39,305,306],{},"FROM mcr.microsoft.com\u002Fdotnet\u002Fsdk:8.0 AS build\n",[39,308,309],{"class":41,"line":67},[39,310,311],{},"WORKDIR \u002Fsrc\n",[39,313,314],{"class":41,"line":73},[39,315,77],{"emptyLinePlaceholder":76},[39,317,318],{"class":41,"line":80},[39,319,320],{},"# Copy project file first — Docker caches the restore layer until .csproj changes:\n",[39,322,323],{"class":41,"line":86},[39,324,325],{},"COPY [\"MyApp\u002FMyApp.csproj\", \"MyApp\u002F\"]\n",[39,327,328],{"class":41,"line":107},[39,329,330],{},"RUN dotnet restore \"MyApp\u002FMyApp.csproj\"\n",[39,332,333],{"class":41,"line":113},[39,334,77],{"emptyLinePlaceholder":76},[39,336,337],{"class":41,"line":118},[39,338,339],{},"COPY . .\n",[39,341,342],{"class":41,"line":124},[39,343,344],{},"WORKDIR \u002Fsrc\u002FMyApp\n",[39,346,347],{"class":41,"line":145},[39,348,349],{},"RUN dotnet publish \"MyApp.csproj\" -c Release -o \u002Fapp\u002Fpublish --no-restore\n",[39,351,352],{"class":41,"line":151},[39,353,77],{"emptyLinePlaceholder":76},[39,355,356],{"class":41,"line":156},[39,357,358],{},"# Stage 2: runtime\n",[39,360,361],{"class":41,"line":162},[39,362,363],{},"FROM mcr.microsoft.com\u002Fdotnet\u002Faspnet:8.0 AS final\n",[39,365,367],{"class":41,"line":366},15,[39,368,369],{},"WORKDIR \u002Fapp\n",[39,371,373],{"class":41,"line":372},16,[39,374,77],{"emptyLinePlaceholder":76},[39,376,378],{"class":41,"line":377},17,[39,379,380],{},"# Run as non-root user:\n",[39,382,384],{"class":41,"line":383},18,[39,385,386],{},"RUN useradd --uid 1001 --no-create-home appuser\n",[39,388,390],{"class":41,"line":389},19,[39,391,392],{},"USER appuser\n",[39,394,396],{"class":41,"line":395},20,[39,397,77],{"emptyLinePlaceholder":76},[39,399,401],{"class":41,"line":400},21,[39,402,403],{},"COPY --from=build \u002Fapp\u002Fpublish .\n",[39,405,407],{"class":41,"line":406},22,[39,408,77],{"emptyLinePlaceholder":76},[39,410,412],{"class":41,"line":411},23,[39,413,414],{},"ENV ASPNETCORE_ENVIRONMENT=Production\n",[39,416,418],{"class":41,"line":417},24,[39,419,420],{},"ENV ASPNETCORE_URLS=http:\u002F\u002F+:8080\n",[39,422,424],{"class":41,"line":423},25,[39,425,77],{"emptyLinePlaceholder":76},[39,427,429],{"class":41,"line":428},26,[39,430,431],{},"EXPOSE 8080\n",[39,433,435],{"class":41,"line":434},27,[39,436,437],{},"ENTRYPOINT [\"dotnet\", \"MyApp.dll\"]\n",[15,439,440],{},"SDK image: ~750 MB. Runtime image: ~220 MB. Copying only the publish output\nkeeps the final image small and free of source code.",[15,442,443,444,447],{},"The ",[25,445,446],{},"COPY [\"MyApp.csproj\"] \u002F dotnet restore \u002F COPY . ."," sequence is critical for\nlayer caching — the restore layer only rebuilds when the project file changes,\nnot on every source change.",[10,449,451],{"id":450},"environment-configuration","Environment configuration",[15,453,454],{},"ASP.NET Core layers configuration in priority order (later sources override earlier):",[456,457,458,465,471,474],"ol",{},[459,460,461,464],"li",{},[25,462,463],{},"appsettings.json"," — base config, committed to source control",[459,466,467,470],{},[25,468,469],{},"appsettings.{Environment}.json"," — environment-specific overrides",[459,472,473],{},"Environment variables — injected by OS, Docker, or Kubernetes",[459,475,476],{},"Command-line arguments — highest priority",[30,478,480],{"className":32,"code":479,"language":34,"meta":35,"style":35},"# Environment variables use __ (double underscore) as the colon separator:\nConnectionStrings__Default=Server=prod-db;Database=app;\n\n# Equivalent JSON path: ConnectionStrings.Default\n",[25,481,482,487,516,520],{"__ignoreMap":35},[39,483,484],{"class":41,"line":42},[39,485,486],{"class":45},"# Environment variables use __ (double underscore) as the colon separator:\n",[39,488,489,493,497,500,502,505,508,510,513],{"class":41,"line":49},[39,490,492],{"class":491},"sVt8B","ConnectionStrings__Default",[39,494,496],{"class":495},"szBVR","=",[39,498,499],{"class":491},"Server",[39,501,496],{"class":495},[39,503,504],{"class":56},"prod-db",[39,506,507],{"class":491},";Database",[39,509,496],{"class":495},[39,511,512],{"class":56},"app",[39,514,515],{"class":491},";\n",[39,517,518],{"class":41,"line":67},[39,519,77],{"emptyLinePlaceholder":76},[39,521,522],{"class":41,"line":73},[39,523,524],{"class":45},"# Equivalent JSON path: ConnectionStrings.Default\n",[15,526,527],{},"Never commit secrets to source control. The mapping:",[529,530,531,544],"table",{},[532,533,534],"thead",{},[535,536,537,541],"tr",{},[538,539,540],"th",{},"Environment",[538,542,543],{},"Secret source",[545,546,547,558,569,577],"tbody",{},[535,548,549,553],{},[550,551,552],"td",{},"Development",[550,554,555],{},[25,556,557],{},"dotnet user-secrets",[535,559,560,563],{},[550,561,562],{},"Docker",[550,564,565,568],{},[25,566,567],{},"-e"," flag or Docker secrets",[535,570,571,574],{},[550,572,573],{},"Kubernetes",[550,575,576],{},"Kubernetes Secrets via env vars",[535,578,579,582],{},[550,580,581],{},"Azure",[550,583,584],{},"Azure Key Vault via Managed Identity",[30,586,588],{"className":198,"code":587,"language":200,"meta":35,"style":35},"\u002F\u002F Azure Key Vault — no credentials needed with Managed Identity:\nbuilder.Configuration.AddAzureKeyVault(\n    new Uri($\"https:\u002F\u002F{vaultName}.vault.azure.net\u002F\"),\n    new DefaultAzureCredential());\n",[25,589,590,595,600,605],{"__ignoreMap":35},[39,591,592],{"class":41,"line":42},[39,593,594],{},"\u002F\u002F Azure Key Vault — no credentials needed with Managed Identity:\n",[39,596,597],{"class":41,"line":49},[39,598,599],{},"builder.Configuration.AddAzureKeyVault(\n",[39,601,602],{"class":41,"line":67},[39,603,604],{},"    new Uri($\"https:\u002F\u002F{vaultName}.vault.azure.net\u002F\"),\n",[39,606,607],{"class":41,"line":73},[39,608,609],{},"    new DefaultAzureCredential());\n",[10,611,613],{"id":612},"kubernetes-liveness-and-readiness-probes","Kubernetes: liveness and readiness probes",[15,615,616],{},"These two probe types serve entirely different purposes and must be wired to\nseparate endpoints.",[30,618,622],{"className":619,"code":620,"language":621,"meta":35,"style":35},"language-yaml shiki shiki-themes github-light github-dark","livenessProbe:\n  httpGet:\n    path: \u002Fhealth\u002Flive    # no dependency checks — just \"is the process alive?\"\n    port: 8080\n  initialDelaySeconds: 5\n  periodSeconds: 10\n  failureThreshold: 3     # restart pod after 3 consecutive failures\n\nreadinessProbe:\n  httpGet:\n    path: \u002Fhealth\u002Fready   # checks database, Redis, etc.\n    port: 8080\n  initialDelaySeconds: 10\n  periodSeconds: 5\n  failureThreshold: 2     # remove from load balancer after 2 failures\n","yaml",[25,623,624,629,634,639,644,649,654,659,663,668,672,677,681,686,691],{"__ignoreMap":35},[39,625,626],{"class":41,"line":42},[39,627,628],{},"livenessProbe:\n",[39,630,631],{"class":41,"line":49},[39,632,633],{},"  httpGet:\n",[39,635,636],{"class":41,"line":67},[39,637,638],{},"    path: \u002Fhealth\u002Flive    # no dependency checks — just \"is the process alive?\"\n",[39,640,641],{"class":41,"line":73},[39,642,643],{},"    port: 8080\n",[39,645,646],{"class":41,"line":80},[39,647,648],{},"  initialDelaySeconds: 5\n",[39,650,651],{"class":41,"line":86},[39,652,653],{},"  periodSeconds: 10\n",[39,655,656],{"class":41,"line":107},[39,657,658],{},"  failureThreshold: 3     # restart pod after 3 consecutive failures\n",[39,660,661],{"class":41,"line":113},[39,662,77],{"emptyLinePlaceholder":76},[39,664,665],{"class":41,"line":118},[39,666,667],{},"readinessProbe:\n",[39,669,670],{"class":41,"line":124},[39,671,633],{},[39,673,674],{"class":41,"line":145},[39,675,676],{},"    path: \u002Fhealth\u002Fready   # checks database, Redis, etc.\n",[39,678,679],{"class":41,"line":151},[39,680,643],{},[39,682,683],{"class":41,"line":156},[39,684,685],{},"  initialDelaySeconds: 10\n",[39,687,688],{"class":41,"line":162},[39,689,690],{},"  periodSeconds: 5\n",[39,692,693],{"class":41,"line":366},[39,694,695],{},"  failureThreshold: 2     # remove from load balancer after 2 failures\n",[30,697,699],{"className":198,"code":698,"language":200,"meta":35,"style":35},"\u002F\u002F \u002Fhealth\u002Flive — always 200 if the process is running:\napp.MapHealthChecks(\"\u002Fhealth\u002Flive\", new HealthCheckOptions { Predicate = _ => false });\n\n\u002F\u002F \u002Fhealth\u002Fready — checks external dependencies:\napp.MapHealthChecks(\"\u002Fhealth\u002Fready\", new HealthCheckOptions\n{\n    Predicate = c => c.Tags.Contains(\"readiness\"),\n});\n",[25,700,701,706,711,715,720,725,729,734],{"__ignoreMap":35},[39,702,703],{"class":41,"line":42},[39,704,705],{},"\u002F\u002F \u002Fhealth\u002Flive — always 200 if the process is running:\n",[39,707,708],{"class":41,"line":49},[39,709,710],{},"app.MapHealthChecks(\"\u002Fhealth\u002Flive\", new HealthCheckOptions { Predicate = _ => false });\n",[39,712,713],{"class":41,"line":67},[39,714,77],{"emptyLinePlaceholder":76},[39,716,717],{"class":41,"line":73},[39,718,719],{},"\u002F\u002F \u002Fhealth\u002Fready — checks external dependencies:\n",[39,721,722],{"class":41,"line":80},[39,723,724],{},"app.MapHealthChecks(\"\u002Fhealth\u002Fready\", new HealthCheckOptions\n",[39,726,727],{"class":41,"line":86},[39,728,217],{},[39,730,731],{"class":41,"line":107},[39,732,733],{},"    Predicate = c => c.Tags.Contains(\"readiness\"),\n",[39,735,736],{"class":41,"line":113},[39,737,227],{},[15,739,740,741,744,745,748],{},"A database outage should make pods ",[192,742,743],{},"unready"," (stop receiving traffic), not\n",[192,746,747],{},"unlive"," (restart). Restarting a pod can't fix a database problem.",[10,750,752],{"id":751},"graceful-shutdown","Graceful shutdown",[15,754,755],{},"Kubernetes sends SIGTERM before forcibly killing a pod. ASP.NET Core handles\nthis automatically — it stops accepting new connections and waits for in-flight\nrequests to complete. The default timeout is 5 seconds; increase it for long\nrequests.",[30,757,759],{"className":198,"code":758,"language":200,"meta":35,"style":35},"builder.Host.ConfigureHostOptions(opts =>\n    opts.ShutdownTimeout = TimeSpan.FromSeconds(30));\n\n\u002F\u002F Background services must respect the cancellation token:\nprotected override async Task ExecuteAsync(CancellationToken stoppingToken)\n{\n    while (!stoppingToken.IsCancellationRequested)\n    {\n        var job = await _queue.DequeueAsync(stoppingToken);\n        await ProcessAsync(job, stoppingToken);\n    }\n}\n",[25,760,761,766,771,775,780,785,789,794,799,804,809,814],{"__ignoreMap":35},[39,762,763],{"class":41,"line":42},[39,764,765],{},"builder.Host.ConfigureHostOptions(opts =>\n",[39,767,768],{"class":41,"line":49},[39,769,770],{},"    opts.ShutdownTimeout = TimeSpan.FromSeconds(30));\n",[39,772,773],{"class":41,"line":67},[39,774,77],{"emptyLinePlaceholder":76},[39,776,777],{"class":41,"line":73},[39,778,779],{},"\u002F\u002F Background services must respect the cancellation token:\n",[39,781,782],{"class":41,"line":80},[39,783,784],{},"protected override async Task ExecuteAsync(CancellationToken stoppingToken)\n",[39,786,787],{"class":41,"line":86},[39,788,217],{},[39,790,791],{"class":41,"line":107},[39,792,793],{},"    while (!stoppingToken.IsCancellationRequested)\n",[39,795,796],{"class":41,"line":113},[39,797,798],{},"    {\n",[39,800,801],{"class":41,"line":118},[39,802,803],{},"        var job = await _queue.DequeueAsync(stoppingToken);\n",[39,805,806],{"class":41,"line":124},[39,807,808],{},"        await ProcessAsync(job, stoppingToken);\n",[39,810,811],{"class":41,"line":145},[39,812,813],{},"    }\n",[39,815,816],{"class":41,"line":151},[39,817,818],{},"}\n",[15,820,821,822,825],{},"A ",[25,823,824],{},"preStop"," lifecycle hook in Kubernetes adds a sleep before SIGTERM, giving the\nload balancer time to drain connections before the process starts shutting down:",[30,827,829],{"className":619,"code":828,"language":621,"meta":35,"style":35},"lifecycle:\n  preStop:\n    exec:\n      command: [\"\u002Fbin\u002Fsh\", \"-c\", \"sleep 5\"]\n",[25,830,831,836,841,846],{"__ignoreMap":35},[39,832,833],{"class":41,"line":42},[39,834,835],{},"lifecycle:\n",[39,837,838],{"class":41,"line":49},[39,839,840],{},"  preStop:\n",[39,842,843],{"class":41,"line":67},[39,844,845],{},"    exec:\n",[39,847,848],{"class":41,"line":73},[39,849,850],{},"      command: [\"\u002Fbin\u002Fsh\", \"-c\", \"sleep 5\"]\n",[10,852,854],{"id":853},"github-actions-cicd","GitHub Actions CI\u002FCD",[30,856,858],{"className":619,"code":857,"language":621,"meta":35,"style":35},"name: Build and Deploy\non:\n  push:\n    branches: [main]\n\njobs:\n  build-and-test:\n    runs-on: ubuntu-latest\n    steps:\n    - uses: actions\u002Fcheckout@v4\n    - uses: actions\u002Fsetup-dotnet@v4\n      with: { dotnet-version: '8.0.x' }\n    - run: dotnet restore\n    - run: dotnet build --no-restore -c Release\n    - run: dotnet test --no-build -c Release --collect:\"XPlat Code Coverage\"\n    - run: dotnet publish src\u002FMyApp -c Release -o .\u002Fpublish\n    - uses: actions\u002Fupload-artifact@v4\n      with: { name: publish, path: .\u002Fpublish }\n\n  deploy:\n    needs: build-and-test\n    runs-on: ubuntu-latest\n    if: github.ref == 'refs\u002Fheads\u002Fmain'\n    environment: production          # requires manual approval if configured\n    steps:\n    - uses: actions\u002Fdownload-artifact@v4\n      with: { name: publish, path: .\u002Fpublish }\n    - uses: azure\u002Fwebapps-deploy@v3\n      with:\n        app-name: myapp-prod\n        publish-profile: ${{ secrets.AZURE_WEBAPP_PUBLISH_PROFILE }}\n        package: .\u002Fpublish\n",[25,859,860,865,870,875,880,884,889,894,899,904,909,914,919,924,929,934,939,944,949,953,958,963,967,972,977,981,986,990,996,1002,1008,1014],{"__ignoreMap":35},[39,861,862],{"class":41,"line":42},[39,863,864],{},"name: Build and Deploy\n",[39,866,867],{"class":41,"line":49},[39,868,869],{},"on:\n",[39,871,872],{"class":41,"line":67},[39,873,874],{},"  push:\n",[39,876,877],{"class":41,"line":73},[39,878,879],{},"    branches: [main]\n",[39,881,882],{"class":41,"line":80},[39,883,77],{"emptyLinePlaceholder":76},[39,885,886],{"class":41,"line":86},[39,887,888],{},"jobs:\n",[39,890,891],{"class":41,"line":107},[39,892,893],{},"  build-and-test:\n",[39,895,896],{"class":41,"line":113},[39,897,898],{},"    runs-on: ubuntu-latest\n",[39,900,901],{"class":41,"line":118},[39,902,903],{},"    steps:\n",[39,905,906],{"class":41,"line":124},[39,907,908],{},"    - uses: actions\u002Fcheckout@v4\n",[39,910,911],{"class":41,"line":145},[39,912,913],{},"    - uses: actions\u002Fsetup-dotnet@v4\n",[39,915,916],{"class":41,"line":151},[39,917,918],{},"      with: { dotnet-version: '8.0.x' }\n",[39,920,921],{"class":41,"line":156},[39,922,923],{},"    - run: dotnet restore\n",[39,925,926],{"class":41,"line":162},[39,927,928],{},"    - run: dotnet build --no-restore -c Release\n",[39,930,931],{"class":41,"line":366},[39,932,933],{},"    - run: dotnet test --no-build -c Release --collect:\"XPlat Code Coverage\"\n",[39,935,936],{"class":41,"line":372},[39,937,938],{},"    - run: dotnet publish src\u002FMyApp -c Release -o .\u002Fpublish\n",[39,940,941],{"class":41,"line":377},[39,942,943],{},"    - uses: actions\u002Fupload-artifact@v4\n",[39,945,946],{"class":41,"line":383},[39,947,948],{},"      with: { name: publish, path: .\u002Fpublish }\n",[39,950,951],{"class":41,"line":389},[39,952,77],{"emptyLinePlaceholder":76},[39,954,955],{"class":41,"line":395},[39,956,957],{},"  deploy:\n",[39,959,960],{"class":41,"line":400},[39,961,962],{},"    needs: build-and-test\n",[39,964,965],{"class":41,"line":406},[39,966,898],{},[39,968,969],{"class":41,"line":411},[39,970,971],{},"    if: github.ref == 'refs\u002Fheads\u002Fmain'\n",[39,973,974],{"class":41,"line":417},[39,975,976],{},"    environment: production          # requires manual approval if configured\n",[39,978,979],{"class":41,"line":423},[39,980,903],{},[39,982,983],{"class":41,"line":428},[39,984,985],{},"    - uses: actions\u002Fdownload-artifact@v4\n",[39,987,988],{"class":41,"line":434},[39,989,948],{},[39,991,993],{"class":41,"line":992},28,[39,994,995],{},"    - uses: azure\u002Fwebapps-deploy@v3\n",[39,997,999],{"class":41,"line":998},29,[39,1000,1001],{},"      with:\n",[39,1003,1005],{"class":41,"line":1004},30,[39,1006,1007],{},"        app-name: myapp-prod\n",[39,1009,1011],{"class":41,"line":1010},31,[39,1012,1013],{},"        publish-profile: ${{ secrets.AZURE_WEBAPP_PUBLISH_PROFILE }}\n",[39,1015,1017],{"class":41,"line":1016},32,[39,1018,1019],{},"        package: .\u002Fpublish\n",[15,1021,1022,1023,1026],{},"Separating build-and-test from deploy ensures tests pass before deployment runs.\nThe ",[25,1024,1025],{},"environment: production"," setting enables manual approval gates in GitHub.",[10,1028,1030],{"id":1029},"azure-app-service-deployment-slots","Azure App Service: deployment slots",[15,1032,1033],{},"Azure App Service deployment slots enable zero-downtime releases:",[30,1035,1037],{"className":32,"code":1036,"language":34,"meta":35,"style":35},"# Create a staging slot:\naz webapp deployment slot create --name myapp-prod --slot staging \\\n    --resource-group myapp-rg\n\n# Deploy to staging (app is not yet live):\naz webapp deployment source config-zip --name myapp-prod --slot staging \\\n    --resource-group myapp-rg --src .\u002Fpublish.zip\n\n# Warm up the staging slot (health probes, cache population):\ncurl https:\u002F\u002Fmyapp-prod-staging.azurewebsites.net\u002Fhealth\u002Fready\n\n# Swap staging → production (atomic, instant):\naz webapp deployment slot swap --name myapp-prod --slot staging \\\n    --resource-group myapp-rg\n",[25,1038,1039,1044,1076,1084,1088,1093,1117,1130,1134,1139,1147,1151,1156,1179],{"__ignoreMap":35},[39,1040,1041],{"class":41,"line":42},[39,1042,1043],{"class":45},"# Create a staging slot:\n",[39,1045,1046,1049,1052,1055,1058,1061,1064,1067,1070,1073],{"class":41,"line":49},[39,1047,1048],{"class":52},"az",[39,1050,1051],{"class":56}," webapp",[39,1053,1054],{"class":56}," deployment",[39,1056,1057],{"class":56}," slot",[39,1059,1060],{"class":56}," create",[39,1062,1063],{"class":60}," --name",[39,1065,1066],{"class":56}," myapp-prod",[39,1068,1069],{"class":60}," --slot",[39,1071,1072],{"class":56}," staging",[39,1074,1075],{"class":60}," \\\n",[39,1077,1078,1081],{"class":41,"line":67},[39,1079,1080],{"class":60},"    --resource-group",[39,1082,1083],{"class":56}," myapp-rg\n",[39,1085,1086],{"class":41,"line":73},[39,1087,77],{"emptyLinePlaceholder":76},[39,1089,1090],{"class":41,"line":80},[39,1091,1092],{"class":45},"# Deploy to staging (app is not yet live):\n",[39,1094,1095,1097,1099,1101,1104,1107,1109,1111,1113,1115],{"class":41,"line":86},[39,1096,1048],{"class":52},[39,1098,1051],{"class":56},[39,1100,1054],{"class":56},[39,1102,1103],{"class":56}," source",[39,1105,1106],{"class":56}," config-zip",[39,1108,1063],{"class":60},[39,1110,1066],{"class":56},[39,1112,1069],{"class":60},[39,1114,1072],{"class":56},[39,1116,1075],{"class":60},[39,1118,1119,1121,1124,1127],{"class":41,"line":107},[39,1120,1080],{"class":60},[39,1122,1123],{"class":56}," myapp-rg",[39,1125,1126],{"class":60}," --src",[39,1128,1129],{"class":56}," .\u002Fpublish.zip\n",[39,1131,1132],{"class":41,"line":113},[39,1133,77],{"emptyLinePlaceholder":76},[39,1135,1136],{"class":41,"line":118},[39,1137,1138],{"class":45},"# Warm up the staging slot (health probes, cache population):\n",[39,1140,1141,1144],{"class":41,"line":124},[39,1142,1143],{"class":52},"curl",[39,1145,1146],{"class":56}," https:\u002F\u002Fmyapp-prod-staging.azurewebsites.net\u002Fhealth\u002Fready\n",[39,1148,1149],{"class":41,"line":145},[39,1150,77],{"emptyLinePlaceholder":76},[39,1152,1153],{"class":41,"line":151},[39,1154,1155],{"class":45},"# Swap staging → production (atomic, instant):\n",[39,1157,1158,1160,1162,1164,1166,1169,1171,1173,1175,1177],{"class":41,"line":156},[39,1159,1048],{"class":52},[39,1161,1051],{"class":56},[39,1163,1054],{"class":56},[39,1165,1057],{"class":56},[39,1167,1168],{"class":56}," swap",[39,1170,1063],{"class":60},[39,1172,1066],{"class":56},[39,1174,1069],{"class":60},[39,1176,1072],{"class":56},[39,1178,1075],{"class":60},[39,1180,1181,1183],{"class":41,"line":162},[39,1182,1080],{"class":60},[39,1184,1083],{"class":56},[15,1186,1187],{},"If the swap introduces a regression, swap back in seconds.",[10,1189,1191],{"id":1190},"production-deployment-checklist","Production deployment checklist",[15,1193,1194],{},"The most common reasons a first production deployment fails:",[30,1196,1198],{"className":198,"code":1197,"language":200,"meta":35,"style":35},"\u002F\u002F 1. Environment name is Production (not Development):\n\u002F\u002F ASPNETCORE_ENVIRONMENT=Production\n\u002F\u002F — disables developer exception pages, enables HSTS\n\n\u002F\u002F 2. Secrets are NOT in committed files:\n\u002F\u002F Use env vars, Azure Key Vault, or Kubernetes Secrets\n\n\u002F\u002F 3. HTTPS enforced:\napp.UseHttpsRedirection();\napp.UseHsts();\n\n\u002F\u002F 4. Security headers:\nctx.Response.Headers.Append(\"X-Content-Type-Options\", \"nosniff\");\nctx.Response.Headers.Append(\"X-Frame-Options\", \"DENY\");\n\n\u002F\u002F 5. Data protection keys are shared between instances:\nbuilder.Services.AddDataProtection()\n    .PersistKeysToAzureBlobStorage(blobClient)\n    .ProtectKeysWithAzureKeyVault(keyIdentifier, credential);\n\u002F\u002F Without this, cookies set by one instance cannot be read by another\n\n\u002F\u002F 6. Database migrations ran BEFORE startup:\n\u002F\u002F dotnet ef database update (in CI, before deploy step)\n\n\u002F\u002F 7. Required configuration validated at startup:\nbuilder.Services.AddOptions\u003CDatabaseOptions>()\n    .BindConfiguration(\"Database\")\n    .ValidateDataAnnotations()\n    .ValidateOnStart(); \u002F\u002F startup crash > runtime NullReferenceException\n",[25,1199,1200,1205,1210,1215,1219,1224,1229,1233,1238,1243,1248,1252,1257,1262,1267,1271,1276,1281,1286,1291,1296,1300,1305,1310,1314,1319,1324,1329,1334],{"__ignoreMap":35},[39,1201,1202],{"class":41,"line":42},[39,1203,1204],{},"\u002F\u002F 1. Environment name is Production (not Development):\n",[39,1206,1207],{"class":41,"line":49},[39,1208,1209],{},"\u002F\u002F ASPNETCORE_ENVIRONMENT=Production\n",[39,1211,1212],{"class":41,"line":67},[39,1213,1214],{},"\u002F\u002F — disables developer exception pages, enables HSTS\n",[39,1216,1217],{"class":41,"line":73},[39,1218,77],{"emptyLinePlaceholder":76},[39,1220,1221],{"class":41,"line":80},[39,1222,1223],{},"\u002F\u002F 2. Secrets are NOT in committed files:\n",[39,1225,1226],{"class":41,"line":86},[39,1227,1228],{},"\u002F\u002F Use env vars, Azure Key Vault, or Kubernetes Secrets\n",[39,1230,1231],{"class":41,"line":107},[39,1232,77],{"emptyLinePlaceholder":76},[39,1234,1235],{"class":41,"line":113},[39,1236,1237],{},"\u002F\u002F 3. HTTPS enforced:\n",[39,1239,1240],{"class":41,"line":118},[39,1241,1242],{},"app.UseHttpsRedirection();\n",[39,1244,1245],{"class":41,"line":124},[39,1246,1247],{},"app.UseHsts();\n",[39,1249,1250],{"class":41,"line":145},[39,1251,77],{"emptyLinePlaceholder":76},[39,1253,1254],{"class":41,"line":151},[39,1255,1256],{},"\u002F\u002F 4. Security headers:\n",[39,1258,1259],{"class":41,"line":156},[39,1260,1261],{},"ctx.Response.Headers.Append(\"X-Content-Type-Options\", \"nosniff\");\n",[39,1263,1264],{"class":41,"line":162},[39,1265,1266],{},"ctx.Response.Headers.Append(\"X-Frame-Options\", \"DENY\");\n",[39,1268,1269],{"class":41,"line":366},[39,1270,77],{"emptyLinePlaceholder":76},[39,1272,1273],{"class":41,"line":372},[39,1274,1275],{},"\u002F\u002F 5. Data protection keys are shared between instances:\n",[39,1277,1278],{"class":41,"line":377},[39,1279,1280],{},"builder.Services.AddDataProtection()\n",[39,1282,1283],{"class":41,"line":383},[39,1284,1285],{},"    .PersistKeysToAzureBlobStorage(blobClient)\n",[39,1287,1288],{"class":41,"line":389},[39,1289,1290],{},"    .ProtectKeysWithAzureKeyVault(keyIdentifier, credential);\n",[39,1292,1293],{"class":41,"line":395},[39,1294,1295],{},"\u002F\u002F Without this, cookies set by one instance cannot be read by another\n",[39,1297,1298],{"class":41,"line":400},[39,1299,77],{"emptyLinePlaceholder":76},[39,1301,1302],{"class":41,"line":406},[39,1303,1304],{},"\u002F\u002F 6. Database migrations ran BEFORE startup:\n",[39,1306,1307],{"class":41,"line":411},[39,1308,1309],{},"\u002F\u002F dotnet ef database update (in CI, before deploy step)\n",[39,1311,1312],{"class":41,"line":417},[39,1313,77],{"emptyLinePlaceholder":76},[39,1315,1316],{"class":41,"line":423},[39,1317,1318],{},"\u002F\u002F 7. Required configuration validated at startup:\n",[39,1320,1321],{"class":41,"line":428},[39,1322,1323],{},"builder.Services.AddOptions\u003CDatabaseOptions>()\n",[39,1325,1326],{"class":41,"line":434},[39,1327,1328],{},"    .BindConfiguration(\"Database\")\n",[39,1330,1331],{"class":41,"line":992},[39,1332,1333],{},"    .ValidateDataAnnotations()\n",[39,1335,1336],{"class":41,"line":998},[39,1337,1338],{},"    .ValidateOnStart(); \u002F\u002F startup crash > runtime NullReferenceException\n",[15,1340,1341],{},"Data protection key sharing is the single most common first-production surprise\nfor .NET developers — without it, cookie authentication silently breaks on\nmulti-instance deployments.",[15,1343,1344,1347],{},[192,1345,1346],{},"Rule of thumb:"," Check the environment name, secret injection, HTTPS enforcement,\ndata protection key sharing, and migration status before every first production\ndeployment. A startup crash with a clear configuration error message is always\npreferable to a runtime failure discovered under load.",[1349,1350,1351],"style",{},"html .default .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}html .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}html .dark .shiki span {color: var(--shiki-dark);background: var(--shiki-dark-bg);font-style: var(--shiki-dark-font-style);font-weight: var(--shiki-dark-font-weight);text-decoration: var(--shiki-dark-text-decoration);}html.dark .shiki span {color: var(--shiki-dark);background: var(--shiki-dark-bg);font-style: var(--shiki-dark-font-style);font-weight: var(--shiki-dark-font-weight);text-decoration: var(--shiki-dark-text-decoration);}html pre.shiki code .sJ8bj, html code.shiki .sJ8bj{--shiki-default:#6A737D;--shiki-dark:#6A737D}html pre.shiki code .sScJk, html code.shiki .sScJk{--shiki-default:#6F42C1;--shiki-dark:#B392F0}html pre.shiki code .sZZnC, html code.shiki .sZZnC{--shiki-default:#032F62;--shiki-dark:#9ECBFF}html pre.shiki code .sj4cs, html code.shiki .sj4cs{--shiki-default:#005CC5;--shiki-dark:#79B8FF}html pre.shiki code .sVt8B, html code.shiki .sVt8B{--shiki-default:#24292E;--shiki-dark:#E1E4E8}html pre.shiki code .szBVR, html code.shiki .szBVR{--shiki-default:#D73A49;--shiki-dark:#F97583}",{"title":35,"searchDepth":49,"depth":49,"links":1353},[1354,1355,1356,1357,1358,1359,1360,1361,1362,1363],{"id":12,"depth":49,"text":13},{"id":20,"depth":49,"text":21},{"id":187,"depth":49,"text":188},{"id":285,"depth":49,"text":286},{"id":450,"depth":49,"text":451},{"id":612,"depth":49,"text":613},{"id":751,"depth":49,"text":752},{"id":853,"depth":49,"text":854},{"id":1029,"depth":49,"text":1030},{"id":1190,"depth":49,"text":1191},"How to get an ASP.NET Core app into production — publish modes, multi-stage Docker builds, Kubernetes health probes, graceful shutdown, and the configuration mistakes that bite most teams on first deployment.","hard","md",".NET Core",{},"\u002Fblog\u002Fdotnet-deployment","\u002Fdotnet\u002Fperformance-deployment\u002Fdeployment",{"title":5,"description":1364},"blog\u002Fdotnet-deployment","Deployment","Performance & Deployment","performance-deployment","2026-06-23","d2VfPj8g-6emKevtMr3S_dE1Cq5pyKdS1UR3GM7XN7Q",1782244086288]