Deploy .NET Apps with Azure Pipelines

Khimananda Oli 8 min read DevOps
Deploy .NET Apps with Azure Pipelines

By Khimananda Oli | Last reviewed: August 2026

Shipping .NET applications reliably requires more than a simple build script; you need a repeatable, auditable process that handles dependencies, secrets, and environment-specific configurations without manual intervention. When you deploy .NET apps with Azure Pipelines, you gain native integration with the Microsoft ecosystem, but success depends on structuring your YAML correctly and avoiding common configuration pitfalls. This guide walks through the exact pipeline architecture, security patterns, and deployment steps I use in production environments to ensure consistent, safe releases.

How do you structure a YAML pipeline to deploy .NET apps with Azure Pipelines?

A well-structured pipeline separates concerns into distinct stages: Build, Test, and Deploy. This separation ensures that a failed test never triggers a production deployment and allows you to reuse build artifacts across environments. Before writing YAML, review the Azure DevOps YAML pipelines practical guide to understand schema fundamentals and validation techniques.

Source RepoGit TriggerBuild Stagedotnet publishTest StageUnit + IntegrationDeploy StageApp ServiceCI/CD Flow for .NET DeploymentArtifact: $(Build.ArtifactStagingDirectory)
Multi-stage pipeline flow for deploying .NET apps with Azure Pipelines, separating build, test, and deployment concerns.

Define triggers and pool configuration

Your pipeline should trigger only on relevant branches to avoid wasting build minutes. Use path filters if your repository contains multiple projects. Always specify a VM image explicitly rather than relying on defaults, as Microsoft updates images regularly and implicit versions can break builds unexpectedly.

trigger:
  branches:
    include:
      - main
      - release/*
  paths:
    include:
      - src/
    exclude:
      - docs/

pool:
  vmImage: 'ubuntu-24.04'

variables:
  buildConfiguration: 'Release'
  dotnetVersion: '8.x'

Restore, build, and publish in sequence

The DotNetCoreCLI@2 task handles all .NET operations. A common mistake is combining restore and build into a single step; keep them separate so you can cache restored packages independently. Publishing creates a self-contained output folder that includes all dependencies, which is critical for consistent deployments.

- task: DotNetCoreCLI@2
  displayName: 'Restore NuGet packages'
  inputs:
    command: 'restore'
    projects: '/*.csproj'
    feedsToUse: 'select'
    vstsFeed: 'my-feed'

- task: DotNetCoreCLI@2
  displayName: 'Build solution'
  inputs:
    command: 'build'
    projects: '/*.csproj'
    arguments: '--configuration $(buildConfiguration) --no-restore'

- task: DotNetCoreCLI@2
  displayName: 'Publish application'
  inputs:
    command: 'publish'
    publishWebProjects: true
    arguments: '--configuration $(buildConfiguration) --output $(Build.ArtifactStagingDirectory) --no-build'
    zipAfterPublish: true

How do you manage secrets securely when deploying .NET apps?

Never hardcode connection strings, API keys, or credentials in YAML files or application settings. Azure Pipelines integrates directly with Azure Key Vault through service connections, allowing you to fetch secrets at runtime without exposing them in logs or variable groups. For deeper guidance on secret handling patterns, see using Azure Key Vault secrets in pipelines.

Configure Key Vault integration

Add the AzureKeyVault@2 task early in your pipeline, typically right after checkout. This task downloads specified secrets and exposes them as pipeline variables. The service connection used must have GET permissions on the Key Vault's access policy or RBAC role.

- task: AzureKeyVault@2
  displayName: 'Fetch secrets from Key Vault'
  inputs:
    azureSubscription: 'Production-Service-Connection'
    KeyVaultName: 'prod-app-kv'
    SecretsFilter: 'DbConnectionString,ApiKey,RedisPassword'
    RunAsPreJob: false

Override app settings during deployment

When deploying to App Service, use the appSettings parameter in the deployment task to inject Key Vault-sourced values. This overrides whatever exists in the portal or web.config, ensuring the deployed instance uses exactly what the pipeline provides. Values are masked automatically in logs.

- task: AzureRmWebAppDeployment@4
  displayName: 'Deploy to Staging Slot'
  inputs:
    ConnectionType: 'AzureRM'
    azureSubscription: 'Production-Service-Connection'
    appType: 'webAppLinux'
    WebAppName: 'my-dotnet-app'
    deployToSlotOrASE: true
    SlotName: 'staging'
    packageForLinux: '$(Build.ArtifactStagingDirectory)//*.zip'
    appSettings: '-ConnectionStrings__Default "$(DbConnectionString)" -ApiKey "$(ApiKey)"'

What testing strategy prevents broken deployments in Azure Pipelines?

Testing in CI must be fast, deterministic, and gated. If tests fail, the pipeline stops immediately and no artifact is published. I recommend running unit tests first (under 5 minutes), then integration tests against ephemeral containers or managed test databases. Refer to integration testing in CI pipelines for patterns that work reliably with .NET and Azure SQL.

Unit TestsxUnit / NUnit< 5 min runtime✓ Fast FeedbackIntegration TestsTestcontainers / DBReal dependencies⚠ Validate ContractsQuality GateCoverage ≥ 80%No critical vulns✗ Fail = Stop PipelineTesting Gates Before DeploymentArtifacts published ONLY after all gates pass
Sequential testing gates ensure only validated builds proceed when you deploy .NET apps with Azure Pipelines.

Run tests and publish results

Always use --no-build when running tests if you've already built the solution; this avoids recompilation and speeds up feedback. Publish test results in TRX format so Azure DevOps renders them in the UI with pass/fail breakdowns and stack traces.

- task: DotNetCoreCLI@2
  displayName: 'Run unit tests'
  inputs:
    command: 'test'
    projects: '/*Tests.csproj'
    arguments: '--configuration $(buildConfiguration) --no-build --collect:"XPlat Code Coverage" --logger trx'
    publishTestResults: true

- task: PublishTestResults@2
  displayName: 'Publish test results'
  condition: always()
  inputs:
    testResultsFormat: 'VSTest'
    testResultsFiles: '/*.trx'
    mergeTestResults: true
    failTaskOnFailedTests: true

Enforce code coverage thresholds

Add a quality gate that fails the pipeline if coverage drops below your team's baseline. This prevents gradual erosion of test quality over time. Combine this with dependency scanning to catch vulnerable packages before they reach production.

How does Azure App Service deployment compare to AKS for .NET workloads?

Choosing between App Service and Azure Kubernetes Service (AKS) depends on operational complexity tolerance, scaling requirements, and team expertise. App Service is fully managed with minimal overhead; AKS offers fine-grained control at the cost of significant operational burden. For teams new to containers or managing fewer than five services, App Service is usually the better starting point. If you're evaluating container orchestration, read Azure AKS practical guide for realistic operational expectations.

CriteriaAzure App ServiceAzure Kubernetes Service
Setup ComplexityLow — create resource, deploy ZIPHigh — cluster provisioning, networking, ingress
Scaling ModelAutomatic scale-out/in rulesHPA/VPA + Cluster Autoscaler configuration
Secret ManagementApp Settings + Key Vault referencesKubernetes Secrets + CSI driver or external-secrets
Deployment SpeedSeconds to low minutes (ZIP deploy)Minutes (image pull + rollout)
Multi-service NetworkingVNet integration available but limitedNative service mesh, network policies
Operational OverheadMinimal — platform-managedSignificant — upgrades, node pools, monitoring
Best ForWeb apps, APIs, small-to-mid scaleMicroservices, high-scale, custom runtimes

Use deployment slots for zero-downtime releases

App Service deployment slots allow you to deploy to a staging slot, validate it with smoke tests, then swap into production atomically. This eliminates downtime during deployments and provides an instant rollback mechanism by swapping back. Always enable "Swap with preview" for critical applications to verify warm-up behavior.

- task: AzureRmWebAppDeployment@4
  displayName: 'Swap staging to production'
  inputs:
    ConnectionType: 'AzureRM'
    azureSubscription: 'Production-Service-Connection'
    appType: 'webAppLinux'
    WebAppName: 'my-dotnet-app'
    SourceSlot: 'staging'
    SwapWithProduction: true
    PreserveVnet: true

How do you optimize pipeline performance and caching for .NET builds?

NuGet restore is often the slowest part of a .NET pipeline. Enable caching to skip redundant downloads across runs. Azure Pipelines provides a built-in Cache@2 task that hashes your packages.lock.json or *.csproj files to determine cache validity. Without caching, every build re-downloads identical packages, adding 2–5 minutes unnecessarily.

Cache MISSDownload all NuGet packages+3–5 minutes per buildNetwork I/O + extraction overheadCache HITRestore from pipeline cache< 30 secondsLocal disk extraction onlyCache Configurationkey: 'nuget | "$(Agent.OS)" | /packages.lock.json'path: $(NUGET_PACKAGES)Invalidates automatically on dependency change
Cache hit vs miss impact on build duration when deploying .NET apps with Azure Pipelines.

Enable NuGet caching correctly

Generate a lock file to ensure deterministic cache keys. Without it, the cache key may not invalidate properly when dependencies change, leading to stale package resolution. Add <RestorePackagesWithLockFile>true</RestorePackagesWithLockFile> to your Directory.Build.props and commit the resulting packages.lock.json.

- task: Cache@2
  displayName: 'Cache NuGet packages'
  inputs:
    key: 'nuget | "$(Agent.OS)" | /packages.lock.json'
    restoreKeys: |
      nuget | "$(Agent.OS)"
    path: $(NUGET_PACKAGES)
    cacheHitVar: NUGET_CACHE_HIT

- task: DotNetCoreCLI@2
  displayName: 'Restore NuGet packages'
  condition: ne(variables.NUGET_CACHE_HIT, 'true')
  inputs:
    command: 'restore'
    projects: '/*.csproj'
    feedsToUse: 'select'
    vstsFeed: 'my-feed'

Parallelize independent jobs

If your solution contains multiple independent projects (e.g., API, background worker, frontend), run their build/test jobs in parallel using job dependencies. This reduces total pipeline duration significantly on larger solutions. Use dependsOn only where ordering is actually required.

Reliable .NET Deployment Requires Intentional Pipeline Design

When you deploy .NET apps with Azure Pipelines, reliability comes from deliberate choices: separating stages, securing secrets outside source control, enforcing test gates, choosing the right hosting target, and optimizing build performance through caching. These aren't optional best practices—they're the difference between a pipeline that breaks at 2 AM and one your team trusts completely. Start with the YAML structure outlined here, add Key Vault integration before your first production deploy, and measure your cache hit rate weekly. If your pipeline feels fragile or your deployments require manual verification steps, the foundation needs work before adding more automation. Need help auditing your existing pipeline or designing one from scratch? Reach out to discuss your deployment challenges.

Frequently Asked Questions

Create a new pipeline, select your repository, and choose the ASP.NET Core template. This generates a starter azure-pipelines.yml file with build, test, and publish tasks preconfigured for .NET 9 applications targeting Linux or Windows agents.

Azure DevOps provides one free Microsoft-hosted parallel job with 1,800 monthly minutes. Additional parallel jobs cost $40 per month in 2026. Self-hosted agents are free but require you to manage underlying compute infrastructure and maintenance costs separately.

Yes. Use the VSBuild task instead of dotnet build. You must use Windows agents since .NET Framework requires MSBuild. Configure the solution path and platform settings explicitly in your YAML pipeline definition for legacy framework compatibility.

Never commit secrets to source control. Use Azure Key Vault linked variable groups or pipeline variables marked as secret. The FileTransform task replaces tokens at deploy time without exposing sensitive connection strings or API keys in logs.

Use ubuntu-latest for cross-platform .NET 9 apps to reduce build times and costs. Use windows-latest only when targeting Windows-specific APIs or IIS deployment. Verify SDK versions match your global.json specification to prevent runtime mismatches.

Generate idempotent SQL scripts using dotnet ef migrations script during the build stage. Apply these scripts via the SqlAzureDacpacDeployment task before app deployment. This prevents direct database access from production agents and ensures repeatable schema changes.

Your global.json specifies an SDK version not installed on the agent. Add the UseDotNet task before building to install the exact required version. Pin specific versions rather than relying on latest tags to ensure consistent builds across all environments.

Define stages for dev, staging, and production in your YAML. Attach environment resources with configured approvals and checks. Production deployments pause automatically until designated approvers validate the release through the Azure DevOps web interface or mobile notifications.

Yes. Use the KubernetesManifest task after building Docker images with the Docker task. Push containers to Azure Container Registry first. Reference image tags via pipeline variables to maintain traceability between commits and running pods in your cluster.

Add the Cache task targeting $(NUGET_PACKAGES) directory with restoreKeys based on csproj hash. This reduces package download time by reusing previously restored dependencies across pipeline runs. Expect forty to sixty percent faster restore phases on subsequent builds.

Use branch filters on main and release branches for CD triggers. Configure path filters excluding docs and tests folders to avoid unnecessary deployments. Enable batch changes to merge multiple commits into single pipeline runs and reduce queue congestion.

Run dotnet test with --collect:"XPlat Code Coverage" flag. Add the PublishCodeCoverageResults task specifying Cobertura format. Azure Pipelines displays coverage trends directly in the UI and can enforce minimum thresholds via policy checks before merging pull requests.

Azure Pipelines offers superior integration with Azure App Service, Key Vault, and Artifacts feeds. GitHub Actions has larger community marketplace support. Choose Azure Pipelines for enterprise Azure-centric workflows requiring granular RBAC and compliance controls over open-source flexibility.

Add a deployment job condition checking previous task failure status. Trigger a separate rollback stage that redeploys the last known good artifact version stored in Azure Artifacts. Combine with health probes to detect failures within seconds of completion.

Yes.