When building a multiplatform image in GitHub Actions, sometimes you must use a custom configuration for the Docker daemon. For example, if you want to load multiplatform images to Docker, you must enable the containerd image store feature. And, of course, there is a GitHub action to do that:
- name: Set up Docker uses: crazy-max/ghaction-setup-docker@v3 with: daemon-config: | { "features": { "containerd-snapshotter": true } }
Then, if you need to pass the resulting image to another action running in a Docker container, you typically do something like this:
- name: Build and push container image uses: docker/build-push-action@v6 id: push with: context: ${{ inputs.context }} file: ${{ inputs.file }} platforms: ${{ inputs.platforms }} outputs: | type=docker,rewrite-timestamp=true type=image,push=${{ inputs.push }},rewrite-timestamp=true tags: | ${{ inputs.primaryTag }} ${{ inputs.tags }} build-args: ${{ inputs.args }} cache-from: ${{ inputs.cache-from }} cache-to: ${{ inputs.cache-to }} no-cache: ${{ inputs.no-cache }} env: SOURCE_DATE_EPOCH: 0 - name: Security Scan shell: bash run: | docker run --rm \ -v /var/run/docker.sock:/var/run/docker.sock \ -v $(pwd)/.cache:/root/.cache \ -v $(pwd):/workdir \ -w /workdir \ aquasec/trivy:0.57.1 image --format json --ignore-unfixed --pkg-types os --scanners vuln --db-repository ghcr.io/aquasecurity/trivy-db:2,public.ecr.aws/aquasecurity/trivy-db:2 ${{ inputs.primaryTag }}
The above steps build the image and then run the Aqua Trivy Vulnerability Scanner on the built image. inputs.push
is true
for pushes to the main branch and false
for
pull requests.
Suppose we have a custom image, and Dependabot wants to update its base image. Our custom image uses the same tag as the base image. If we use the above configuration, the workflow will fail: Trivy will be unable to find the image.
But why? We see that docker image ls
confirms that the image is there. But why doesn’t Trivy see it? The answer is simple: Docker contexts.
When we set up our custom Docker configuration with the containerd
image store, the action created a new Docker context. If we run docker context ls
, we will see something like this:
We see that the current context is called setup-docker-action
and it uses its own socket instead of the standard /var/run/docker.sock
. And because we pass the wrong socket to Trivy, it talks to a different Docker instance that is unaware of the image we have just built. And because this is a pull request, we don’t push the image into the registry. Therefore, the images exists neither in the Docker image store, nor in the registry. If, however, we have passed .../nginx:latest
to Trivy, it would download the old image from the registry, and we would get the wrong scan results.
My solution to this issue was to extract the socket from the current context and pass it to the scanner:
- name: Get Docker socket id: socket run: echo docker_socket="$(docker context ls --format json | jq -r 'select(.Current == true) | .DockerEndpoint' | sed 's!^unix://!!')" >> "${GITHUB_OUTPUT}" shell: bash - name: Security Scan shell: bash run: | docker run --rm \ -v ${{ steps.socket.outputs.docker_socket }}:/var/run/docker.sock \ # ...
When using custom Docker configurations in GitHub Actions, understanding the implications of Docker contexts is crucial to avoid workflow failures. Mismatched endpoints can lead to discrepancies between the runtime environment and external processes, resulting in errors or incorrect behavior. Never forget about Docker contexts.