Skip to content

Now Supporting .NET 10 LTS! | Specialist Blazor & ASP.NET Hosting on AWS

Back to Blog
📘Guides

ASP.NET Core CI/CD: GitHub Actions + IIS Guide

Brandon FerroBrandon Ferro
April 7, 2026
8 min read
ASP.NET Core CI/CD: GitHub Actions + IIS Guide

If you're still publishing your ASP.NET Core app by right-clicking → Publish → Folder in Visual Studio, you're doing it wrong. Modern .NET deployment is reproducible, scripted, and ideally takes you from git push to live production in under 5 minutes with zero downtime.

Here's the GitHub Actions + IIS pipeline we ship for Adaptive's ASP.NET Core hosting customers, plus the patterns that prevent the worst CI/CD pain points.

< 5 min
Git Push to Production
0 sec
Downtime (Blue-Green)
Auto
Rollback on Health-Check Fail
10 Sites
On Professional Plan

Continuous integration pipeline visualization

The Goal

A working .NET CI/CD pipeline should:

  1. Build on every push — catch compile errors before merge
  2. Run tests — unit, integration, smoke
  3. Produce a deployable artifact — once, used everywhere
  4. Deploy to staging on merge to main — automatic verification environment
  5. Promote to production with a click — or automatic, with feature flags
  6. Roll back automatically — if health checks fail post-deploy

We'll wire all of these together for IIS 10 on Windows Server 2022.

Project Layout

The pipeline assumes a typical ASP.NET Core repo:

/src
  /MyApp.Web              — ASP.NET Core project
  /MyApp.Domain           — business logic
  /MyApp.Infrastructure   — EF Core, repositories
/tests
  /MyApp.Tests            — unit + integration
/.github/workflows
  ci-cd.yml               — our pipeline
MyApp.sln
global.json               — pinned .NET SDK

Pin Your .NET SDK

global.json at the repo root locks the SDK version so CI matches local dev:

{ "sdk": { "version": "10.0.100", "rollForward": "latestFeature" } }

💡 Why pin: This eliminates "works on my machine" disasters where a teammate's SDK 9.0.200 emits subtly different IL than the CI runner's 10.0.100.

GitHub Actions Workflow

The complete .github/workflows/ci-cd.yml:

name: ASP.NET Core CI/CD on: push: branches: [main, develop] pull_request: branches: [main] env: DOTNET_VERSION: '10.0.x' SOLUTION_FILE: 'MyApp.sln' PROJECT_PATH: 'src/MyApp.Web/MyApp.Web.csproj' jobs: build-test: runs-on: windows-latest steps: - uses: actions/checkout@v4 - name: Setup .NET uses: actions/setup-dotnet@v4 with: dotnet-version: ${{ env.DOTNET_VERSION }} - name: Restore run: dotnet restore ${{ env.SOLUTION_FILE }} - name: Build run: dotnet build ${{ env.SOLUTION_FILE }} --configuration Release --no-restore - name: Test run: dotnet test ${{ env.SOLUTION_FILE }} --no-build --verbosity normal --logger trx --collect:"XPlat Code Coverage" - name: Publish if: github.ref == 'refs/heads/main' run: dotnet publish ${{ env.PROJECT_PATH }} --configuration Release --output ./publish --no-build - name: Upload artifact if: github.ref == 'refs/heads/main' uses: actions/upload-artifact@v4 with: name: app-package path: ./publish deploy-staging: needs: build-test if: github.ref == 'refs/heads/main' runs-on: ubuntu-latest environment: staging steps: - uses: actions/download-artifact@v4 with: name: app-package path: ./publish - name: Deploy via WebDeploy run: | curl -X POST -u "${{ secrets.DEPLOY_USER }}:${{ secrets.DEPLOY_PASS }}" \ -H "Content-Type: application/zip" \ --data-binary "@./publish.zip" \ "${{ secrets.STAGING_DEPLOY_URL }}" - name: Smoke test run: | for i in {1..30}; do if curl -fsS https://staging.example.com/health; then exit 0; fi sleep 10 done exit 1 deploy-production: needs: deploy-staging if: github.ref == 'refs/heads/main' runs-on: ubuntu-latest environment: production steps: - uses: actions/download-artifact@v4 with: name: app-package path: ./publish - name: Deploy run: | curl -X POST -u "${{ secrets.PROD_USER }}:${{ secrets.PROD_PASS }}" \ -H "Content-Type: application/zip" \ --data-binary "@./publish.zip" \ "${{ secrets.PROD_DEPLOY_URL }}" - name: Health check + auto-rollback run: | for i in {1..30}; do if curl -fsS https://www.example.com/health; then exit 0; fi sleep 10 done # rollback by re-deploying previous artifact exit 1

The environment: production block requires manual approval in GitHub before the production deploy runs.

Deploying to IIS Without Downtime

The brute-force approach (stop site → copy files → start site) creates 5–30 seconds of downtime. Two better patterns:

PatternHow It WorksDowntimeWhen to Use
App Offline fileDrop app_offline.htm → IIS gracefully shuts down → deploy → remove file~10 sec maintenance pageSmall sites, infrequent deploys
Blue-Green via two IIS sitesDeploy to standby site, run smoke tests, flip DNS or upstream0 secProduction, frequent deploys

Our Professional plan includes 10 websites, so you can easily run staging and production side-by-side for blue-green deployments. Business plan customers get 5 websites; Developer plan supports 2.

Publishing Profile

src/MyApp.Web/Properties/PublishProfiles/Production.pubxml:

<Project> <PropertyGroup> <PublishProtocol>WebDeploy</PublishProtocol> <Configuration>Release</Configuration> <TargetFramework>net10.0</TargetFramework> <PublishUrl>https://hosting.example.com:8172/msdeploy.axd</PublishUrl> <MSDeployServiceURL>hosting.example.com:8172</MSDeployServiceURL> <DeployIisAppPath>example.com</DeployIisAppPath> <SkipExtraFilesOnServer>true</SkipExtraFilesOnServer> <SelfContained>false</SelfContained> <PublishTrimmed>false</PublishTrimmed> </PropertyGroup> </Project>

⚠️ SkipExtraFilesOnServer=true prevents accidentally deleting user uploads that aren't in source control. Without it, every deploy wipes your wwwroot/uploads/ folder.

Health Check Endpoint

Your app needs a /health endpoint that exercises critical dependencies:

builder.Services.AddHealthChecks() .AddSqlServer(builder.Configuration.GetConnectionString("Default")!) .AddCheck<DiskSpaceHealthCheck>("disk-space") .AddRedis(builder.Configuration.GetConnectionString("Redis")!, failureStatus: HealthStatus.Degraded); app.MapHealthChecks("/health", new HealthCheckOptions { ResponseWriter = UIResponseWriter.WriteHealthCheckUIResponse, });

/health returns 200 OK only when all checks pass. CI uses this to verify successful deployment.

Server monitoring dashboard

Automatic Rollback

If health checks fail after deploy, you want to roll back automatically. Three approaches:

ApproachComplexityRollback Time
Re-deploy previous artifactLow30–60 sec
Swap IIS site bindings (blue-green)Medium<5 sec
DNS flip back to old versionMedium30–60 sec (DNS TTL)

For most teams, re-deploying the previous artifact is sufficient. Keep the last 3 artifacts in CI cache.

Database Migrations: The Risky Part

EF Core migrations done wrong cause production outages. Three rules:

⚠️ Rule 1: Migrations are always backward-compatible. Old code must run against new schema. This means add columns NULL-able first, deploy code that writes both, then make NOT NULL in a later migration.

⚠️ Rule 2: Migrations run separately from app deploy. Use a CLI step in CI, not MigrateAsync() on app startup. Otherwise you have multiple instances racing the migration.

⚠️ Rule 3: Test rollback paths. Every migration should have a tested down-migration. EF Core supports this but most teams skip it.

- name: Apply migrations run: dotnet ef database update --project src/MyApp.Web --connection "${{ secrets.PROD_CONNECTION_STRING }}"

Secrets Management

Never commit connection strings or API keys. Use:

LayerTool
DevelopmentUser Secrets (dotnet user-secrets)
CIGitHub Secrets / Azure DevOps Library
Runtimeappsettings.Production.json outside web root, OR environment variables, OR Azure Key Vault, OR AWS Secrets Manager

For larger teams: HashiCorp Vault or AWS Secrets Manager with appsettings.json provider:

builder.Configuration.AddAWSSecretsManager(builder.Configuration["AWS:SecretsArn"]);

Monitoring the Pipeline Itself

Your CI/CD pipeline can fail silently if you're not watching:

  • GitHub Actions sends Slack/email on workflow failure
  • Sentry or Application Insights captures runtime exceptions post-deploy
  • Real User Monitoring detects user-facing regressions even when health checks pass

Why This Works on Adaptive

Our ASP.NET Core hosting is configured for CI/CD out of the box:

FeatureWhat You Get
WebDeploy via PleskSecure deployment endpoint, per-site credentials
Dedicated IIS app poolYour deploys never affect other tenants (1/2/4 GB RAM by plan)
Multiple websites per plan2 (Developer), 5 (Business), 10 (Professional) — useful for staging/prod separation
SQL Server databases (2025 or 2022)2 / 5 / 10 by plan, with Query Store enabled
MariaDB databases2 / 5 / 10 by plan, alongside SQL Server
Plesk Control PanelGit deploy, scheduled tasks, log viewer
Higher Priority Resource SchedulingSmooth deploys under load (Business and Professional)

Frequently Asked Questions

Can I use Azure DevOps instead of GitHub Actions?

Yes — Azure Pipelines integrates with WebDeploy natively. The pattern is identical: build artifact → deploy → smoke test → rollback path.

What about Docker-based deployments?

Windows containers on IIS work, but they add complexity for marginal benefit on small-to-medium .NET apps. Most teams are better off with WebDeploy until they need Kubernetes-level orchestration.

How do I deploy database migrations safely?

Make them backward-compatible (NULL-able adds, no destructive drops in the same deploy), run them as a separate CI step before the app deploy, and always have a tested rollback path.

Can the pipeline catch performance regressions?

Yes — add a load-test step (k6, NBomber) running against staging post-deploy. Set thresholds for p95 latency and error rate; fail the pipeline if exceeded.

Does Adaptive support self-hosted GitHub Actions runners?

Yes — you can run runners on a Plesk-managed Windows VM if you need access to private resources. We help configure this for Professional-plan customers.

Ship Faster With Hosting Built for CI/CD

WebDeploy via Plesk. Dedicated IIS app pools so your deploys never break neighbors. SQL Server 2025 and 2022 with Query Store. Plans from $9.49/mo.

Talk to our team about setting up CI/CD for your application — or compare all hosting plans side by side.

DevOps
CI/CD
GitHub Actions
ASP.NET Core