BuildKit Cache Dance

repository·main·Indexed 43 days ago

https://github.com/reproducible-containers/buildkit-cache-dance

A tool to persist Docker BuildKit cache mounts (`RUN --mount=type=cache`) across CI runs on platforms like GitHub Actions. It allows developers to extract cache contents from containers into persistent directories after a build and inject them back before a new build, supporting tools like apt-get and go-build. Available as a GitHub Action and a CLI tool.

Tokens
3.4K
Snippets
9
Records
12
Agent score
56%

What's inside BuildKit Cache Dance

  1. How BuildKit Cache Dance works

    main

    BuildKit Cache Dance solves the problem of losing RUN --mount=type=cache data between CI runs (like GitHub Actions). Since these cache mounts are local to the BuildKit builder and not automatically persisted by CI providers, this tool allows you to:

    1. Extract: After a build, extract the contents of the cache mounts from the container into a persistent directory (e.g., using actions/cache).
    2. Inject: Before a new build, inject the contents of that persistent directory back into the BuildKit builder's cache mounts.

    This enables persistent caches for tools like apt-get, go-build, and others across different CI jobs.

  2. Use BuildKit Cache Dance in GitHub Actions

    main

    To use this action in a GitHub Actions workflow, you need to follow a specific sequence:

    1. Use actions/cache to persist a directory (e.g., cache-mount).
    2. Use reproducible-containers/buildkit-cache-dance@v3 to restore (inject) the cache before your build step. Use skip-extraction: ${{ steps.cache.outputs.cache-hit }} to avoid redundant extraction if the cache was already found.
    3. Run your build (e.g., using docker/build-push-action).

    Note: Your Dockerfile must use --mount=type=cache for the paths you wish to persist.

          - name: Cache
            uses: actions/cache@v4
            id: cache
            with:
              path: cache-mount
              key: cache-mount-${{ hashFiles('Dockerfile') }}
    
          - name: Restore Docker cache mounts
            uses: reproducible-containers/buildkit-cache-dance@v3
            with:
              builder: ${{ steps.setup-buildx.outputs.name }}
              cache-dir: cache-mount
              dockerfile: Dockerfile
              skip-extraction: ${{ steps.cache.outputs.cache-hit }}
    
          - name: Build and push
            uses: docker/build-push-action@v5
            with:
              context: .
              cache-from: type=gha
              cache-to: type=gha,mode=max
              file: Dockerfile
  3. Auto-discovery of cache mounts from Dockerfile

    main

    If no --cache-map is provided, build-cache-dance attempts to automatically discover cache mounts by parsing the specified --dockerfile.

    It looks for RUN instructions containing a --mount=type=cache flag. For each match, it extracts the id or target attribute.

    If --cache-dir is provided, the discovered host path will be constructed as ${cache-dir}/${id}. The container target is automatically set to /var/cache-target for these discovered mounts.

  4. Configure the cache-map

    main

    The --cache-map option defines the relationship between host paths (or mount IDs) and their container targets. It accepts a JSON string representing a CacheMap.

    A CacheMap is a record where the key is the SourcePath (the directory on the host/action runner) and the value is CacheOptions.

    CacheOptions can be:

    1. A simple string representing the TargetPath.
    2. An object containing a target key and optional metadata like uid or gid.
    {
      "/host/cache/path": "/container/target/path",
      "/another/host/path": {
        "target": "/another/container/path",
        "uid": "1000",
        "gid": "1000"
      }
    }
  5. Configure the cache-map for fine-grained control

    main

    By default, the action attempts to auto-discover cache mounts from your Dockerfile. If you need manual control, provide a cache-map as a JSON string.

    Simple Mapping: Map a key (representing a path on the host/cache-dir) to the target path used in the Dockerfile.

    Advanced Mapping: Provide a JSON object for each key to include additional BuildKit mount options like id. The target property is mandatory.

    In GitHub Actions, use the cache-map input. In the CLI, use the --cache-map flag.

          - name: Restore Docker cache mounts
            uses: reproducible-containers/buildkit-cache-dance@v3
            with:
              builder: ${{ steps.setup-buildx.outputs.name }}
              cache-map: |
                {
                  "var-cache-apt": {
                    "target": "/var/cache/apt",
                    "id": "1"
                  },
                  "var-lib-apt": "/var/lib/apt"
                }
              skip-extraction: ${{ steps.cache.outputs.cache-hit }}
  6. Reference: BuildKit Cache Dance CLI options

    main

    The following options are available for the buildkit-cache-dance CLI:

    • --extract: Extract the cache from the docker container (extract step). Otherwise, inject the cache (main step).
    • --cache-map: The map of actions source to container destination paths for the cache paths.
    • --dockerfile: The Dockerfile to use for the auto-discovery of cache-map. Default: Dockerfile.
    • --cache-dir: The root directory where cache content is injected from/extracted to when using auto-discovery of the cache-map.
    • --scratch-dir: Where the action stores some temporary files for its processing. Default: scratch.
    • --skip-extraction: Skip the extraction of the cache from the docker container.
    • --builder: The name of the buildx builder. Default: default.
    • --help: Show this help.
  7. Run BuildKit Cache Dance via CLI

    main

    For CI systems other than GitHub Actions, you can run the tool directly using node.

    1. Download and extract the package from the GitHub releases/tags.
    2. Inject cache (before build): Run the script without the --extract flag.
    3. Extract cache (after build): Run the script with the --extract flag.

    Always provide a --cache-map to define which paths to handle.

    # Download
    curl -LJO https://github.com/reproducible-containers/buildkit-cache-dance/archive/refs/tags/v3.1.0.tar.gz
    tar xvf buildkit-cache-dance-3.1.0.tar.gz
    
    # Injection (Before Build)
    node ./buildkit-cache-dance-3.1.0/dist/index.js --cache-map '{"var-cache-apt": "/var/cache/apt", "var-lib-apt": "/var/lib/apt"}'
    
    # Extraction (After Build)
    node ./buildkit-cache-dance-3.1.0/dist/index.js --extract --cache-map '{"var-cache-apt": "/var/cache/apt", "var-lib-apt": "/var/lib/apt"}'
  8. Inject BuildKit caches using injectCaches()

    main

    The injectCaches function is the primary entrypoint for injecting BuildKit caches into a container image. It iterates through a cache map and performs a BuildKit build to copy cached files from a source directory into the target container image using --mount flags.

    To use this, you must provide an Opts object containing:

    • scratch-dir: A directory used to store temporary build files (like the generated Dancefile.inject).
    • utility-image: The base container image used for the injection build.
    • A cache map (retrieved via getCacheMap(opts)) which defines the source-to-target mapping and cache options.
    • An optional builder name (retrieved via getBuilder(opts)).
    import { injectCaches } from './src/inject-cache.js';
    
    // Example usage (requires an implementation of Opts)
    await injectCaches({
      'scratch-dir': './tmp',
      'utility-image': 'ubuntu:latest',
      // ... other options required by getCacheMap and getBuilder
    });
  9. Extract BuildKit caches using extractCaches()

    main

    The extractCaches function is the primary entrypoint for extracting BuildKit cache mounts from a container image and saving them to a local filesystem. It iterates through a mapping of cache sources and options, uses a temporary 'Dancefile' to mount the caches within a new container layer, and then unpacks those layers back to your host machine.

    Workflow

    1. Generates a Dancefile: Creates a temporary Dockerfile (Dancefile.extract) that uses --mount to access the specified cache paths.
    2. Builds an Extraction Image: Uses docker buildx build to create a temporary image tagged dance:extract.
    3. Unpacks Cache: Creates a container from the extraction image and pipes the contents of /var/dance-cache to the host using docker cp and tar.
    4. Atomic Replacement: Removes the existing cacheSource directory and renames the newly extracted directory to the target cacheSource path.

    Error Handling: Permission Issues

    If the process fails to remove the existing cache directory due to permission errors (EACCES or EPERM), the function automatically falls back to using a Docker container with a bind mount to perform the rm -rf operation on the host path.

    import { extractCaches } from './path-to-module';
    
    // Example usage with an Opts object
    await extractCaches({
      "skip-extraction": false,
      "scratch-dir": "./tmp/scratch",
      "utility-image": "debian:latest",
      // ... other Opts required by getCacheMap
    } as any);
  10. Reference: build-cache-dance CLI flags

    main

    The following flags are available for the build-cache-dance command:

    --extract      Extract the cache from the docker container (extract step). Otherwise, inject the cache (main step)
    --cache-map    The map of actions source paths to container destination paths or mount arguments
    --dockerfile   The Dockerfile to use for auto-discovery of the cache-map. Default: 'Dockerfile'
    --cache-dir    The root directory where cache content is injected from/extracted to when using auto-discovery of the cache-map.
    --scratch-dir  Where the action is stores some temporary files for its processing. Default: 'scratch'
    --skip-extraction  Skip the extraction of the cache from the docker container
    --utility-image  The container image to use for injecting and extracting the cache. Default: 'ghcr.io/containerd/busybox:latest'
    --builder      The name of the buildx builder to use for the cache injection
    --help         Show this help
  11. Use the buildkit-cache-dance CLI

    main

    The buildkit-cache-dance CLI is designed to manage BuildKit caches, typically within a CI environment like GitHub Actions. It operates in two primary modes:

    1. Injection Mode (Main Step): By default, the CLI runs injectCaches to prepare the environment for BuildKit. If running in a GitHub Actions environment (detected via the presence of GITHUB_STATE), it automatically appends POST=true to the GitHub Actions state file to signal that a post-run step is required.
    2. Extraction Mode (Post Step): When the --extract flag is provided, the CLI runs extractCaches to persist the caches after the build process is complete.

    To see all available options, run the CLI with the --help flag.

    # Example: Run the main injection step
    buildkit-cache-dance
    
    # Example: Run the post-build extraction step
    buildkit-cache-dance --extract
  12. CLI Options for build-cache-dance

    main

    The build-cache-dance CLI accepts several options to control how BuildKit cache mounts are injected or extracted.

    Note on Deprecation: The options --cache-source and --cache-target are deprecated. You should use --cache-map instead.

    # Example usage (conceptual)
    build-cache-dance --cache-map '{"/host/path": "/container/path"}' --dockerfile Dockerfile