Every .NET team on Azure eventually asks the same question: App Service, Container Apps, AKS or Functions? All four run the same dotnet publish output, and all four can be made to work for almost any workload with enough effort, which is exactly why the choice matters. This guide compares them on the things that actually change your day-to-day work: how much control you get, how scaling behaves, what networking and TLS look like, who owns operations, and what a realistic decision matrix and migration path look like once requirements shift.

Four Ways to Run .NET on Azure#

The four options sit at different points on a control-versus-convenience curve:

  • Azure App Service is a mature PaaS for web apps and APIs: you deploy code or a container, and Azure runs it on a shared or dedicated compute plan with built-in deployment slots, TLS and autoscale rules.
  • Azure Container Apps (ACA) runs containers on a serverless, Kubernetes-based platform without exposing Kubernetes itself. Scaling is driven by KEDA, and Dapr is a first-class, managed add-on.
  • Azure Kubernetes Service (AKS) gives you a real, standard Kubernetes API server and full control over networking, ingress, scheduling and add-ons, at the cost of owning the cluster.
  • Azure Functions is event-driven and consumption-based: you write handlers for triggers, and the platform worries about hosting and scaling. See Azure Functions with .NET for the isolated worker model, Flex Consumption and Durable Functions in depth.

None of these are mutually exclusive within one organization. A typical estate runs customer-facing APIs on App Service or Container Apps, background processing on Functions, and a platform team's shared services on AKS, all calling each other over the same virtual network.

How Each Option Manages Your Application#

The real difference between these four is who does what when your code needs to run, scale or recover from a failure.

App Service and Functions on the Dedicated or Premium plans manage a pool of VM-based workers for you: you pick a plan and instance count or scaling rule, and Azure handles OS patching, the web server and the health model. Container Apps manages a Kubernetes cluster for you behind the scenes, so you never see nodes, pods or a kubectl command, but you also cannot reach in for cluster-level customization when you need it. AKS gives you the cluster itself: you (or a platform team) choose node pools, networking mode, ingress and every add-on, and you are responsible for keeping the control plane and nodes patched and sized correctly, in exchange for complete control over how workloads are scheduled.

That gradient shows up again in identity and configuration. All four integrate with Microsoft Entra ID through managed identity, so the same DefaultAzureCredential code works unchanged on every one of them; what differs is how you attach the identity, from a one-line setting on App Service and Container Apps to a workload identity federation you configure yourself on AKS.

Getting Started: The Same API, Four Ways#

A minimal ASP.NET Core API needs almost no code changes across these hosts; the differences are in how you get the build there.

Bash
# App Service: build and deploy a .NET project directly, no Dockerfile needed
az webapp up --name shop-api --resource-group rg-shop --runtime "DOTNETCORE:10.0"

# Container Apps: build a container image and deploy it in one step
az containerapp up --name shop-api --resource-group rg-shop \
  --source . --target-port 8080 --ingress external

# AKS: apply a Deployment and Service against an existing cluster
az aks get-credentials --resource-group rg-shop --name aks-shop
kubectl apply -f shop-api-deployment.yaml

az webapp up and az containerapp up build from source and are meant for getting started quickly; production pipelines typically build a container image explicitly and deploy it through CI/CD instead. AKS has no equivalent one-liner because a Deployment manifest, not a CLI verb, is the unit of deployment.

Azure App Service: PaaS with Deployment Slots#

App Service's signature feature is deployment slots: separate, fully functional instances of your app that share the same App Service Plan. Standard tier plans include a handful of slots and Premium tiers include more, which is enough for a staging slot plus one or two feature branches. Swapping a slot into production is a warm operation, not a redeploy: App Service restarts the app in the target slot, waits on any configured warm-up requests, then switches routing, so users see no cold start and a bad deployment can be swapped back in seconds.

Bash
az webapp deployment slot create --name shop-api --resource-group rg-shop --slot staging
az webapp deploy --name shop-api --resource-group rg-shop --slot staging \
  --src-path ./publish.zip --type zip

# Swap validates the target, then routes production traffic to it
az webapp deployment slot swap --name shop-api --resource-group rg-shop \
  --slot staging --target-slot production

App Service also owns autoscale rules based on CPU, memory or custom metrics, regional VNet integration, and free, auto-renewing managed TLS certificates for custom domains. It is the least operationally demanding option for a conventional web app or API that does not need Kubernetes-level scheduling control, and it remains the natural home for .NET Framework workloads that are not yet containerized.

Azure Container Apps: Serverless Containers with KEDA and Dapr#

Container Apps scales on KEDA (Kubernetes Event-Driven Autoscaling), which means scale rules are not limited to CPU and memory: a revision can scale on HTTP concurrency, a Service Bus queue length, an Event Hub's consumer lag or dozens of other event sources, including down to zero replicas when there is no traffic to serve.

Bicep
resource shopApi 'Microsoft.App/containerApps@2024-03-01' = {
  name: 'shop-api'
  properties: {
    template: {
      containers: [ { name: 'shop-api', image: acrLoginServer } ]
      scale: {
        minReplicas: 0
        maxReplicas: 10
        rules: [
          { name: 'http-concurrency', http: { metadata: { concurrentRequests: '50' } } }
          {
            name: 'orders-queue'
            custom: {
              type: 'azure-servicebus'
              metadata: { queueName: 'orders', messageCount: '5' }
              auth: [ { triggerParameter: 'connection', secretRef: 'servicebus-connection' } ]
            }
          }
        ]
      }
    }
  }
}

Dapr is where Container Apps stands apart from the other three: enabling it is a flag on the container app, not a separate service to deploy, and it gives you service invocation, pub/sub, state and secrets without adding client libraries for each backing service.

Bash
az containerapp create --name orders --resource-group rg-shop \
  --environment shop-env --image contoso.azurecr.io/orders:1.4.2 \
  --ingress internal --target-port 8080 \
  --enable-dapr --dapr-app-id orders --dapr-app-port 8080 --dapr-app-protocol http

Check the current support matrix before you design around it: core building blocks are generally available on Container Apps, but Dapr's actor and workflow SDK packages are not supported there as of Microsoft's own documentation. See Dapr for .NET Developers for the full building-block picture and where AKS is the better fit for actors and workflow.

Azure Kubernetes Service: Full Control, Full Responsibility#

AKS is the option to reach for when you need something the other three intentionally do not expose: custom admission controllers, a service mesh, GPU node pools, DaemonSets, multiple ingress controllers, or fine-grained pod scheduling and affinity rules. Every one of those is available because AKS is a standard, conformant Kubernetes API server; nothing is Azure-specific about the workloads you run on it.

YAML
apiVersion: apps/v1
kind: Deployment
metadata:
  name: shop-api
spec:
  replicas: 3
  selector:
    matchLabels: { app: shop-api }
  template:
    metadata:
      labels: { app: shop-api }
    spec:
      containers:
        - name: shop-api
          image: contoso.azurecr.io/shop-api:1.4.2
          ports: [ { containerPort: 8080 } ]
          resources:
            requests: { cpu: "250m", memory: "256Mi" }
            limits: { memory: "512Mi" }
---
apiVersion: autoscaling/v2
kind: HorizontalPodAutoscaler
metadata:
  name: shop-api
spec:
  scaleTargetRef: { apiVersion: apps/v1, kind: Deployment, name: shop-api }
  minReplicas: 3
  maxReplicas: 20
  metrics:
    - type: Resource
      resource: { name: cpu, target: { type: Utilization, averageUtilization: 70 } }

The cluster autoscaler adds and removes nodes as pending pods demand them, on top of the HorizontalPodAutoscaler adding and removing pods; you own both layers, along with ingress (NGINX or the Application Gateway Ingress Controller), certificate issuance (typically cert-manager) and Azure AD Workload Identity for pod-level managed identity. Microsoft also offers a more opinionated, preconfigured AKS cluster mode with sensible defaults for teams that want Kubernetes without owning every setting from day one, which narrows but does not eliminate the operational gap with Container Apps. The Kubernetes on .NET guide covers probes, resource limits and rollout strategy in depth, and containerizing .NET applications covers building the images all three container-based options deploy.

App Service vs Container Apps vs AKS vs Functions: Head-to-Head#

DimensionApp ServiceContainer AppsAKSFunctions
ControlPlatform-managed VMs; some tuningPlatform-managed Kubernetes; app-level configFull cluster and Kubernetes API accessFully platform-managed; event-driven only
ScalingAutoscale rules; CPU, memory, metricsKEDA rules; scale to zeroHPA plus cluster autoscaler; you configure bothPer-function, event-driven; scale to zero
NetworkingRegional VNet integrationVNet integration; internal or external ingressFull control: CNI mode, network policies, service meshVNet integration on Premium/Flex/Dedicated
Deployment modelDeployment slots with warm swapTraffic-split revisionsRolling updates, canaries via tooling you addVersioned deployments; slots on non-Flex plans
DaprNot built inFirst-class, managed sidecarYou install and operate Dapr yourselfNot applicable
Cost modelPlan-based, always-on by defaultPay for allocated vCPU/memory, scales to zeroPay for nodes regardless of pod countPay per execution (Consumption/Flex) or plan-based
Ops burdenLowLow to moderateHigh: you own the clusterLowest for event-driven workloads
Best fitConventional web apps and APIsMicroservices, event-driven containers, Dapr usersComplex scheduling, service mesh, multi-tenant platformsEvent handlers, queues, timers, webhooks

Networking, Custom Domains and TLS#

Custom domains and TLS follow the same shape on three of the four: bind a domain, then either upload a certificate or let Azure issue and renew a free managed certificate.

Bash
# App Service: bind a custom domain, then request a free managed certificate for it
az webapp config hostname add --webapp-name shop-api --resource-group rg-shop \
  --hostname api.contoso.com
az webapp config ssl create --resource-group rg-shop --name shop-api \
  --hostname api.contoso.com

Container Apps supports the same pattern of custom domains with managed or bring-your-own certificates at the environment level. AKS has no built-in equivalent: you terminate TLS at an ingress controller you deploy and manage certificate issuance yourself, most commonly with cert-manager, which trades a one-line Azure feature for full control over the certificate chain and renewal process. Functions on Premium, Dedicated or Flex Consumption plans follows the App Service model, since it shares the same underlying hosting stack.

Managed Identity Across All Four#

Every option authenticates to other Azure services through Microsoft Entra ID managed identity, and the C# is identical regardless of host:

C#
using Azure.Identity;
using Azure.Storage.Blobs;

var credential = new DefaultAzureCredential();
var blobClient = new BlobServiceClient(
    new Uri("https://shopstorage.blob.core.windows.net"), credential);

What changes is how the identity gets attached. App Service, Container Apps and Functions expose it as a toggle on the resource (system-assigned) or a reference to a user-assigned identity resource. AKS requires configuring Azure AD Workload Identity: a federated credential trust between a Kubernetes service account and the managed identity, which is more setup but scopes the identity precisely to the pods that use that service account, rather than to every process on a shared compute plan.

Decision Matrix: Which Should You Choose?#

If your priority is...Choose
Fastest path to production for a conventional web app or APIApp Service
Microservices with event-driven scaling and you want DaprContainer Apps
You already run Kubernetes elsewhere, or need custom scheduling, a mesh or GPUsAKS
Pure event handlers: queues, timers, webhooks, blob triggersFunctions
Minimizing idle cost for spiky or intermittent trafficContainer Apps or Functions (both scale to zero)
Zero-downtime blue/green releases via a platform featureApp Service (slots) or Container Apps (revisions)
One platform team operating dozens of unrelated servicesAKS, if the team already carries that operational cost

Migration Paths Between Options#

Moving between these four is usually a redeployment, not a rewrite, because the differences live in infrastructure and configuration rather than application code:

  • App Service to Container Apps. Containerize the app if it is not already, replace autoscale rules with KEDA scale rules, and move deployment-slot workflows to revision-based traffic splitting.
  • Container Apps to AKS. The container image is already portable; the work is writing Kubernetes manifests or a Helm chart for what Container Apps generated implicitly, and standing up ingress, certificates and identity federation yourself.
  • AKS to Container Apps. Realistic once a workload no longer needs cluster-level customization; drop custom operators, CRDs and mesh dependencies first, since Container Apps has no equivalent extension point for them.
  • Any option to Functions. Works cleanly for genuinely event-driven pieces of a system; forcing a long-running or stateful service into functions usually fights the platform instead of simplifying it.

.NET Aspire smooths the App Service and Container Apps paths in particular: its azd-based deployment provisions a resource group, container registry and identity for you and wires connection strings between services, though production infrastructure usually still needs the customization layer Aspire's deployment tooling provides for anything beyond the default topology. See .NET Aspire: Cloud-Native Orchestration for .NET for the local development side of that story.

Best Practices#

  • Start with App Service or Container Apps unless you already have a concrete reason to need AKS; both cut operational work substantially versus running Kubernetes yourself.
  • Use deployment slots or revisions for every production release, not just major ones, so rollback is a routing change instead of a redeploy.
  • Scope managed identities narrowly. Prefer user-assigned identities with least-privilege role assignments over broad, shared credentials.
  • Design scale rules around the actual bottleneck. Queue depth or concurrency usually predicts load better than CPU for I/O-bound .NET services.
  • Budget real time for AKS operations. Patching, upgrades, ingress and certificate management are ongoing work, not a one-time setup cost.
  • Keep container images identical across environments and let configuration, not image content, vary between App Service, Container Apps and AKS.

Common Pitfalls#

  • Choosing AKS by default. Many teams adopt Kubernetes for workloads that Container Apps or App Service would run with far less ongoing effort.
  • Scaling on CPU alone in Container Apps or AKS. I/O-bound APIs often sit at low CPU while queued work or connections pile up; add a custom or HTTP scale rule.
  • Forgetting AKS has no default TLS story. Teams that assume "Azure handles certificates" are surprised when nothing terminates TLS until an ingress controller is deployed.
  • Treating deployment slots as a staging environment substitute. A slot shares the App Service Plan's compute and can share connection limits with production; size the plan for both.
  • Mixing Dapr resiliency and in-process retries on Container Apps. Both retrying the same call multiplies failures into far more attempts than either was tuned for.
  • Assuming Dapr actors and workflow work on Container Apps. Confirm the current support matrix before designing around them; AKS remains the reliable home for full Dapr feature coverage.

Frequently Asked Questions#

Is Azure Container Apps built on Kubernetes?#

Yes, internally, but it does not expose the Kubernetes API to you. You configure container apps, revisions and scale rules through Azure Resource Manager or the CLI, not kubectl, which is the trade-off for much lower operational overhead than AKS.

Can I run Dapr on Azure App Service?#

No. Dapr integration is specific to Container Apps and to Kubernetes (including AKS), where it runs as a sidecar. On App Service, use the target service's native SDK or a resilience library such as Polly instead.

Do App Service deployment slots cost extra?#

Slots do not have a separate price; they share the compute of the App Service Plan they belong to, so the cost impact is indirect, through needing a plan large enough for staging and production traffic together during a swap.

When does AKS's extra complexity actually pay off?#

When you need something the other platforms deliberately do not expose: a service mesh, custom Kubernetes operators, GPU workloads, multiple ingress controllers, or one platform team standardizing dozens of otherwise unrelated services on a shared cluster.

Can Azure Functions and Container Apps share the same environment?#

Yes. Functions can run in a Container Apps environment alongside other container apps, which is useful when event-driven and always-on services belong in the same virtual network and share the same Dapr or networking configuration.

Summary#

  • App Service is the lowest-effort choice for conventional web apps and APIs, with deployment slots as its standout feature.
  • Container Apps suits event-driven microservices that want KEDA-based scaling and managed Dapr without owning Kubernetes.
  • AKS earns its operational cost when you need Kubernetes-native control: custom scheduling, a service mesh or GPU workloads.
  • Azure Functions remains the best fit for pure event handlers; see the dedicated Functions guide for the isolated worker model and Durable Functions.
  • Managed identity, custom domains and TLS work on all four, but AKS is the only one where you build the certificate and ingress story yourself.

Further Reading#