# 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.
**Note:** This page covers the **deployment workflow** for Docker Compose. For hosting
  integration setup, resource configuration, and local development details, see
  the [Docker integration](/integrations/compute/docker/) page.

## Container runtime support

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.

### Auto-detection

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.
**Tip:** To see which runtime Aspire detected and why, run `aspire doctor`. It reports
  all available runtimes with their status, version, and which one is active.

### Override the active runtime

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:

```bash title="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.

### Podman-specific behavior

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.
**Caution:** Aspire requires **Podman 5.0.0 or later**. Older Podman versions (such as the
4.9.x series shipped with Ubuntu 24.04) are reported by `aspire doctor` with a
  warning that the client version is below the minimum, and they are not used
  even when Podman is the only installed runtime. On distributions that don't
  package Podman 5+ natively, install it from a vendor channel (for example,
  Fedora 42+ ships Podman 5 in the default repositories) before deploying.

## Prerequisites

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

```csharp title="AppHost.cs" {3}
var builder = DistributedApplication.CreateBuilder(args);

builder.AddDockerComposeEnvironment("env");

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

// After adding all resources, run the app...
builder.Build().Run();
```
```typescript title="apphost.mts" {5} twoslash
import { createBuilder } from './.aspire/modules/aspire.mjs';

const builder = await createBuilder();

await builder.addDockerComposeEnvironment('env');

await builder.addProject('api', '../Api/Api.csproj');

await builder.build().run();
```
When a Docker Compose environment is present, all resources are automatically published as Docker Compose services — no additional opt-in is required.

<LearnMore>
  For more information on the hosting integration and resource configuration,
  see [Docker integration](/integrations/compute/docker/).
</LearnMore>

## Publishing and deployment workflow

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:

   ```bash title="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:

   ```bash title="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:

   ```bash title="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

### Clean up deployment

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

```bash title="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.
**Caution:** `aspire destroy` permanently removes the deployed containers, networks, and
  volumes. This action cannot be undone. Pass `--yes` only in automated or CI
  scenarios where you've already validated what will be removed.

## Multi-environment deployments

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`.

### Deploy to staging and production

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

```bash title="Deploy to staging"
aspire deploy --environment staging
```

```bash title="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](/deployment/deployment-state-caching/) per environment.

### Publish artifacts for multiple environments

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

```bash title="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.

<LearnMore>
  For more on how environments work across all deployment targets, see the
  [Environments](/deployment/environments/) guide. For command details on
  `prepare-*` steps, see the [CLI reference: `aspire
  do`](/reference/cli/commands/aspire-do/).
</LearnMore>

## Output artifacts

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

| Artifact                    | Description                                                                                         |
| --------------------------- | --------------------------------------------------------------------------------------------------- |
| `docker-compose.yaml`       | The generated Compose file defining all services, networks, and volumes.                            |
| `.env`                      | Environment 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. |

## Generate Dockerfiles from your AppHost

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.
**Caution:** The Dockerfile builder APIs are experimental and may change in future
  releases. In C# AppHosts, suppress diagnostic
  [`ASPIREDOCKERFILEBUILDER001`](/diagnostics/aspiredockerfilebuilder001/) when you choose to use them.

```csharp title="AppHost.cs"
using Aspire.Hosting.ApplicationModel.Docker;

var builder = DistributedApplication.CreateBuilder(args);

builder.AddDockerComposeEnvironment("env");

#pragma warning disable ASPIREDOCKERFILEBUILDER001
builder.AddDockerfileBuilder("frontend", "../frontend", context =>
{
    var build = context.Builder.From("node:22-alpine", "build");
    build.WorkDir("/app")
        .Copy("package*.json", "./")
        .Run("npm ci")
        .Copy(".", ".")
        .Run("npm run build");

    var runtime = context.Builder.From("nginx:alpine", "runtime");
    runtime.CopyFrom("build", "/app/dist", "/usr/share/nginx/html")
        .Expose(80);

    return Task.CompletedTask;
}, stage: "runtime");
#pragma warning restore ASPIREDOCKERFILEBUILDER001

builder.Build().Run();
```
```typescript title="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.

## Environment variables

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.

### Customize environment file

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

```csharp title="AppHost.cs"
using Aspire.Hosting.Docker;

var builder = DistributedApplication.CreateBuilder(args);

builder.AddDockerComposeEnvironment("env")
    .ConfigureEnvFile(env =>
    {
        env["CUSTOM_VAR"] = new CapturedEnvironmentVariable
        {
            Name = "CUSTOM_VAR",
            DefaultValue = "my-value"
        };
    });
```
```typescript title="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.

## Customize the Docker Compose file

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

```csharp title="AppHost.cs"
var builder = DistributedApplication.CreateBuilder(args);

builder.AddDockerComposeEnvironment("env")
    .ConfigureComposeFile(composeFile =>
    {
        composeFile.Name = "my-app";

        var api = composeFile.Services["api"];
        api.PullPolicy = "always";
    });

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

builder.Build().Run();
```
```typescript title="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();
```
## Customize Docker Compose services

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:

```csharp title="AppHost.cs"
var builder = DistributedApplication.CreateBuilder(args);

builder.AddDockerComposeEnvironment("env");

var containerName = builder.AddParameter("container-name");

builder.AddContainer("cache", "redis", "latest")
    .PublishAsDockerComposeService((resource, service) =>
    {
        // Customize the generated Docker Compose service
        service.ContainerName =
            containerName.AsEnvironmentPlaceholder(resource);
        service.Labels.Add("com.example.team", "backend");
        service.Restart = "unless-stopped";
    });

builder.Build().Run();
```
```typescript title="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.

## Resolve Docker Compose service host names

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.

```csharp title="AppHost.cs"
var builder = DistributedApplication.CreateBuilder(args);

var compose = builder.AddDockerComposeEnvironment("env");

var api = builder.AddContainer("api", "nginx", "alpine")
    .WithHttpEndpoint(name: "http", targetPort: 80);

var apiHost = compose.Resource.GetHostAddressExpression(
    api.GetEndpoint("http"));
```
```typescript title="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);
```
## Image pull policy

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:

```csharp title="AppHost.cs"
var builder = DistributedApplication.CreateBuilder(args);

builder.AddContainer("mycontainer", "myimage:latest")
    .WithImagePullPolicy(ImagePullPolicy.Always);
```
```typescript title="apphost.mts" twoslash
import { createBuilder, ImagePullPolicy } from './.aspire/modules/aspire.mjs';

const builder = await createBuilder();

const container = await builder.addContainer('mycontainer', { image: 'myimage', tag: 'latest' });
await container.withImagePullPolicy(ImagePullPolicy.Always);
```
When you publish resources to a Docker Compose environment, the `ImagePullPolicy` is automatically mapped to the Docker Compose [`pull_policy`](https://docs.docker.com/reference/compose-file/services/#pull_policy) field:

| `ImagePullPolicy` | Docker Compose `pull_policy`       |
| ----------------- | ---------------------------------- |
| `Always`          | `always`                           |
| `Missing`         | `missing`                          |
| `Never`           | `never`                            |
| `Default`         | _(omitted — uses runtime default)_ |
**Tip:** The `Never` policy is useful when working with locally-built images that
  shouldn't be pulled from a registry. If the image isn't available locally, the
  container fails to start.

## Container image push

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

### Set remote image name and tag

Use `WithRemoteImageName` and `WithRemoteImageTag` to customize the image reference:

```csharp title="AppHost.cs"
var builder = DistributedApplication.CreateBuilder(args);

#pragma warning disable ASPIREPIPELINES003
var api = builder.AddProject<Projects.Api>("api")
    .PublishAsDockerComposeService(
        (resource, service) => { service.Name = "api"; })
    .WithRemoteImageName("myorg/myapi")
    .WithRemoteImageTag("v1.0.0");
#pragma warning restore ASPIREPIPELINES003

// After adding all resources, run the app...
```
```typescript title="apphost.mts" twoslash
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');
});
await api.withRemoteImageName('myorg/myapi');
await api.withRemoteImageTag('v1.0.0');
```
### Advanced customization with callbacks

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

```csharp title="AppHost.cs"
var builder = DistributedApplication.CreateBuilder(args);

#pragma warning disable ASPIREPIPELINES003
var api = builder.AddProject<Projects.Api>("api")
    .PublishAsDockerComposeService(
        (resource, service) => { service.Name = "api"; })
    .WithImagePushOptions(context =>
    {
        var imageName = context.Resource.Name.ToLowerInvariant();
        context.Options.RemoteImageName = $"myorg/{imageName}";

        var version =
            Environment.GetEnvironmentVariable("APP_VERSION")
            ?? "latest";
        context.Options.RemoteImageTag = version;
    });
#pragma warning restore ASPIREPIPELINES003

// After adding all resources, run the app...
```
```typescript title="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.

### Configure a container registry

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

```csharp title="AppHost.cs"
var builder = DistributedApplication.CreateBuilder(args);

#pragma warning disable ASPIRECOMPUTE003
// Add a container registry
var registry = builder.AddContainerRegistry(
    "ghcr",                              // Registry name
    "ghcr.io",                           // Registry endpoint
    "your-github-username/your-repo"     // Repository path
);

// Associate resources with the registry
var api = builder.AddProject<Projects.Api>("api")
    .PublishAsDockerComposeService(
        (resource, service) => { service.Name = "api"; })
    .WithContainerRegistry(registry);

// After adding all resources, run the app...
```
```typescript title="apphost.mts" twoslash
import { createBuilder } from './.aspire/modules/aspire.mjs';

const builder = await createBuilder();

const registry = await builder.addContainerRegistry(
  'ghcr',
  'ghcr.io',
  { repository: 'your-github-username/your-repo' }
);

const api = await builder.addProject('api', '../Api/Api.csproj');
await api.publishAsDockerComposeService(async (resource, service) => {
  await service.name.set('api');
});
await api.withContainerRegistry(registry);
```
**Caution:** The `AddContainerRegistry` API is experimental and may change in future
  releases. Use diagnostic code `ASPIRECOMPUTE003` to suppress the experimental
  warning. For more information, see
  [ASPIRECOMPUTE003](/diagnostics/aspirecompute003/).

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

```csharp title="AppHost.cs"
var builder = DistributedApplication.CreateBuilder(args);

var registryEndpoint = builder.AddParameterFromConfiguration(
    "registryEndpoint", "REGISTRY_ENDPOINT");
var registryRepository = builder.AddParameterFromConfiguration(
    "registryRepository", "REGISTRY_REPOSITORY");

#pragma warning disable ASPIRECOMPUTE003
var registry = builder.AddContainerRegistry(
    "my-registry",
    registryEndpoint,
    registryRepository
);

var api = builder.AddProject<Projects.Api>("api")
    .PublishAsDockerComposeService(
        (resource, service) => { service.Name = "api"; })
    .WithContainerRegistry(registry);
#pragma warning restore ASPIRECOMPUTE003
```
```typescript title="apphost.mts" twoslash
import { createBuilder } from './.aspire/modules/aspire.mjs';

const builder = await createBuilder();

const registryEndpoint = builder.addParameterFromConfiguration(
  'registryEndpoint',
  'REGISTRY_ENDPOINT'
);
const registryRepository = builder.addParameterFromConfiguration(
  'registryRepository',
  'REGISTRY_REPOSITORY'
);

const registry = await builder.addContainerRegistry(
  'my-registry',
  registryEndpoint,
  registryRepository
);

const api = await builder.addProject('api', '../Api/Api.csproj');
await api.publishAsDockerComposeService(async (resource, service) => {
  await service.name.set('api');
});
await api.withContainerRegistry(registry);
```
### Push images with the Aspire CLI

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

```bash title="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:

```bash title="Terminal"
echo $GITHUB_TOKEN | docker login ghcr.io -u your-github-username --password-stdin
```

## GitHub Actions example

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

```yaml title=".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`.

## See also

- [Docker integration](/integrations/compute/docker/)
- [Pipelines and app topology](/deployment/pipelines/)
- [Deploy your first Aspire app](/get-started/deploy-first-app/)
- [aspire doctor command](/reference/cli/commands/aspire-doctor/)
- [`Aspire.Hosting.Docker` C# API reference](https://aspire.dev/reference/api/csharp/aspire.hosting.docker/)
- [`Aspire.Hosting.Docker` TypeScript API reference](https://aspire.dev/reference/api/typescript/aspire.hosting.docker/)
- [Working with GitHub Container Registry](https://docs.github.com/en/packages/working-with-a-github-packages-registry/working-with-the-container-registry)