Skip to content

Conversation

Copy link
Contributor

Copilot AI commented Oct 31, 2025

Python and Node.js projects now auto-generate Dockerfiles, but developers couldn't override the default base images. This adds support for specifying custom build and runtime images.

Changes

  • Added DockerfileBaseImageAnnotation - stores optional BuildImage and RuntimeImage properties
  • Added WithDockerfileBaseImage<T>() extension method - fluent API to set custom images on any IResource
  • Updated Python Dockerfile generation - checks annotation before falling back to default ghcr.io/astral-sh/uv and python images
  • Updated Node.js Dockerfile generation - checks annotation before falling back to default node image

Usage

Multi-stage builds (Python with UV):

builder.AddPythonApp("app", ".", "main.py")
    .WithUvEnvironment()
    .WithDockerfileBaseImage(
        buildImage: "ghcr.io/astral-sh/uv:python3.13-bookworm",
        runtimeImage: "python:3.13-alpine");

Single-stage builds (Node.js):

builder.AddViteApp("vite", ".")
    .WithNpm()
    .WithDockerfileBaseImage(runtimeImage: "node:22-alpine");

Marked experimental with ASPIREDOCKERFILEBUILDER001 diagnostic. Fully backward compatible - defaults unchanged when annotation absent.

Warning

Firewall rules blocked me from connecting to one or more addresses (expand for details)

I tried to connect to the following addresses, but was blocked by firewall rules:

  • centralus.data.mcr.microsoft.com
    • Triggering command: dotnet publish /home/REDACTED/work/aspire/aspire/tests/testproject/TestProject.ServiceA/TestProject.ServiceA.csproj --configuration Release /t:PublishContainer /p:ContainerRepository=servicea (dns block)
    • Triggering command: dotnet publish /home/REDACTED/work/aspire/aspire/tests/testproject/TestProject.ServiceA/TestProject.ServiceA.csproj --configuration Release /t:PublishContainer /p:ContainerRepository=servicea /p:ContainerArchiveOutputPath=/tmp/test-output /p:ContainerImageFormat=OCI /p:ContainerRuntimeIdentifier=linux-x64 (dns block)
    • Triggering command: buildkitd --allow-insecure-entitlement=network.host (dns block)

If you need me to access, download, or install something from one of these locations, you can either:

Original prompt

This section details on the original issue you should resolve

<issue_title>Add an Annotation to set the generated dockerfile base image</issue_title>
<issue_description>In python and javascript we generate dockerfiles by default now. We should have an annotation that allows developers to specify which base image they want. The annotation should have properties for both build base image and runtime base image so someone can specify one separately.

c.WithDockerfileBuilder(resource.WorkingDirectory,
context =>
{
if (!c.Resource.TryGetLastAnnotation<PythonEnvironmentAnnotation>(out var pythonEnvironmentAnnotation) ||
!pythonEnvironmentAnnotation.Uv)
{
// Use the default Dockerfile if not using UV
return;
}
if (!context.Resource.TryGetLastAnnotation<PythonEntrypointAnnotation>(out var entrypointAnnotation))
{
// No entrypoint annotation found, cannot generate Dockerfile
return;
}
var pythonVersion = pythonEnvironmentAnnotation.Version ?? PythonVersionDetector.DetectVersion(appDirectory, pythonEnvironmentAnnotation.VirtualEnvironment!);
if (pythonVersion is null)
{
// Could not detect Python version, skip Dockerfile generation
return;
}
var entrypointType = entrypointAnnotation.Type;
var entrypoint = entrypointAnnotation.Entrypoint;
// Determine entry command for Dockerfile
string[] entryCommand = entrypointType switch
{
EntrypointType.Script => ["python", entrypoint],
EntrypointType.Module => ["python", "-m", entrypoint],
EntrypointType.Executable => [entrypoint],
_ => throw new InvalidOperationException($"Unsupported entrypoint type: {entrypointType}")
};
// Check if uv.lock exists in the working directory
var uvLockPath = Path.Combine(resource.WorkingDirectory, "uv.lock");
var hasUvLock = File.Exists(uvLockPath);
var builderStage = context.Builder
.From($"ghcr.io/astral-sh/uv:python{pythonVersion}-bookworm-slim", "builder")
.EmptyLine()
.Comment("Enable bytecode compilation and copy mode for the virtual environment")
.Env("UV_COMPILE_BYTECODE", "1")
.Env("UV_LINK_MODE", "copy")
.EmptyLine()
.WorkDir("/app")
.EmptyLine();
if (hasUvLock)
{
// If uv.lock exists, use locked mode for reproducible builds
builderStage
.Comment("Install dependencies first for better layer caching")
.Comment("Uses BuildKit cache mounts to speed up repeated builds")
.RunWithMounts(
"uv sync --locked --no-install-project --no-dev",
"type=cache,target=/root/.cache/uv",
"type=bind,source=uv.lock,target=uv.lock",
"type=bind,source=pyproject.toml,target=pyproject.toml")
.EmptyLine()
.Comment("Copy the rest of the application source and install the project")
.Copy(".", "/app")
.RunWithMounts(
"uv sync --locked --no-dev",
"type=cache,target=/root/.cache/uv");
}
else
{
// If uv.lock doesn't exist, copy pyproject.toml and generate lock file
builderStage
.Comment("Copy pyproject.toml to install dependencies")
.Copy("pyproject.toml", "/app/")
.EmptyLine()
.Comment("Install dependencies and generate lock file")
.Comment("Uses BuildKit cache mount to speed up repeated builds")
.RunWithMounts(
"uv sync --no-install-project --no-dev",
"type=cache,target=/root/.cache/uv")
.EmptyLine()
.Comment("Copy the rest of the application source and install the project")
.Copy(".", "/app")
.RunWithMounts(
"uv sync --no-dev",
"type=cache,target=/root/.cache/uv");
}
var runtimeBuilder = context.Builder
.From($"python:{pythonVersion}-slim-bookworm", "app")
.EmptyLine()
.AddContainerFiles(context.Resource, "/app")
.Comment("------------------------------")
.Comment("🚀 Runtime stage")
.Comment("------------------------------")
.Comment("Create non-root user for security")
.Run("groupadd --system --gid 999 appuser && useradd --system --gid 999 --uid 999 --create-home appuser")
.EmptyLine()
.Comment("Copy the application and virtual environment from builder")
.CopyFrom(builderStage.StageName!, "/app", "/app", "appuser:appuser")
.EmptyLine()
.Comment("Add virtual environment to PATH and set VIRTUAL_ENV")
.Env("PATH", "/app/.venv/bin:${PATH}")
.Env("VIRTUAL_ENV", "/app/.venv")
.Env("PYTHONDONTWRITEBYTECODE", "1")
.Env("PYTHONUNBUFFERED", "1")
.EmptyLine()
.Comment("Use the non-root user to run the application")
.User("appuser")
.EmptyLine()
.Comment("Set working directory")
.WorkDir("/app")
.EmptyLine()
.Comment("Run the application");
// Set the appropriate entrypoint and command based on entrypoint type
switch (entrypointType)
{
case EntrypointType.Script:
runtimeBuilder.Entrypoint(["python", entrypoint]);
break;
case EntrypointType.Module:
runtimeBuilder.Entrypoint(["python", "-m", entrypoint]);
break;
case EntrypointType.Executable:
runtimeBuilder.Entrypoint([entrypoint]);
break;
}
});
});

c.WithDockerfileBuilder(appDirectory, dockerfileContext =>
{
if (c.Resource.TryGetLastAnnotation<JavaScriptPackageManagerAnnotation>(out var packageManager))
{
var logger = dockerfileContext.Services.GetService<ILogger<ViteAppResource>>() ?? NullLogger<ViteAppResource>.Instance;
var nodeVersion = DetectNodeVersion(appDirectory, logger) ?? DefaultNodeVersion;
var dockerBuilder = dockerfileContext.Builder
.From($"node:{nodeVersion}-slim")
.WorkDir("/app")
.Copy(".", ".");
if (c.Resource.TryGetLastAnnotation<JavaScriptInstallCommandAnnotation>(out var installCommand))
{
dockerBuilder.Run($"{packageManager.ExecutableName} {string.Join(' ', installCommand.Args)}");
}
if (c.Resource.TryGetLastAnnotation<JavaScriptBuildScriptAnnotation>(out var buildCommand))
{
var command = packageManager.ExecutableName;
if (!string.IsNullOrEmpty(packageManager.ScriptCommand))
{
command += $" {packageManager.ScriptCommand}";
}
var args = string.Join(' ', buildCommand.Args);
if (args.Length > 0)
{
args = " " + args;
}
dockerBuilder.Run($"{command} {buildCommand.ScriptName}{args}");
}
}
});
// Javascript apps don't have an entrypoint
if (resource.TryGetLastAnnotation<DockerfileBuildAnnotation>(out var dockerFileAnnotation))
{
dockerFileAnnotation.HasEntrypoint = false;
}
else
{
throw new InvalidOperationException("DockerfileBuildAnnotation should exist after calling PublishAsDockerFile.");
}
})

</issue_description>

Comments on the Issue (you are @copilot in this section)


✨ Let Copilot coding agent set things up for you — coding agent works faster and does higher quality work when set up for your repo.

Copilot AI changed the title [WIP] Add annotation for specifying dockerfile base image Add annotation to specify custom base images for generated Dockerfiles Oct 31, 2025
Copilot AI requested a review from eerhardt October 31, 2025 17:48
@eerhardt eerhardt force-pushed the copilot/add-dockerfile-base-image-annotation branch from 4683fe6 to 3d4b330 Compare October 31, 2025 20:20
@github-actions
Copy link
Contributor

github-actions bot commented Oct 31, 2025

🚀 Dogfood this PR with:

⚠️ WARNING: Do not do this without first carefully reviewing the code of this PR to satisfy yourself it is safe.

curl -fsSL https://raw.githubusercontent.com/dotnet/aspire/main/eng/scripts/get-aspire-cli-pr.sh | bash -s -- 12566

Or

  • Run remotely in PowerShell:
iex "& { $(irm https://raw.githubusercontent.com/dotnet/aspire/main/eng/scripts/get-aspire-cli-pr.ps1) } 12566"

@eerhardt eerhardt marked this pull request as ready for review October 31, 2025 20:20
@eerhardt eerhardt requested review from Copilot and davidfowl October 31, 2025 20:20
@eerhardt
Copy link
Member

@copilot also respect this annotation in Yarp in this code:

return builder.WithDockerfileBuilder(".", ctx =>
{
var logger = ctx.Services.GetRequiredService<ILogger<YarpResource>>();
var imageName = GetYarpImageName(ctx.Resource);
var stage = ctx.Builder.From(imageName).WorkDir("/app");
if (ctx.Resource.TryGetAnnotationsOfType<ContainerFilesDestinationAnnotation>(out var containerFilesDestinationAnnotations))
{
foreach (var containerFileDestination in containerFilesDestinationAnnotations)
{
var source = containerFileDestination.Source;
if (!source.TryGetContainerImageName(out var sourceImageName))
{
logger.LogWarning("Cannot get container image name for source resource {SourceName}, skipping", source.Name);
return;
}
foreach (var containerFilesSource in source.Annotations.OfType<ContainerFilesSourceAnnotation>())
{
stage.CopyFrom(sourceImageName, containerFilesSource.SourcePath, "/app/wwwroot");
}
}
}
});

Copy link
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull Request Overview

This PR introduces an experimental API for customizing Docker base images in generated Dockerfiles. The feature allows developers to override default base images for Python (with UV), Node.js, and Vite applications during containerization.

Key changes:

  • Adds DockerfileBaseImageAnnotation to store custom build and runtime image specifications
  • Implements WithDockerfileBaseImage() extension method marked as experimental API
  • Updates Python and Node.js Dockerfile generators to consume the annotation when available

Reviewed Changes

Copilot reviewed 7 out of 7 changed files in this pull request and generated 3 comments.

Show a summary per file
File Description
src/Aspire.Hosting/ApplicationModel/DockerfileBaseImageAnnotation.cs New annotation class for storing custom build and runtime base images
src/Aspire.Hosting/ContainerResourceBuilderExtensions.cs New experimental WithDockerfileBaseImage() extension method with validation
src/Aspire.Hosting.Python/PythonAppResourceBuilderExtensions.cs Updated UV Dockerfile generator to use custom base images from annotation
src/Aspire.Hosting.NodeJs/NodeExtensions.cs Updated Node.js and Vite Dockerfile generators to use custom base images from annotation
tests/Aspire.Hosting.Python.Tests/AddPythonAppTests.cs Test verifying custom base images in Python UV Dockerfiles
tests/Aspire.Hosting.NodeJs.Tests/AddNodeAppTests.cs Test verifying custom base images in Node.js multi-stage Dockerfiles
tests/Aspire.Hosting.NodeJs.Tests/AddViteAppTests.cs Test verifying custom base images in Vite single-stage Dockerfiles

@eerhardt
Copy link
Member

also respect this annotation in Yarp in this code:

@copilot - scratch that. It isn't necessary. Yarp is already a ContainerResource that has a ContainerImageAnnotation. So the base image is already able to be specified that way.

@eerhardt eerhardt requested a review from mitchdenny as a code owner October 31, 2025 21:18
@eerhardt eerhardt merged commit a352eff into main Oct 31, 2025
296 checks passed
@eerhardt eerhardt deleted the copilot/add-dockerfile-base-image-annotation branch October 31, 2025 22:09
@eerhardt
Copy link
Member

/backport to release/13.0

@dotnet-policy-service dotnet-policy-service bot added this to the 13.1 milestone Oct 31, 2025
@github-actions
Copy link
Contributor

Started backporting to release/13.0: https://github.com/dotnet/aspire/actions/runs/18986383052

Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Add an Annotation to set the generated dockerfile base image

3 participants