CI/CD for .NET on Azure: From Zero to Production
Feb 15, 2026
Setting up CI/CD for a .NET application on Azure means automating the entire path from code commit to production deployment — build, test, and release — using either Azure DevOps Pipelines or GitHub Actions. After five years of building and maintaining these pipelines at HUSS B.V., I can tell you the gap between a tutorial pipeline and a production-ready one is significant. This guide covers what that gap looks like and how to close it.
Why CI/CD Matters for .NET Projects
Manual deployments are a liability. Every time someone right-clicks “Publish” in Visual Studio, you are rolling the dice on consistency. CI/CD eliminates that risk by making every deployment identical, traceable, and reversible. It also forces you to write testable code — because if your pipeline runs tests on every commit, you actually have to write them.
For .NET on Azure specifically, the tooling is mature. Microsoft has invested heavily in making this path smooth, whether you pick Azure DevOps or GitHub Actions. The question is not if you should automate, it is how to do it properly.
Azure DevOps vs GitHub Actions for .NET
Both platforms can get the job done. The choice usually comes down to your organization’s existing tooling and preferences rather than a fundamental technical gap. Here is how they compare in practice:
| Feature | Azure DevOps Pipelines | GitHub Actions |
|---|---|---|
| YAML pipeline support | Yes | Yes |
| Built-in .NET SDK tasks | Rich task library (DotNetCoreCLI@2) | Community actions + dotnet CLI directly |
| Azure integration | Native service connections | Azure Login action + service principals |
| Deployment slots | First-class swap task | Supported via azure/webapps-deploy |
| Artifact management | Azure Artifacts built-in | GitHub Packages or external feeds |
| Self-hosted agents | Supported | Supported (self-hosted runners) |
| Environments & approvals | Environments with gates and checks | Environments with protection rules |
| Pricing for private repos | 1 free parallel job (1800 min/month) | 2000 min/month free |
| Learning curve | Steeper — more concepts to learn | Lower — simpler YAML structure |
My recommendation: if your organization already uses Azure DevOps for boards and repos, stick with Azure DevOps Pipelines. If your code lives on GitHub, use GitHub Actions. Migrating between the two is straightforward — the pipeline logic is nearly identical, just different YAML syntax.
How to Set Up CI/CD for .NET on Azure
A production-ready pipeline follows the same core flow regardless of platform: restore, build, test, publish, deploy. Below are working examples for both Azure DevOps and GitHub Actions that deploy a .NET 8 web application to Azure App Service.
Azure DevOps Pipeline
trigger:
branches:
include:
- main
pool:
vmImage: 'ubuntu-latest'
variables:
buildConfiguration: 'Release'
dotnetVersion: '8.x'
azureSubscription: 'MyAzureServiceConnection'
appName: 'my-app-production'
stages:
- stage: Build
displayName: 'Build & Test'
jobs:
- job: BuildJob
steps:
- task: UseDotNet@2
inputs:
packageType: 'sdk'
version: '$(dotnetVersion)'
- task: DotNetCoreCLI@2
displayName: 'Restore'
inputs:
command: 'restore'
projects: '**/*.csproj'
- task: DotNetCoreCLI@2
displayName: 'Build'
inputs:
command: 'build'
arguments: '--configuration $(buildConfiguration) --no-restore'
- task: DotNetCoreCLI@2
displayName: 'Test'
inputs:
command: 'test'
arguments: '--configuration $(buildConfiguration) --no-build --collect:"XPlat Code Coverage"'
- task: DotNetCoreCLI@2
displayName: 'Publish'
inputs:
command: 'publish'
publishWebProjects: true
arguments: '--configuration $(buildConfiguration) --output $(Build.ArtifactStagingDirectory)'
- publish: $(Build.ArtifactStagingDirectory)
artifact: 'webapp'
- stage: DeployStaging
displayName: 'Deploy to Staging'
dependsOn: Build
jobs:
- deployment: DeployStaging
environment: 'staging'
strategy:
runOnce:
deploy:
steps:
- task: AzureWebApp@1
inputs:
azureSubscription: '$(azureSubscription)'
appType: 'webAppLinux'
appName: '$(appName)'
deployToSlotOrASE: true
slotName: 'staging'
package: '$(Pipeline.Workspace)/webapp/**/*.zip'
- stage: DeployProduction
displayName: 'Deploy to Production'
dependsOn: DeployStaging
jobs:
- deployment: DeployProduction
environment: 'production'
strategy:
runOnce:
deploy:
steps:
- task: AzureAppServiceManage@0
inputs:
azureSubscription: '$(azureSubscription)'
action: 'Swap Slots'
webAppName: '$(appName)'
sourceSlot: 'staging'
targetSlot: 'production' GitHub Actions Workflow
name: Build and Deploy
on:
push:
branches: [main]
env:
DOTNET_VERSION: '8.x'
AZURE_WEBAPP_NAME: 'my-app-production'
jobs:
build-and-test:
runs-on: ubuntu-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
- name: Build
run: dotnet build --configuration Release --no-restore
- name: Test
run: dotnet test --configuration Release --no-build --verbosity normal
- name: Publish
run: dotnet publish --configuration Release --output ./publish
- name: Upload artifact
uses: actions/upload-artifact@v4
with:
name: webapp
path: ./publish
deploy-staging:
needs: build-and-test
runs-on: ubuntu-latest
environment: staging
steps:
- name: Download artifact
uses: actions/download-artifact@v4
with:
name: webapp
- name: Login to Azure
uses: azure/login@v2
with:
creds: ${{ secrets.AZURE_CREDENTIALS }}
- name: Deploy to staging slot
uses: azure/webapps-deploy@v3
with:
app-name: ${{ env.AZURE_WEBAPP_NAME }}
slot-name: staging
deploy-production:
needs: deploy-staging
runs-on: ubuntu-latest
environment: production
steps:
- name: Login to Azure
uses: azure/login@v2
with:
creds: ${{ secrets.AZURE_CREDENTIALS }}
- name: Swap staging to production
run: |
az webapp deployment slot swap
--name ${{ env.AZURE_WEBAPP_NAME }}
--resource-group my-resource-group
--slot staging
--target-slot production Deployment Slots and Zero-Downtime Releases
Deployment slots are the single most important feature for production .NET deployments on Azure App Service. The strategy is simple: deploy to a staging slot, verify it works, then swap staging into production. The swap is near-instant because Azure just flips the virtual IP routing — your app is already warmed up in the staging slot.
A few things I have learned the hard way:
- Always configure slot-sticky settings. Connection strings and app settings that differ between staging and production — like database connections — need to be marked as “slot sticky” so they do not follow the swap.
- Use health checks. Configure the App Service health check endpoint so Azure can verify your app is healthy before completing the swap.
- Test the swap path, not just the deployment. I have seen pipelines where the deploy-to-slot step worked perfectly, but the swap failed because of configuration mismatches. Automate the full flow.
Infrastructure as Code: Provision Before You Deploy
Your pipeline should not assume the infrastructure already exists. Use Bicep or ARM templates to provision your Azure resources as part of the pipeline — or at least in a separate infrastructure pipeline that runs first.
At HUSS, I used Bicep templates to define the App Service Plan, App Service, deployment slots, Application Insights, and Key Vault references. This meant spinning up a new environment was a single pipeline run, not a day of clicking through the Azure portal.
A minimal Bicep snippet for an App Service with a staging slot:
resource appService 'Microsoft.Web/sites@2023-01-01' = {
name: appName
location: location
properties: {
serverFarmId: appServicePlan.id
siteConfig: {
netFrameworkVersion: 'v8.0'
}
}
}
resource stagingSlot 'Microsoft.Web/sites/slots@2023-01-01' = {
parent: appService
name: 'staging'
location: location
properties: {
serverFarmId: appServicePlan.id
}
} Common Mistakes to Avoid
Not pinning your .NET SDK version. If you rely on whatever version is pre-installed on the build agent, your builds will break randomly when the agent image updates. Always specify the exact SDK version.
Skipping tests in the pipeline. I get it — tests slow things down. But a pipeline without tests is just automated risk. At minimum, run your unit tests. Add integration tests against a test database if you can.
Using the same service principal for everything. Create separate service principals for staging and production with appropriately scoped permissions. Least privilege is not optional.
Not caching NuGet packages. Both platforms support dependency caching. Use it. It cuts build times significantly on larger solutions.
Deploying directly to production. Even if you are a solo developer, use a staging slot. The five minutes it adds to your pipeline saves hours of debugging production incidents.
Frequently Asked Questions
Do I need Azure DevOps if my code is on GitHub?
No. GitHub Actions has excellent Azure integration through official actions like azure/login and azure/webapps-deploy. You only need Azure DevOps if you want its other features like Boards, Test Plans, or Azure Artifacts. For pure CI/CD, GitHub Actions is perfectly capable.
Can I deploy .NET Framework (not .NET Core) with these pipelines?
Yes, but with caveats. .NET Framework requires Windows build agents (windows-latest instead of ubuntu-latest), and you will use MSBuild tasks instead of dotnet CLI commands. The overall pipeline structure stays the same — build, test, publish, deploy — but the specific commands differ.
How do I handle database migrations in the pipeline?
Run EF Core migrations as a step between deployment and slot swap. Deploy your code to the staging slot, run dotnet ef database update targeting the staging database, verify everything works, then swap. Never run migrations directly against production during the swap — do them before.
What is the cost of deployment slots on Azure App Service?
Deployment slots are available on Standard tier and above at no extra cost for the slot itself. However, the staging slot consumes resources from your App Service Plan when it is running. You can stop the staging slot after the swap to reclaim those resources if cost is a concern.
Should I use YAML pipelines or the classic editor in Azure DevOps?
Always use YAML pipelines. The classic editor is being phased out, and YAML pipelines live in your repository alongside your code. This means your pipeline definition is version-controlled, reviewable in pull requests, and portable. There is no good reason to use the classic editor for new projects.