A GitHub Action that caches Docker images specified in Docker Compose files to reduce workflow execution time. This action parses your Compose files, intelligently caches images using digests for verification, and only pulls images when necessary.
Pulling Docker images can significantly slow down CI/CD workflows in GitHub Actions. While actions/cache is effective for many scenarios, caching Docker images from registries presents unique challenges. This action addresses these challenges by:
- Parsing Docker Compose files to identify all images used in your services
- Caching each image as a separate tarball for better granularity
- Verifying image freshness via digest checks to prevent stale cache issues
- Selective image pulling based on cache status and digest verification
- Supporting multiple Compose files and image exclusion options
sequenceDiagram
autonumber
participant Workflow as GitHub Actions Workflow
participant Action as Docker Compose Cache Action
participant Cache as GitHub Actions Cache
participant Docker as Docker CLI
participant Registry as Docker Registry
Workflow->>Action: Start Action
Action->>Action: Parse Compose files
par For each service image (in parallel)
Action->>Cache: Restore image tar & manifest by cache key
alt Cache hit
par Restore & Remote Digest in parallel
Action->>Docker: docker load (from tar)
Action->>Docker: docker inspect (local)
and
Action->>Registry: docker buildx imagetools inspect (remote)
end
Action->>Action: Compare digests
alt Digest match
Action->>Action: Mark as Cached
else Digest mismatch
Action->>Docker: docker pull (with platform)
Action->>Docker: docker save (to tar)
Action->>Docker: docker buildx imagetools inspect (remote)
Action->>Cache: Save new tar & manifest
Action->>Action: Mark as Pulled
end
else Cache miss
Action->>Docker: docker pull (with platform)
Action->>Docker: docker save (to tar)
Action->>Docker: docker buildx imagetools inspect (remote)
Action->>Cache: Save new tar & manifest
Action->>Action: Mark as Pulled
end
end
Action->>Workflow: Set outputs (cache-hit, image-list)
Action->>Workflow: Write summary
- name: Cache Docker Compose Images
uses: seijikohara/docker-compose-cache-action@v1name: CI with Docker Compose Cache
on: [push, pull_request]
jobs:
test:
runs-on: ubuntu-latest
steps:
- name: Checkout code
uses: actions/checkout@v4
# Recommended: Login to Docker registries if using private images
# - name: Log in to Docker Hub
# uses: docker/login-action@v3
# with:
# username: ${{ secrets.DOCKERHUB_USERNAME }}
# password: ${{ secrets.DOCKERHUB_TOKEN }}
- name: Cache Docker Compose Images
id: cache-docker
uses: seijikohara/docker-compose-cache-action@v1
with:
# Optional: Specify one or more compose files
compose-files: |
docker-compose.yml
docker-compose.prod.yml
# Optional: Exclude specific images from caching (supports glob patterns)
exclude-images: |
nginx:*
*:latest
ghcr.io/myorg/*
# Optional: Change the cache key prefix
cache-key-prefix: my-project-docker-images
# Optional: Skip digest verification against the registry
skip-digest-verification: false
- name: Display Cache Info
run: |
echo "Cache hit for all images: ${{ steps.cache-docker.outputs.cache-hit }}"
# Parse the JSON output to display image information
IMAGE_LIST='${{ steps.cache-docker.outputs.image-list }}'
if [[ -n "$IMAGE_LIST" ]]; then
echo "Images processed:"
echo "$IMAGE_LIST" | jq -r '.[] | "- \(.name) [\(.status)]: \(.size) bytes processed in \(.processingTimeMs)ms"'
else
echo "No images processed"
fi
- name: Start services with Docker Compose
run: docker compose -f docker-compose.yml -f docker-compose.prod.yml up -d
# Your test and build steps go here
- name: Stop services
if: always()
run: docker compose down- Discovery: Parses Compose files to identify all Docker images used by services
- Cache Check: For each image, checks if a valid cached version exists using
actions/cache - Digest Validation: Uses
docker buildx imagetools inspectto compare cached image digest with registry - Smart Pulling: Only pulls images when necessary (not in cache or digest mismatch)
- Caching: Saves pulled images to the cache using
actions/cachefor future workflows - Detailed JSON Output: Provides comprehensive information about each image in JSON format, including size, processing time, and cache status
- GitHub Actions Summary: Generates a visual summary table with processing results directly in the GitHub Actions UI
This action also handles platform-specific images automatically. When a platform is specified in the Docker Compose service definition (e.g., platform: linux/arm64), it is respected during pulling and caching. For services without an explicit platform, the action uses the runner's architecture.
Cache keys include the image digest to ensure that updated images create new cache entries. The cache key format is:
{prefix}-{imageName}-{tag}-{os}-{arch}-{variant}-{digestPrefix}
For example:
docker-compose-image-nginx-latest-linux-amd64-none-a1b2c3d4e5f6
This design ensures that:
- Floating tags are handled correctly: When an image with a tag like
latestis updated in the registry, a new cache entry is created with the new digest - No stale cache issues: Each unique image version gets its own cache entry
- Automatic cache rotation: Old cache entries are naturally evicted by GitHub Actions' cache policies (unused entries expire after 7 days)
| Component | Description | Example |
|---|---|---|
prefix |
Custom prefix from cache-key-prefix input |
docker-compose-image |
imageName |
Docker image name (sanitized) | nginx, myregistry-com-myapp |
tag |
Image tag | latest, v1.0.0 |
os |
Operating system | linux, windows |
arch |
Architecture | amd64, arm64 |
variant |
Architecture variant (if applicable) | v7, v8, none |
digestPrefix |
First 12 characters of image digest | a1b2c3d4e5f6 |
By default, this action performs digest verification to ensure cached images are up-to-date with the registry. However, you can disable this behavior using the skip-digest-verification option:
- name: Cache Docker Compose Images (Skip Digest Verification)
uses: seijikohara/docker-compose-cache-action@v1
with:
skip-digest-verification: trueEnable skip-digest-verification: true when:
- You want faster workflow execution by avoiding registry calls
- Working with specific image versions that don't change (e.g.,
mysql:8.0.32instead ofmysql:8.0) - Registry connectivity is limited or slow
- You need consistent image versions across workflow runs
- Working in air-gapped environments
Keep skip-digest-verification: false (default) when:
- You use floating tags like
latest,stable, ormain - You want to ensure you have the most recent security updates
- Your images are frequently updated in the registry
- Cache freshness is more important than speed
| Setting | Behavior |
|---|---|
skip-digest-verification: false (default) |
Compares cached image digest with registry. Pulls fresh image if digests don't match. |
skip-digest-verification: true |
Uses cached images directly without registry verification. Significantly faster but may use outdated images. |
When skip-digest-verification: true is enabled and the registry is unavailable (e.g., network issues, registry downtime), the action will attempt to restore images from the cache using prefix-based matching. This allows CI workflows to continue even when the registry is temporarily unreachable.
Behavior when registry is unavailable:
| Setting | Behavior |
|---|---|
skip-digest-verification: true |
Attempts to restore from cache with a warning. CI continues if cache exists. |
skip-digest-verification: false |
Fails immediately since digest verification requires registry access. |
force-refresh: true |
Fails immediately since fresh images must be pulled from the registry. |
Example warning message:
Warning: Registry unavailable for nginx:latest. Using cached version. Image may be outdated. Enable network access or set force-refresh to pull fresh images.
This fallback mechanism is particularly useful for:
- Intermittent network issues: Continue CI when registry connectivity is temporarily lost
- Registry maintenance windows: Workflows can proceed using cached images during planned downtime
- Rate limiting: If you hit registry rate limits, cached versions can be used as a fallback
Note: The
skip-latest-checkinput is deprecated and will be removed in a future major version. Please useskip-digest-verificationinstead.
The force-refresh option allows you to bypass the cache completely and pull all images fresh from the registry. This is useful for:
- Security updates: Ensuring you have the latest security patches
- Debugging cache issues: Troubleshooting problems related to cached images
- Scheduled refreshes: Periodically updating images in scheduled workflows
# Force refresh - always pull fresh images
- name: Cache Docker Compose Images
uses: seijikohara/docker-compose-cache-action@v1
with:
force-refresh: true
# Conditional force refresh based on event type
- name: Cache Docker Compose Images
uses: seijikohara/docker-compose-cache-action@v1
with:
force-refresh: ${{ github.event_name == 'schedule' }}
# Force refresh on manual workflow dispatch
- name: Cache Docker Compose Images
uses: seijikohara/docker-compose-cache-action@v1
with:
force-refresh: ${{ github.event_name == 'workflow_dispatch' }}When force-refresh: true:
- Existing cache is completely ignored
- All images are pulled fresh from the registry
- Pulled images are saved to cache for future runs (when
force-refreshis not enabled)
This differs from changing cache-key-prefix in that the new images are still saved with the standard cache key, so subsequent runs without force-refresh will use the newly cached images.
The exclude-images input supports glob-style patterns for flexible image exclusion:
| Wildcard | Description | Example |
|---|---|---|
* |
Matches any characters | nginx:* |
? |
Matches single character | app?:latest |
exclude-images: |
# Exclude all nginx tags
nginx:*
# Exclude all 'latest' tags
*:latest
# Exclude all images from a registry
ghcr.io/myorg/*
# Exclude specific registry + tag combination
myregistry.com/*:dev
# Exact match (no wildcards)
redis:alpine
# Single character wildcard
app?:latest| Use Case | Pattern |
|---|---|
| Exclude all tags of an image | nginx:* |
| Exclude private registry images | ghcr.io/myorg/* |
| Exclude all latest tags | *:latest |
| Exclude dev/test images | *-dev:*, *-test:* |
| Exclude images by version range | postgres:1?-alpine |
| Input | Description | Required | Default |
|---|---|---|---|
compose-files |
Path(s) to Docker Compose file(s). Provide multiple files as multiline string with pipe character. | false |
Searches compose.yaml, compose.yml, docker-compose.yaml, docker-compose.yml |
exclude-images |
Images to exclude from caching. Supports glob patterns (* and ?). Provide multiple patterns as multiline string with pipe character. |
false |
(empty list) |
cache-key-prefix |
Prefix for the generated cache key for each image. Change to invalidate existing caches. | false |
docker-compose-image |
skip-digest-verification |
Skip verifying image digests against the remote registry. When enabled, cached images will be used without checking if newer versions are available. | false |
false |
force-refresh |
Ignore existing cache and pull all images fresh from the registry. Pulled images will still be saved to cache for future runs. | false |
false |
skip-latest-check |
[DEPRECATED] Use skip-digest-verification instead. This option will be removed in a future major version. |
false |
false |
| Output | Description | Example Value |
|---|---|---|
cache-hit |
Boolean value ('true' or 'false') indicating if all images were restored from cache. |
'true' |
image-list |
JSON array of image details with information about each image. Each image object contains: - name: Image name with tag- platform: Platform the image was pulled for- status: Either 'Cached', 'Pulled', or 'Error'- size: Size in bytes (numeric)- digest: Image digest string (may be empty if unavailable)- processingTimeMs: Processing time in milliseconds- cacheKey: Used cache key |
See image-list example below |
[
{
"name": "mysql:8.0",
"platform": "linux/amd64",
"status": "Cached",
"size": 524288000,
"digest": "sha256:abc123def456...",
"processingTimeMs": 1200.5,
"cacheKey": "docker-compose-image-mysql-8.0-linux-amd64-none-abc123def456"
},
{
"name": "redis:alpine",
"platform": "linux/amd64",
"status": "Pulled",
"size": 32768000,
"digest": "sha256:def456789abc...",
"processingTimeMs": 3500.2,
"cacheKey": "docker-compose-image-redis-alpine-linux-amd64-none-def456789abc"
},
{
"name": "node:18",
"platform": "linux/amd64",
"status": "Cached",
"size": 128456789,
"digest": "sha256:789abcdef012...",
"processingTimeMs": 950.8,
"cacheKey": "docker-compose-image-node-18-linux-amd64-none-789abcdef012"
}
]This action works best with public Docker images. For private registries, add authentication steps before this action:
steps:
- uses: actions/checkout@v4
- name: Log in to GitHub Container Registry
uses: docker/login-action@v3
with:
registry: ghcr.io
username: ${{ github.actor }}
password: ${{ secrets.GITHUB_TOKEN }}
- name: Cache Docker Compose Images
uses: seijikohara/docker-compose-cache-action@v1This action fully supports platform-specific Docker images, which is particularly useful for multi-architecture environments:
-
Explicit platform definition: When a
platformis specified in your compose file, the action honors it:services: myservice: image: myimage:latest platform: linux/arm64
-
Automatic platform detection: When no platform is specified, the action uses the runner's platform.
-
Cache isolation by platform: Images are cached separately for each platform, ensuring correct architecture-specific images are stored and retrieved.
-
Multi-architecture workflow support: You can use the same compose files across different runner architectures without issues.
- Works optimally with public Docker images
- Private registries require authentication configured before invoking the action
- Images built in the workflow (not pulled from a registry) won't have registry digests to verify against
-
Cache Size Limit: GitHub Actions provides up to 10 GB of cache storage per repository. Large Docker images (e.g.,
pytorch/pytorch,nvidia/cuda) may consume significant space. Consider usingexclude-imagesfor very large or frequently changing images. -
Cache Immutability: GitHub Actions cache entries cannot be overwritten with the same key. This action handles this by including the image digest in the cache key, ensuring updated images create new cache entries automatically.
-
Cache Eviction: Cache entries not accessed for 7 days are automatically deleted. Older entries are evicted first when approaching the storage limit. Branch-specific caches may also be evicted when branches are deleted.
-
Cross-Platform Caching: When caching ARM images on x86 runners (or vice versa), the images are cached correctly but cannot run without QEMU emulation:
- uses: docker/setup-qemu-action@v3 - uses: seijikohara/docker-compose-cache-action@v1
When an image fails to process, the image-list output will include an entry with status: "Error":
{
"name": "private-registry.com/myapp:latest",
"platform": "linux/amd64",
"status": "Error",
"size": 0,
"digest": "",
"processingTimeMs": 5234.7,
"cacheKey": ""
}| Error Scenario | Possible Cause | Solution |
|---|---|---|
| Authentication failure | Private registry without login | Add docker/login-action before this action |
| Image not found | Typo in image name or tag | Verify image exists in the registry |
| Digest retrieval failure | Registry doesn't support manifest inspection | Use skip-digest-verification: true |
| Network timeout | Registry unreachable | Use skip-digest-verification: true to fallback to cache, or check network |
| Registry unavailable | Registry downtime or network issues | Enable skip-digest-verification: true to use cached images as fallback |
- name: Log in to Docker Hub
uses: docker/login-action@v3
with:
username: ${{ secrets.DOCKERHUB_USERNAME }}
password: ${{ secrets.DOCKERHUB_TOKEN }}
- name: Cache Docker Compose Images
id: cache-docker
uses: seijikohara/docker-compose-cache-action@v1
- name: Check for errors
run: |
IMAGE_LIST='${{ steps.cache-docker.outputs.image-list }}'
ERRORS=$(echo "$IMAGE_LIST" | jq '[.[] | select(.status == "Error")]')
if [ "$ERRORS" != "[]" ]; then
echo "::warning::Some images failed to process:"
echo "$ERRORS" | jq -r '.[] | " - \(.name)"'
fiContributions are welcome! Please feel free to submit a Pull Request.
This project is licensed under the MIT License.