-
Notifications
You must be signed in to change notification settings - Fork 816
Closed
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.
aspire/src/Aspire.Hosting.Python/PythonAppResourceBuilderExtensions.cs
Lines 432 to 561 in ad6efe8
| 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; | |
| } | |
| }); | |
| }); |
aspire/src/Aspire.Hosting.NodeJs/NodeExtensions.cs
Lines 161 to 203 in ad6efe8
| 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."); | |
| } | |
| }) |
Reactions are currently unavailable
Metadata
Metadata
Assignees
Labels
No labels