From 2c08f11d4ec319878fb9a7f742e43c06d08d2a8c Mon Sep 17 00:00:00 2001 From: David Schmitt Date: Sun, 13 Apr 2025 17:20:31 +0200 Subject: [PATCH] Always cache apt metadata and downloads outside the image This change uses cache mounts in the generated Dockerfiles for features to ensure that apt metadata and downloaded packages remain available throughout the build process without ever getting baked into layers. To make life easier for feature implementations, this also runs `apt update` once before the feature build, so that the metadata cache is up-to-date. This has the following benefits: * reduced build-time costs: apt metadata is only downloaded once * smaller layer size: features cannot accidentally include downloaded packages in their layer (see https://github.com/devcontainers/features/pull/1298) * reduced complexity: features do not have to worry about apt caching and cache cleaning * optionally, if building on a caching buildkit service like depot.dev, the cache is re-used across builds, further reducing build-time --- .../containerFeaturesConfiguration.ts | 62 +++++++++++-------- 1 file changed, 37 insertions(+), 25 deletions(-) diff --git a/src/spec-configuration/containerFeaturesConfiguration.ts b/src/spec-configuration/containerFeaturesConfiguration.ts index 18c20309d..701ada3a6 100644 --- a/src/spec-configuration/containerFeaturesConfiguration.ts +++ b/src/spec-configuration/containerFeaturesConfiguration.ts @@ -211,6 +211,10 @@ FROM $_DEV_CONTAINERS_BASE_IMAGE AS dev_containers_target_stage USER root +RUN --mount=type=cache,target=/var/cache/apt \\ + --mount=type=cache,target=/var/lib/apt/lists \\ + apt update + RUN mkdir -p ${FEATURES_CONTAINER_TEMP_DEST_FOLDER} COPY --from=dev_containers_feature_content_normalize /tmp/build-features/ ${FEATURES_CONTAINER_TEMP_DEST_FOLDER} @@ -306,20 +310,24 @@ echo "_REMOTE_USER_HOME=$(${getEntPasswdShellCommand(remoteUser)} | cut -d: -f6) const dest = path.posix.join(FEATURES_CONTAINER_TEMP_DEST_FOLDER, folder!); if (!useBuildKitBuildContexts) { result += `COPY --chown=root:root --from=dev_containers_feature_content_source ${source} ${dest} -RUN chmod -R 0755 ${dest} \\ -&& cd ${dest} \\ -&& chmod +x ./install.sh \\ -&& ./install.sh +RUN --mount=type=cache,target=/var/cache/apt \\ + --mount=type=cache,target=/var/lib/apt/lists \\ + chmod -R 0755 ${dest} \\ + && cd ${dest} \\ + && chmod +x ./install.sh \\ + && ./install.sh `; } else { - result += `RUN --mount=type=bind,from=dev_containers_feature_content_source,source=${source},target=/tmp/build-features-src/${folder}${useSELinuxLabel ? ',z' : ''} \\ - cp -ar /tmp/build-features-src/${folder} ${FEATURES_CONTAINER_TEMP_DEST_FOLDER} \\ - && chmod -R 0755 ${dest} \\ - && cd ${dest} \\ - && chmod +x ./install.sh \\ - && ./install.sh \\ - && rm -rf ${dest} + result += `RUN --mount=type=cache,target=/var/cache/apt \\ + --mount=type=cache,target=/var/lib/apt/lists \\ + --mount=type=bind,from=dev_containers_feature_content_source,source=${source},target=/tmp/build-features-src/${folder}${useSELinuxLabel ? ',z' : ''} \\ + cp -ar /tmp/build-features-src/${folder} ${FEATURES_CONTAINER_TEMP_DEST_FOLDER} \\ + && chmod -R 0755 ${dest} \\ + && cd ${dest} \\ + && chmod +x ./install.sh \\ + && ./install.sh \\ + && rm -rf ${dest} `; } @@ -333,21 +341,25 @@ RUN chmod -R 0755 ${dest} \\ if (!useBuildKitBuildContexts) { result += ` COPY --chown=root:root --from=dev_containers_feature_content_source ${source} ${dest} -RUN chmod -R 0755 ${dest} \\ -&& cd ${dest} \\ -&& chmod +x ./devcontainer-features-install.sh \\ -&& ./devcontainer-features-install.sh +RUN --mount=type=cache,target=/var/cache/apt \\ + --mount=type=cache,target=/var/lib/apt/lists \\ + chmod -R 0755 ${dest} \\ + && cd ${dest} \\ + && chmod +x ./devcontainer-features-install.sh \\ + && ./devcontainer-features-install.sh `; } else { result += ` -RUN --mount=type=bind,from=dev_containers_feature_content_source,source=${source},target=/tmp/build-features-src/${feature.consecutiveId}${useSELinuxLabel ? ',z' : ''} \\ - cp -ar /tmp/build-features-src/${feature.consecutiveId} ${FEATURES_CONTAINER_TEMP_DEST_FOLDER} \\ - && chmod -R 0755 ${dest} \\ - && cd ${dest} \\ - && chmod +x ./devcontainer-features-install.sh \\ - && ./devcontainer-features-install.sh \\ - && rm -rf ${dest} +RUN --mount=type=cache,target=/var/cache/apt \\ + --mount=type=cache,target=/var/lib/apt/lists \\ + --mount=type=bind,from=dev_containers_feature_content_source,source=${source},target=/tmp/build-features-src/${feature.consecutiveId}${useSELinuxLabel ? ',z' : ''} \\ + cp -ar /tmp/build-features-src/${feature.consecutiveId} ${FEATURES_CONTAINER_TEMP_DEST_FOLDER} \\ + && chmod -R 0755 ${dest} \\ + && cd ${dest} \\ + && chmod +x ./devcontainer-features-install.sh \\ + && ./devcontainer-features-install.sh \\ + && rm -rf ${dest} `; } @@ -467,7 +479,7 @@ function updateFromOldProperties>) { const { output } = params; @@ -931,7 +943,7 @@ export async function processFeatureIdentifier(params: CommonParams, configPath: }; return newFeaturesSet; } else { - // We must have a tag, return a tarball URI for the tagged version. + // We must have a tag, return a tarball URI for the tagged version. let newFeaturesSet: FeatureSet = { sourceInformation: { type: 'github-repo', @@ -1151,7 +1163,7 @@ export async function fetchContentsAtTarballUri(params: { output: Log; env: Node // Reads the feature's 'devcontainer-feature.json` and applies any attributes to the in-memory Feature object. // NOTE: -// Implements the latest ('internalVersion' = '2') parsing logic, +// Implements the latest ('internalVersion' = '2') parsing logic, // Falls back to earlier implementation(s) if requirements not present. // Returns a boolean indicating whether the feature was successfully parsed. async function applyFeatureConfigToFeature(output: Log, featureSet: FeatureSet, feature: Feature, featCachePath: string, computedDigest: string | undefined): Promise {