In Aspire 9 there was a single fully supported deployment path: Azure Container Apps via azd. From Aspire 13 onwards that changed fundamentally. This article describes what aspire deploy actually does for each supported target – and where the current limits are.
The Common Foundation: the Pipeline Step
Regardless of the deployment target, every aspire deploy runs through the same pipeline architecture. Each resource in the AppHost contributes steps that declare dependencies on each other. Aspire builds an execution graph from these and runs independent steps in parallel. What used to be a sequential series of azd calls is now a visible, inspectable process:
aspire deploy --list-steps # show steps without executing
aspire deploy # run deployment
aspire destroy # tear down deployment
Azure Container Apps
The established path from Aspire 9 was retained and now runs through the pipeline. What's new is that the Azure Container Registry is no longer implicitly tied to the Container Apps environment – it's provisioned as its own resource. This allows reuse across environments and parallel image pushes during provisioning.
var registry = builder.AddAzureContainerRegistry("acr");
var env = builder.AddAzureContainerAppEnvironment("prod")
.WithAcrPullIdentity(registry);
Container App Jobs – for batch workloads and background processes – are stable from 13.4:
builder.AddProject<Projects.Worker>("worker")
.PublishAsAzureContainerAppJob();
Azure App Service
Azure App Service was added as a fully supported deployment target in 13.0. The audience is teams that want to keep using existing App Service infrastructure without migrating to Container Apps.
builder.AddAzureAppServiceEnvironment("staging");
From 13.1, deployment slots were added. Zero-downtime deployments can be configured directly from the AppHost: Aspire deploys to the staging slot and performs the swap to production automatically.
builder.AddProject<Projects.Api>("api")
.WithDeploymentSlot("staging");
The Aspire Dashboard is automatically wired up as an Application Insights integration for App Service deployments.
Docker Compose
Docker Compose followed a clear maturity path through the 13.x series:
- 13.1: First end-to-end deploy support –
aspire deployworked fully - 13.2: Left preview status, stable
The AppHost describes the application structure as usual. Aspire generates a docker-compose.yaml on publish and runs docker compose up on deploy. aspire destroy brings the stack back down.
builder.AddDockerComposeEnvironment("local");
Docker Compose is therefore the only fully supported deployment path without a cloud dependency – relevant for on-premises scenarios, customer environments without Azure access, or simple staging setups.
Kubernetes (generic)
From Aspire 13.3, aspire deploy supports generic Kubernetes clusters end-to-end. Aspire generates Helm charts from the AppHost and runs helm install or helm upgrade against the configured cluster.
builder.AddKubernetesEnvironment("k8s")
.WithHelm(helm => helm.ReleaseName("myapp").Namespace("production"));
aspire destroy runs helm uninstall and removes namespaces. The generated charts contain individual template files per resource – Deployments, Services, ConfigMaps, Secrets, and PersistentVolumeClaims.
Routing and TLS are modelable directly in the AppHost from 13.3:
builder.AddIngress("api-ingress")
.WithPath("/api", api)
.WithTls();
Aspire generates the corresponding Kubernetes Ingress resources. When WithTls() is used without an explicit hostname, Aspire automatically discovers the FQDN assigned by the cluster.
From 13.4, cert-manager with Let's Encrypt is available as a typed API:
env.AddCertManager()
.WithLetsEncrypt(staging: false);
Also in 13.4: external Helm charts can be included as deployment steps, so Aspire resources and standard ecosystem charts run in a single pipeline:
builder.AddHelmChart("nginx-ingress", "ingress-nginx/ingress-nginx", "4.10.0");
Azure Kubernetes Service (AKS)
AKS received its own hosting integration in 13.3 (Aspire.Hosting.Azure.Kubernetes), combining cluster provisioning and Helm deployment in a single pipeline:
builder.AddAzureKubernetesEnvironment("prod-aks")
.WithHelm();
Aspire generates Bicep templates for the cluster and then runs the Helm deployment step – without separate Terraform configuration or manually created clusters. Node pools, SKU tiers, and private clusters are configurable.
From 13.4, Azure Application Gateway for Containers (AGC) is directly integrable. Aspire automatically assigns the required Network Contributor role to the controller identity:
env.AddLoadBalancer("agc");
Kubernetes: Pitfalls in Practice
PublishAsDockerFile and the Missing args Problem
A silent trap affects Python and JavaScript services: calling .PublishAsDockerFile(configure => { }) on a Uvicorn app causes the generated args to disappear from the Helm deployment manifest – without an error, without a warning. The container starts with the wrong entrypoint.
The Aspire team has marked the issue with the silent-failure label: the behavior is intentional (the callback signals "I'm taking full control"), but the consequence is not documented. Anyone using PublishAsDockerFile with an empty callback must set the args explicitly:
builder.AddUvicornApp("app", "./app", "main:app")
.WithUv()
.PublishAsDockerFile(configure =>
{
configure.WithArgs("main:app", "--host", "0.0.0.0", "--port", "8000");
});
Until this is fixed (microsoft/aspire#16874): always check the generated templates/*/deployment.yaml files for args blocks after publishing.
Sidecar Containers and PersistentVolumes
Sidecar containers are currently not correctly translated into Kubernetes manifests – they end up as separate pods, making shared volumes impossible. Anyone defining sidecars in the AppHost and deploying to Kubernetes gets no error, but also no correct manifest.
WithPersistentVolume exists as of 13.4, but is marked [Experimental("ASPIRECOMPUTE002")]. Stateful workloads (databases, message brokers) on Kubernetes are therefore possible in principle – with the caveat that the API surface is not yet stable. A practical side effect: Aspire automatically promotes the deployment from a Deployment to a StatefulSet once a PVC is bound. This is correct, but can break existing helm upgrade runs if a workload was previously deployed as a Deployment.
The Two-Deploys Problem
A structural limit affects services that produce bootstrap data on first start that their consumers need – Keycloak (client secrets), Vault (tokens), MinIO (access keys), Grafana (API tokens). Locally via aspire run this works seamlessly through ResourceReadyEvent. In deploy mode there is no equivalent mechanism.
The current workaround is a two-deploys pattern: the first aspire deploy starts the bootstrap service, then a custom pipeline step calls the provisioning API and stores the values in IDeploymentStateManager. The next aspire deploy reads the stored state and injects the values into dependent services. This works – but is not idempotent on the first run and is not officially documented anywhere.
The underlying feature request (microsoft/aspire#18097) describes an AddDeferredValue concept that would solve exactly this. As of June 2026 it remains open.
What's Still Missing
AWS, GCP, and fully self-managed on-premises environments beyond Docker Compose still have no official Aspire integrations. The pipeline model is extensible via WithPipelineStepFactory – but custom providers must be fully implemented from scratch. For generic Kubernetes, aspire deploy does not provision the cluster; the cluster must already exist.
Need Support?
Want to run aspire deploy in your production environment – whether on Kubernetes, AKS, or Docker Compose – but not sure where to start or what pitfalls to expect? We're happy to help! Just contact us via our contact page and we'll work together to set up your deployment workflow with Aspire 13.




