Skip to main content
  1. Posts/

Aspire CLI Part 2 - Deployment and Pipelines

Chris Ayers
Author
Chris Ayers
I am a Principal Software Engineer at Microsoft, father, nerd, gamer, and speaker.

In Part 1, we covered the basics of the Aspire CLI: creating projects with aspire new, adding Aspire to existing apps with aspire init, running with aspire run, and managing integrations with aspire add and aspire update. Now let’s dive into deployment and CI/CD pipelines.

Prerequisite: Aspire 13 requires .NET SDK 10.0.100 or later. Make sure you have it installed before using the commands in this post.

The Publish and Deploy Model
#

Aspire separates deployment into two distinct phases:

  1. Publish (aspire publish) — Generates intermediate, parameterized deployment artifacts (Compose files, Kubernetes manifests, Bicep templates, etc.)
  2. Deploy (aspire deploy) — Resolves parameters and applies those artifacts to a target environment

This separation is intentional. Published assets contain placeholders instead of concrete values — secrets and environment-specific configuration are injected later at deploy time. This keeps sensitive data out of your artifacts and enables the same published output to target multiple environments.

aspire publish - Generate Deployment Artifacts
#

The aspire publish command generates deployment artifacts based on the compute environments configured in your AppHost. A compute environment represents a target platform and determines what gets generated.

aspire publish -o ./artifacts

The output depends on which hosting integration packages you’ve added:

NuGet PackageTargetPublishDeploy
Aspire.Hosting.DockerDocker Compose✅ Yes🧪 Preview
Aspire.Hosting.KubernetesKubernetes✅ Yes🧪 Preview
Aspire.Hosting.Azure.AppContainersAzure Container Apps✅ Yes✅ Yes (Preview)
Aspire.Hosting.Azure.AppServiceAzure App Service✅ Yes✅ Yes (Preview)

If no integration supports publishing, aspire publish will tell you:

No resources in the distributed application model support publishing.

Parameterized Output
#

Published artifacts contain placeholders rather than concrete values. For example, a Docker Compose publish might generate:

services:
  pg:
    image: "docker.io/library/postgres:17.2"
    environment:
      POSTGRES_PASSWORD: "${PG_PASSWORD}"
    ports:
      - "8000:5432"
  api:
    image: "${API_IMAGE}"
    environment:
      ConnectionStrings__db: "Host=pg;Port=5432;Username=postgres;Password=${PG_PASSWORD};Database=db"

Notice ${PG_PASSWORD} and ${API_IMAGE} are not resolved during publish. You supply their values at deploy time — through environment variables, .env files, or CI/CD pipeline secrets.

Docker Compose Example
#

The most common pattern from the Aspire samples uses Docker Compose as the compute environment. See the Docker Compose sample for a complete working example.

#:package Aspire.Hosting.Docker@13-*
#:sdk Aspire.AppHost.Sdk@13.0.0

var builder = DistributedApplication.CreateBuilder(args);

builder.AddDockerComposeEnvironment("dc");

var postgres = builder.AddPostgres("postgres")
                      .WithDataVolume()
                      .WithPgAdmin();
var db = postgres.AddDatabase("db");

builder.AddCSharpApp("api", "./api")
       .WithHttpHealthCheck("/health")
       .WithExternalHttpEndpoints()
       .WaitFor(db)
       .WithReference(db);

builder.Build().Run();

Then the standard workflow is:

aspire run                          # Run locally
aspire publish -o ./artifacts       # Generate Docker Compose files
aspire deploy                       # Deploy to Docker Compose
aspire do docker-compose-down-dc    # Tear down the deployment

Azure Container Apps Example
#

For Azure, add the Azure Container Apps environment. See the Azure Container Apps sample for a complete working example.

#:package Aspire.Hosting.Azure.AppContainers@13.0.0
#:package Aspire.Hosting.Azure.Storage@13.0.0
#:sdk Aspire.AppHost.Sdk@13.0.0

var builder = DistributedApplication.CreateBuilder(args);

builder.AddAzureContainerAppEnvironment("env");

var storage = builder.AddAzureStorage("storage")
                     .RunAsEmulator();

var blobs = storage.AddBlobContainer("images");

builder.AddProject<Projects.Api>("api")
       .WithExternalHttpEndpoints()
       .WithReference(blobs);

builder.Build().Run();

Publishing this generates Bicep templates that you can review, customize, and deploy:

aspire publish -o ./azure-artifacts   # Generates Bicep files
aspire deploy                         # Deploys to Azure Container Apps

Kubernetes Example
#

For Kubernetes, add the hosting package and configure a compute environment. See the Kubernetes sample for a complete working example.

var builder = DistributedApplication.CreateBuilder(args);

var k8s = builder.AddKubernetesEnvironment("k8s");

var api = builder.AddProject<Projects.Api>("api")
    .WithExternalHttpEndpoints();

builder.Build().Run();

Generate and deploy:

# Generate Kubernetes manifests
aspire publish -o ./k8s-output

# Apply with kubectl or Helm
kubectl apply -f ./k8s-output

Multiple Compute Environments
#

If you add multiple compute environments, Aspire needs to know which resource goes where. Use WithComputeEnvironment to disambiguate:

var k8s = builder.AddKubernetesEnvironment("k8s-env");
var compose = builder.AddDockerComposeEnvironment("docker-env");

builder.AddProject<Projects.Frontend>("frontend")
    .WithComputeEnvironment(k8s);

builder.AddProject<Projects.Backend>("backend")
    .WithComputeEnvironment(compose);

Without this, Aspire throws an ambiguous environment exception at publish time.

aspire deploy - Deploy to a Target
#

The aspire deploy command resolves parameters and applies published artifacts to a target environment. This command is in preview and under active development.

aspire deploy

Note: The deploy command may change as it matures. Keep an eye on the .NET Blog and the Aspire deployment docs for updates.

What happens depends on your compute environment:

  • Docker Compose — Builds images, resolves variables, runs docker compose up
  • Azure Container Apps — Provisions Azure resources, builds and pushes container images, deploys apps
  • Kubernetes — Generates manifests and applies them

For Docker Compose deployments, the workflow from the official samples is straightforward:

aspire run      # Run locally
aspire deploy   # Deploy to Docker Compose
aspire do docker-compose-down-dc  # Teardown

For Azure deployments, aspire deploy prompts for:

  1. Azure sign-in and subscription selection
  2. Resource group creation or selection
  3. Location for Azure resources

The command then provisions infrastructure, builds containers, pushes to ACR, and deploys — all in one step.

For non-interactive deployment (CI/CD), set these environment variables to skip the prompts:

Azure__SubscriptionId=<your-subscription-id>
Azure__Location=<azure-region>
Azure__ResourceGroup=<resource-group-name>

aspire do - Pipeline Automation
#

The aspire do command executes pipeline steps defined by hosting integrations. Use aspire do diagnostics to discover what steps are available and their dependencies:

# List available pipeline steps
aspire do diagnostics

# Tear down a Docker Compose deployment
aspire do docker-compose-down-dc

# The naming convention is: docker-compose-down-{environment-name}

Well-known pipeline steps include build, push, publish, and deploy. Resources can contribute custom steps — for example, Docker Compose adds teardown steps. This command is particularly useful in CI/CD pipelines and for managing environment lifecycle.

aspire exec - Run Commands in Resource Context
#

The aspire exec command runs commands in the context of a specific resource with the correct connection strings and environment variables. This command is disabled by default — enable it first. See the exec sample for a complete working example with Postgres and Redis.

# Enable the exec feature
aspire config set features.execCommandEnabled true

Then use the --resource (or -r) flag to specify the target:

# Run EF Core migrations
aspire exec --resource mydb -- dotnet ef database update

# Open an interactive shell in a container
aspire exec --resource redis -- redis-cli

# Start a dependency and then run against it
aspire exec --start-resource mydb -- dotnet ef migrations add Init

The --start-resource (or -s) flag is useful when you need to start a resource (and its dependencies) before running a command against it.

Azure Developer CLI (azd) Integration
#

For production Azure deployments, the Azure Developer CLI (azd) has first-class Aspire support and is the more mature deployment path:

# Initialize azd in your project directory
azd init

# Provision infrastructure and deploy in one command
azd up

During azd init, you’ll:

  1. Select which services to expose to the internet
  2. Name your environment (e.g., dev, prod)
  3. Choose your Azure subscription and location

The azd up command handles the full lifecycle: azd packageazd provisionazd deploy. Projects are packaged into containers, Azure resources are provisioned via Bicep, and containers are pushed to Azure Container Registry and deployed to Container Apps.

Generated files:

  • azure.yaml — Maps Aspire AppHost services to Azure resources
  • .azure/config.json — Active environment configuration
  • .azure/{env}/.env — Environment-specific overrides
  • .azure/{env}/config.json — Public endpoint configuration

GitHub Actions
#

Using azd (Recommended for Azure)#

name: Deploy Aspire App

on:
  push:
    branches: [main]

jobs:
  deploy:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4

      - name: Setup .NET
        uses: actions/setup-dotnet@v4
        with:
          dotnet-version: '10.0.x'

      - name: Install azd
        uses: Azure/setup-azd@v2

      - name: Log in to Azure
        uses: azure/login@v2
        with:
          creds: ${{ secrets.AZURE_CREDENTIALS }}

      - name: Provision and Deploy
        run: azd up --no-prompt
        env:
          AZURE_ENV_NAME: ${{ vars.AZURE_ENV_NAME }}
          AZURE_LOCATION: ${{ vars.AZURE_LOCATION }}
          AZURE_SUBSCRIPTION_ID: ${{ vars.AZURE_SUBSCRIPTION_ID }}

Using Aspire CLI
#

name: Deploy with Aspire CLI

on:
  push:
    branches: [main]

jobs:
  deploy:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4

      - name: Setup .NET
        uses: actions/setup-dotnet@v4
        with:
          dotnet-version: '10.0.x'

      - name: Install Aspire CLI
        run: curl -fsSL https://aspire.dev/install.sh | bash

      - name: Publish artifacts
        run: aspire publish -o ./artifacts
        working-directory: ./src/MyApp.AppHost

      - name: Deploy with Docker Compose
        run: |
          cd ./artifacts
          docker compose up --build -d

Legacy Manifest Format
#

Starting with Aspire 9.2, the single deployment manifest is being phased out in favor of the aspire publish / aspire deploy model with hosting integration extensibility. The legacy manifest is still available for debugging:

aspire do publish-manifest --output-path ./diagnostics

This produces a manifest snapshot for inspecting resource graphs and troubleshooting, but it’s not the primary deployment path.

Learn More
#

Wrapping Up
#

The publish/deploy model gives you flexibility: publish generates parameterized artifacts, and deploy resolves values and applies them. Whether you’re targeting Docker Compose for local staging, Kubernetes for container orchestration, or Azure Container Apps for managed hosting, the workflow is consistent.

For production Azure deployments, I recommend azd for its mature infrastructure-as-code capabilities. For Docker Compose and local deployment workflows, aspire deploy is increasingly capable as it matures.

In Part 3, we explore one of Aspire’s most exciting features: MCP (Model Context Protocol) integration, which lets AI coding agents like GitHub Copilot and Claude understand and interact with your running Aspire applications.

Until next time, happy Aspiring!

Related Posts#

Related

Building a Flexible AI Provider Strategy in .NET Aspire

How I architected a single codebase to seamlessly switch between Azure OpenAI, GitHub Models, Ollama, and Foundry Local without touching the API service When building my latest .NET Aspire application, I faced a common challenge: how do you develop and test with different AI providers without constantly rewriting your API service? The answer turned out to be surprisingly elegant - a configuration-driven approach that lets you switch between four different AI providers with zero code changes.

Aspiring .NET & Resilience @ NDC Oslo 2025

As I’m flying from Oslo to Amsterdam, I’m still buzzing with energy and inspiration from NDC Oslo 2025. It was an incredible week of learning, sharing, and connecting with some of the brightest minds in technology. From thought-provoking keynotes that challenged our assumptions to the epic The Linebreakers concert at Brewgata, every moment reinforced why each NDC conference but NDC Oslo remains one of the premier developer conferences in the world.

Clearing NuGet Caches

·680 words·4 mins
What is NuGet? # NuGet is an essential packaging tool used in the .NET ecosystem. NuGet is how packages for .NET are created, hosted, and consumed, along with the tools for each of those roles. For many, NuGet is used through Visual Studio to install and manage packages. The dotnet CLI also provides functionality for adding packages, updating packages, and creating packages.

ARM - Part 1: Azure Resource Manager

The Journey Begins # I’ve been an azure developer for years. Originally I worked with “Classic Mode” and Cloud Services. Then I moved to ARM and Web Apps. Lately I’ve been doing DevOps but I only recently started working with ARM Termplates. First, let’s dive into a little history. History # Azure has grown and changed since it was first introduced. Originally, it was a research project called, “Project Red Dog”. Azure has been commercially available since 2010. For four years, there was a limited way to interact with Azure, ASM the Azure Service Manager.