DocsTry Aspire
DocsTry

Deploy to Docker Compose

Docker Compose is a deployment target for Aspire applications. When you add a Docker Compose environment to your AppHost, Aspire generates Docker Compose files, environment variable configurations, and container images from your app model. You can then deploy these artifacts to any machine running Docker or Podman.

Aspire supports both Docker and Podman as container runtimes for Docker Compose deployments. Podman is popular in security-conscious, daemonless, and rootless environments. Aspire automatically detects and uses the best available runtime — no additional configuration is required.

At deploy time, Aspire probes Docker and Podman in parallel and selects the active runtime using the following priority:

  1. A runtime that is running (daemon active) is preferred over one that is merely installed.
  2. If both runtimes are running, Docker is preferred as the tiebreaker.
  3. The detected runtime is cached for the lifetime of the operation.

To bypass auto-detection and force a specific runtime, set the ASPIRE_CONTAINER_RUNTIME environment variable to docker or podman before invoking the Aspire CLI:

Terminal
ASPIRE_CONTAINER_RUNTIME=podman aspire deploy

When this variable is set, the chosen runtime is shown by aspire doctor with the reason (explicit configuration) and is honored even when both runtimes are running.

When Podman is the active runtime, Aspire uses podman-compose (or the Docker Compose v2 provider for Podman) for compose up / compose down operations. Service discovery uses podman ps --filter label=..., which is compatible with both podman-compose (Python) and Docker Compose v2 providers.

To deploy with Docker Compose, add a Docker Compose environment resource to your AppHost using AddDockerComposeEnvironment:

apphost.mts
import {
function createBuilder(): IDistributedApplicationBuilder

Creates a new distributed application builder

createBuilder
} from './.aspire/modules/aspire.mjs';
const
const builder: IDistributedApplicationBuilder
builder
= await
function createBuilder(): IDistributedApplicationBuilder

Creates a new distributed application builder

createBuilder
();
await
const builder: IDistributedApplicationBuilder
builder
.
IDistributedApplicationBuilder.addDockerComposeEnvironment(name: string): DockerComposeEnvironmentResource

Adds a Docker Compose environment to the application model.

addDockerComposeEnvironment
('env');
await
const builder: IDistributedApplicationBuilder
builder
.
IDistributedApplicationBuilder.addProject(name: string, projectPath: string, options?: {
launchProfileOrOptions?: ProjectResourceOptions;
}): ProjectResource (+1 overload)

Adds a .NET project resource

addProject
('api', '../Api/Api.csproj');
await
const builder: IDistributedApplicationBuilder
builder
.
IDistributedApplicationBuilder.build(): DistributedApplication

Builds the distributed application

build
().
DistributedApplication.run(cancellationToken?: cancellationToken): void

Runs the distributed application

run
();

When a Docker Compose environment is present, all resources are automatically published as Docker Compose services — no additional opt-in is required.

For more information on the hosting integration and resource configuration, see Docker integration.

Aspire provides a progressive deployment workflow for Docker Compose, allowing you to publish, prepare environments, and deploy in separate steps or all at once.

  1. Publish the application

    To generate Docker Compose files and artifacts without building container images, use the aspire publish command:

    Terminal
    aspire publish

    This command:

    • Generates a docker-compose.yaml from the AppHost
    • Generates a .env file with expected parameters (unfilled)
    • Outputs everything to the aspire-output directory
  2. Prepare environment configurations

    To prepare environment-specific configurations and build container images, use the aspire do prepare-{resource-name} command, where {resource-name} is the name of the Docker Compose environment resource:

    Terminal
    # For staging environment
    aspire do prepare-compose --environment staging
    # For production environment
    aspire do prepare-compose --environment production

    These commands:

    • Generate a docker-compose.yaml from the AppHost
    • Generate environment-specific .env files with filled-in values
    • Build container images
    • Output everything to the aspire-output directory
  3. Deploy to Docker Compose

    To perform the complete deployment workflow in one step, use the aspire deploy command:

    Terminal
    aspire deploy

    This command:

    • Generates a docker-compose.yaml from the AppHost
    • Generates environment-specific .env files with filled-in values
    • Builds container images
    • Outputs everything to the aspire-output directory
    • Runs docker compose up -d --remove-orphans against the generated files

To stop and remove a running Docker Compose deployment, use the aspire destroy command:

Terminal
aspire destroy

The command shows the resources that will be removed and prompts for confirmation. Once confirmed, it stops and removes all containers, networks, and volumes created by the Docker Compose deployment.

Docker Compose deployments support multiple environments through the --environment flag. Each environment produces its own .env.{environment} file with environment-specific parameter values, while sharing the same docker-compose.yaml.

Use the --environment flag to deploy the same application to different environments on the same or different Docker hosts:

Deploy to staging
aspire deploy --environment staging
Deploy to production
aspire deploy --environment production

Each deployment:

  • Runs the AppHost with the specified environment, so builder.Environment.EnvironmentName reflects staging or production. Any environment-based branching in your AppHost applies automatically.
  • Generates an .env.staging or .env.production file with filled-in parameter values.
  • Maintains a separate deployment state cache per environment.

Publish artifacts for multiple environments

Section titled “Publish artifacts for multiple environments”

To generate artifacts without deploying, use aspire do prepare-{resource-name} with the --environment flag:

Prepare staging and production artifacts
aspire do prepare-env --environment staging
aspire do prepare-env --environment production

This generates environment-specific .env files that you can deploy independently, for example from a CI pipeline that targets different Docker hosts per environment.

For more on how environments work across all deployment targets, see the Environments guide. For command details on prepare-* steps, see the CLI reference: aspire do.

When you publish or deploy, Aspire generates the following artifacts in the aspire-output directory:

ArtifactDescription
docker-compose.yamlThe generated Compose file defining all services, networks, and volumes.
.envEnvironment variable file with expected parameters (unfilled after aspire publish).
.env.{environment}Environment-specific variable files with filled-in values (generated during prepare or deploy).
Dockerfile (per resource)Dockerfiles for resources that use existing or programmatically generated Dockerfile build contexts.

Docker Compose deployments can build images from Dockerfiles that are generated by your AppHost. Use AddDockerfileBuilder to create a new container resource from a generated Dockerfile, or WithDockerfileBuilder to replace the image for an existing container resource with a generated Dockerfile build.

apphost.mts
import { createBuilder } from './.aspire/modules/aspire.mjs';
import type { DockerfileBuilderCallbackContext } from './.aspire/modules/aspire.mjs';
const builder = await createBuilder();
await builder.addDockerComposeEnvironment('env');
const configureDockerfile = async (context: DockerfileBuilderCallbackContext) => {
const dockerfile = await context.builder();
await dockerfile
.from('node:22-alpine', { stageName: 'build' })
.workDir('/app')
.copy('package*.json', './')
.run('npm ci')
.copy('.', '.')
.run('npm run build');
await dockerfile
.from('nginx:alpine', { stageName: 'runtime' })
.copyFrom('build', '/app/dist', '/usr/share/nginx/html')
.expose(80);
};
await builder.addDockerfileBuilder(
'frontend',
'../frontend',
configureDockerfile,
{ stage: 'runtime' }
);
await builder.build().run();

The generated Dockerfile is included with the Docker Compose output and is used when aspire do prepare-{resource-name} or aspire deploy builds container images.

The Docker hosting integration captures environment variables from your app model and includes them in a .env file. This ensures that all configuration is properly passed to the containerized services.

For advanced scenarios, use ConfigureEnvFile to customize the generated .env file:

apphost.mts
import { createBuilder } from './.aspire/modules/aspire.mjs';
const builder = await createBuilder();
const api = await builder.addContainer('api', 'nginx:alpine');
await api.withBindMount('/host/path/data', '/container/data');
const compose = await builder.addDockerComposeEnvironment('env');
await compose.configureEnvFile(async (envVars) => {
const bindMount = await envVars.get('API_BINDMOUNT_0');
await bindMount.description.set('Customized bind mount source');
await bindMount.defaultValue.set('./data');
});
await builder.build().run();

This is useful when you need to add custom environment variables to the generated .env file or modify how environment variables are captured.

Use ConfigureComposeFile to customize the generated docker-compose.yml model before Aspire writes it to disk:

apphost.mts
import { createBuilder } from './.aspire/modules/aspire.mjs';
const builder = await createBuilder();
const compose = await builder.addDockerComposeEnvironment('env');
await compose.configureComposeFile(async (composeFile) => {
await composeFile.name.set('my-app');
const api = await composeFile.services.get('api');
await api.pullPolicy.set('always');
});
await builder.addProject('api', '../Api/Api.csproj', 'http');
await builder.build().run();

To customize the generated Docker Compose service for a specific resource, use the PublishAsDockerComposeService method. This is optional — all resources are automatically included in the Docker Compose output. Use this method only when you need to modify the generated service definition:

apphost.mts
import { createBuilder } from './.aspire/modules/aspire.mjs';
const builder = await createBuilder();
await builder.addDockerComposeEnvironment('env');
const containerName = await builder.addParameter('container-name');
const cache = await builder.addContainer('cache', 'redis:latest');
await cache.publishAsDockerComposeService(async (composeService, service) => {
await service.containerName.set(
await containerName.asEnvironmentPlaceholder(composeService)
);
await service.labels.set('com.example.team', 'backend');
await service.restart.set('unless-stopped');
});
await builder.build().run();

The configure callback receives the DockerComposeServiceResource and the generated Service object, allowing you to modify properties like labels, restart policy, container name, or other Docker Compose service settings. Use AsEnvironmentPlaceholder for values that should be written as Compose environment variable placeholders in the generated YAML and .env files.

Use GetHostAddressExpression when you need the host name that another Docker Compose service should use for an endpoint. In Docker Compose deployments, this expression resolves to the generated service name on the Compose network.

apphost.mts
import { createBuilder } from './.aspire/modules/aspire.mjs';
const builder = await createBuilder();
const compose = await builder.addDockerComposeEnvironment('env');
const api = await builder.addContainer('api', 'nginx:alpine');
await api.withHttpEndpoint({ name: 'http', targetPort: 80 });
const apiEndpoint = await api.getEndpoint('http');
const apiHost = await compose.getHostAddressExpression(apiEndpoint);

Container resources support an ImagePullPolicy that controls when the container runtime pulls an image. Use the WithImagePullPolicy extension method to set the policy on a container resource:

apphost.mts
import {
function createBuilder(): IDistributedApplicationBuilder

Creates a new distributed application builder

createBuilder
,
type ImagePullPolicy = "Default" | "Always" | "Missing" | "Never"
const ImagePullPolicy: {
readonly Default: "Default";
readonly Always: "Always";
readonly Missing: "Missing";
readonly Never: "Never";
}

Enum Aspire.Hosting.ApplicationModel.ImagePullPolicy

ImagePullPolicy
} from './.aspire/modules/aspire.mjs';
const
const builder: IDistributedApplicationBuilder
builder
= await
function createBuilder(): IDistributedApplicationBuilder

Creates a new distributed application builder

createBuilder
();
const
const container: ContainerResource
container
= await
const builder: IDistributedApplicationBuilder
builder
.
IDistributedApplicationBuilder.addContainer(name: string, image: AddContainerOptions): ContainerResource

Adds a container resource to the application.

addContainer
('mycontainer', {
AddContainerOptions.image?: string | undefined
image
: 'myimage',
AddContainerOptions.tag?: string | undefined
tag
: 'latest' });
await
const container: ContainerResource
container
.
ContainerResource.withImagePullPolicy(pullPolicy: ImagePullPolicy): ContainerResource

Sets the pull policy for the container resource.

withImagePullPolicy
(
const ImagePullPolicy: {
readonly Default: "Default";
readonly Always: "Always";
readonly Missing: "Missing";
readonly Never: "Never";
}

Enum Aspire.Hosting.ApplicationModel.ImagePullPolicy

ImagePullPolicy
.
type Always: "Always"
Always
);

When you publish resources to a Docker Compose environment, the ImagePullPolicy is automatically mapped to the Docker Compose pull_policy field:

ImagePullPolicyDocker Compose pull_policy
Alwaysalways
Missingmissing
Nevernever
Default(omitted — uses runtime default)

When deploying containers, you can customize how container images are named, tagged, and pushed to a registry.

Use WithRemoteImageName and WithRemoteImageTag to customize the image reference:

apphost.mts
import {
function createBuilder(): IDistributedApplicationBuilder

Creates a new distributed application builder

createBuilder
} from './.aspire/modules/aspire.mjs';
const
const builder: IDistributedApplicationBuilder
builder
= await
function createBuilder(): IDistributedApplicationBuilder

Creates a new distributed application builder

createBuilder
();
const
const api: ProjectResource
api
= await
const builder: IDistributedApplicationBuilder
builder
.
IDistributedApplicationBuilder.addProject(name: string, projectPath: string, options?: {
launchProfileOrOptions?: ProjectResourceOptions;
}): ProjectResource (+1 overload)

Adds a .NET project resource

addProject
('api', '../Api/Api.csproj');
await
const api: ProjectResource
api
.
ProjectResource.publishAsDockerComposeService(configure: (arg1: DockerComposeServiceResource, arg2: Service) => Promise<void>): ProjectResource

Publishes the specified resource as a Docker Compose service.

publishAsDockerComposeService
(async (
resource: DockerComposeServiceResource
resource
,
service: Service
service
) => {
await
service: Service
service
.
Service.name: PropertyAccessor<string>

Gets or sets the name of the Docker Compose member.

name
.
function set(value: string): Promise<void> (+1 overload)
set
('api');
});
await
const api: ProjectResource
api
.
ProjectResource.withRemoteImageName(remoteImageName: string): ProjectResource

Sets the remote image name (without registry endpoint or tag) for container push operations.

withRemoteImageName
('myorg/myapi');
await
const api: ProjectResource
api
.
ProjectResource.withRemoteImageTag(remoteImageTag: string): ProjectResource

Sets the remote image tag for container push operations.

withRemoteImageTag
('v1.0.0');

For more complex scenarios, use WithImagePushOptions to register a callback that dynamically configures push options:

apphost.mts
import { createBuilder } from './.aspire/modules/aspire.mjs';
const builder = await createBuilder();
const api = await builder.addProject('api', '../Api/Api.csproj');
await api
.publishAsDockerComposeService(async (resource, service) => {
await service.name.set('api');
})
.withImagePushOptions(async (context) => {
const imageName = context.resource.getResourceName().toLowerCase();
context.options.setRemoteImageName(`myorg/${imageName}`);
context.options.setRemoteImageTag('latest');
});

Multiple callbacks can be registered on the same resource, and they are invoked in the order they were added.

Use the AddContainerRegistry method to define a container registry and WithContainerRegistry to associate resources with it:

apphost.mts
import {
function createBuilder(): IDistributedApplicationBuilder

Creates a new distributed application builder

createBuilder
} from './.aspire/modules/aspire.mjs';
const
const builder: IDistributedApplicationBuilder
builder
= await
function createBuilder(): IDistributedApplicationBuilder

Creates a new distributed application builder

createBuilder
();
const
const registry: ContainerRegistryResource
registry
= await
const builder: IDistributedApplicationBuilder
builder
.
IDistributedApplicationBuilder.addContainerRegistry(name: string, endpoint: string | ParameterResource, options?: {
repository?: string | ParameterResource;
}): ContainerRegistryResource (+1 overload)

Adds a container registry resource

addContainerRegistry
(
'ghcr',
'ghcr.io',
{
repository?: string | ParameterResource | undefined
repository
: 'your-github-username/your-repo' }
);
const
const api: ProjectResource
api
= await
const builder: IDistributedApplicationBuilder
builder
.
IDistributedApplicationBuilder.addProject(name: string, projectPath: string, options?: {
launchProfileOrOptions?: ProjectResourceOptions;
}): ProjectResource (+1 overload)

Adds a .NET project resource

addProject
('api', '../Api/Api.csproj');
await
const api: ProjectResource
api
.
ProjectResource.publishAsDockerComposeService(configure: (arg1: DockerComposeServiceResource, arg2: Service) => Promise<void>): ProjectResource

Publishes the specified resource as a Docker Compose service.

publishAsDockerComposeService
(async (
resource: DockerComposeServiceResource
resource
,
service: Service
service
) => {
await
service: Service
service
.
Service.name: PropertyAccessor<string>

Gets or sets the name of the Docker Compose member.

name
.
function set(value: string): Promise<void> (+1 overload)
set
('api');
});
await
const api: ProjectResource
api
.
ProjectResource.withContainerRegistry(registry: IResource): ProjectResource

Configures the resource to use the specified container registry for container image operations.

withContainerRegistry
(
const registry: ContainerRegistryResource
registry
);

For more flexible configuration in CI/CD pipelines, use parameters with environment variables:

apphost.mts
import {
function createBuilder(): IDistributedApplicationBuilder

Creates a new distributed application builder

createBuilder
} from './.aspire/modules/aspire.mjs';
const
const builder: IDistributedApplicationBuilder
builder
= await
function createBuilder(): IDistributedApplicationBuilder

Creates a new distributed application builder

createBuilder
();
const
const registryEndpoint: ParameterResource
registryEndpoint
=
const builder: IDistributedApplicationBuilder
builder
.
IDistributedApplicationBuilder.addParameterFromConfiguration(name: string, configurationKey: string, options?: {
secret?: boolean;
}): ParameterResource (+1 overload)

Adds a parameter resource to the application, with a value coming from configuration.

addParameterFromConfiguration
(
'registryEndpoint',
'REGISTRY_ENDPOINT'
);
const
const registryRepository: ParameterResource
registryRepository
=
const builder: IDistributedApplicationBuilder
builder
.
IDistributedApplicationBuilder.addParameterFromConfiguration(name: string, configurationKey: string, options?: {
secret?: boolean;
}): ParameterResource (+1 overload)

Adds a parameter resource to the application, with a value coming from configuration.

addParameterFromConfiguration
(
'registryRepository',
'REGISTRY_REPOSITORY'
);
const
const registry: ContainerRegistryResource
registry
= await
const builder: IDistributedApplicationBuilder
builder
.
IDistributedApplicationBuilder.addContainerRegistry(name: string, endpoint: string | ParameterResource, repository?: string | ParameterResource): ContainerRegistryResource (+1 overload)

Adds a container registry resource

addContainerRegistry
(
'my-registry',
const registryEndpoint: ParameterResource
registryEndpoint
,
const registryRepository: ParameterResource
registryRepository
);
const
const api: ProjectResource
api
= await
const builder: IDistributedApplicationBuilder
builder
.
IDistributedApplicationBuilder.addProject(name: string, projectPath: string, options?: {
launchProfileOrOptions?: ProjectResourceOptions;
}): ProjectResource (+1 overload)

Adds a .NET project resource

addProject
('api', '../Api/Api.csproj');
await
const api: ProjectResource
api
.
ProjectResource.publishAsDockerComposeService(configure: (arg1: DockerComposeServiceResource, arg2: Service) => Promise<void>): ProjectResource

Publishes the specified resource as a Docker Compose service.

publishAsDockerComposeService
(async (
resource: DockerComposeServiceResource
resource
,
service: Service
service
) => {
await
service: Service
service
.
Service.name: PropertyAccessor<string>

Gets or sets the name of the Docker Compose member.

name
.
function set(value: string): Promise<void> (+1 overload)
set
('api');
});
await
const api: ProjectResource
api
.
ProjectResource.withContainerRegistry(registry: IResource): ProjectResource

Configures the resource to use the specified container registry for container image operations.

withContainerRegistry
(
const registry: ContainerRegistryResource
registry
);

After configuring your container registry, use the aspire do push command to build and push your container images:

Terminal
aspire do push

This command builds container images for all resources configured with a container registry, tags them with the appropriate registry path, and pushes them to the specified registry.

Before running this command, ensure you are authenticated to your container registry. For example, with GitHub Container Registry:

Terminal
echo $GITHUB_TOKEN | docker login ghcr.io -u your-github-username --password-stdin

The following GitHub Actions workflow builds and pushes container images to GitHub Container Registry (GHCR):

.github/workflows/build-and-push.yml
name: Build and Push Images
on:
push:
branches: [main]
jobs:
build-and-push:
runs-on: ubuntu-latest
permissions:
contents: read
packages: write
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 -sSL https://aspire.dev/install.sh | bash
echo "$HOME/.aspire/bin" >> $GITHUB_PATH
- name: Output Aspire CLI version
run: aspire --version
- name: Log in to GitHub Container Registry
uses: docker/login-action@v3
with:
registry: ghcr.io
username: ${{ github.actor }}
password: ${{ secrets.GITHUB_TOKEN }}
- name: Build and push images
env:
REGISTRY_ENDPOINT: ghcr.io
REGISTRY_REPOSITORY: ${{ github.repository }}
run: aspire do push

This workflow checks out your code, sets up .NET and installs the Aspire CLI, authenticates to GHCR using the built-in GITHUB_TOKEN, and builds and pushes container images using aspire do push.