Resolving Docker Multi-stage Build Errors on GitHub Actions
Setting up a CI/CD pipeline with multi-stage Docker builds on GitHub Actions can be a powerful way to automate your workflow. However, you may encounter challenges, as I recently did, when building a Docker image using multi-stage builds and GitHub Actions.
The Problem
The issue surfaced when I attempted to build a Docker image with multi-stage builds using the pnpm
package manager. The problem is not related to pnpm itself; instead, I utilized an example from the pnpm documentation for multi-stage builds. The error that emerged during the Docker image build process, seemingly tied to pnpm, was actually rooted in the Dockerfile's implementation of multi-stage builds. The error message specifically highlighted an inconsistency in the cache key calculation, leading to confusion and the need for further investigation.
The root cause of the issue lies in the Dockerfile
, particularly during the implementation of multi-stage builds and the integration of the pnpm package manager. For reference, the approach was inspired by the official documentation.
FROM node:20-slim AS base
ENV PNPM_HOME="/pnpm"
ENV PATH="$PNPM_HOME:$PATH"
RUN corepack enable
COPY . /app
WORKDIR /app
FROM base AS prod-deps
RUN pnpm install --prod --frozen-lockfile
FROM base AS build
RUN pnpm install --frozen-lockfile
RUN pnpm run build
FROM base
COPY /app/node_modules /app/node_modules
COPY /app/dist /app/dist
EXPOSE 8000
CMD [ "pnpm", "start" ]
This section of the Dockerfile, influenced by the recommended practices outlined in the official pnpm
documentation, initiates the build process. Despite following the prescribed steps, an unforeseen error arose during the GitHub Actions workflow, prompting the need for investigation and resolution.
GitHub Actions Workflow
The GitHub Actions workflow responsible for building and pushing the Docker image looked like this:
- name: Build and push Docker image
uses: docker/build-push-action@v5
with:
context: .
push: true
The encountered error was as follows:
buildx failed with: ERROR: failed to solve: failed to compute cache key: failed to calculate checksum of ref 8abab386-ee6f-4a46-817c-c1639873e713::8skhbm74dq1epjac59datwfm4: "...": not found
Solution 1: Consolidating Multi-Stage Builds
One approach to resolve this issue is to consolidate the multi-stage build into a single stage:
FROM base AS build
RUN pnpm install --frozen-lockfile
RUN pnpm run build
# Ensure `"type": "module"` is set in package.json
COPY /app/package.json /app/package.json
# Production dependencies
COPY /app/node_modules /app/node_modules
# Application
COPY /app/dist /app
However, be cautious as this modification might lead to another error:
Error: buildx failed with: ERROR: local cache importer requires src
Solution 2: Modifying GitHub Actions Workflow
In the pursuit of resolving the encountered error, I delved into the documentation for the docker/build-push-action
, available at docker/build-push-action. Here, I discovered a valuable configuration option called cache-from
that allows us to tweak the cache mechanism. For example, we can employ type=local,src=path/to/dir
to change the caching strategy.
Additionally, exploring the List of external cache sources revealed the use of gha
(Github Actions Cache) as a viable option for Docker builds within the GitHub Actions environment.
To mitigate the secondary error, it is recommended to enhance the GitHub Actions workflow by incorporating the cache-from
parameter:
- name: Build and push Docker image
uses: docker/build-push-action@v5
with:
context: .
push: true
cache-from: type=gha
These solutions aim to provide a smoother experience when working with multi-stage Docker builds in GitHub Actions. Integrating these adjustments into your pipeline should help mitigate common errors, ensuring a more efficient and reliable CI/CD process for your project.
Here's the full Dockerfile and GitHub Actions configuration:
# https://pnpm.io/docker#example-1-build-a-bundle-in-a-docker-container
FROM node:20-slim As base
ENV PNPM_HOME="/pnpm"
ENV PATH="$PNPM_HOME:$PATH"
RUN corepack enable
COPY . ./app
WORKDIR /app
USER node
FROM base AS build
RUN pnpm install --frozen-lockfile
RUN pnpm run build
# Not Natively support ARM64 (M1)
FROM alpine:3.19 As production
ENV PORT=3333
# Make sure `"type": "module"` is set in package.json
COPY /app/package.json /app/package.json
# Production dependencies
COPY /app/node_modules /app/node_modules
# Application
COPY /app/dist /app
RUN apk add --update --no-cache nodejs=$NODE_VERSION
EXPOSE ${PORT}
# Create a group and user
RUN addgroup -g 1000 node \
&& adduser -u 1000 -G node -s /bin/sh -D node
USER node
CMD [ "node", "app/main.js" ]
and Github Actions
# https://docs.github.com/en/actions/publishing-packages/publishing-docker-images#publishing-images-to-github-packages
name: Create and publish a Docker image
# Configures this workflow to run every time a change is pushed to the branch called `release`.
on:
push:
# branches: ['release']
branches:
- main
# Defines two custom environment variables for the workflow. These are used for the Container registry domain, and a name for the Docker image that this workflow builds.
env:
REGISTRY: ghcr.io
IMAGE_NAME: ${{ github.repository }}
# There is a single job in this workflow. It's configured to run on the latest available version of Ubuntu.
jobs:
build-and-push-image:
runs-on: ubuntu-latest
# Sets the permissions granted to the `GITHUB_TOKEN` for the actions in this job.
permissions:
contents: read
packages: write
#
steps:
- name: Checkout repository
uses: actions/checkout@v4
# Uses the `docker/login-action` action to log in to the Container registry registry using the account and password that will publish the packages. Once published, the packages are scoped to the account defined here.
- name: Log in to the Container registry
uses: docker/login-action@65b78e6e13532edd9afa3aa52ac7964289d1a9c1
with:
registry: ${{ env.REGISTRY }}
username: ${{ github.actor }}
password: ${{ secrets.GITHUB_TOKEN }}
# This step uses [docker/metadata-action](https://github.com/docker/metadata-action#about) to extract tags and labels that will be applied to the specified image. The `id` "meta" allows the output of this step to be referenced in a subsequent step. The `images` value provides the base name for the tags and labels.
- name: Extract metadata (tags, labels) for Docker
id: meta
uses: docker/metadata-action@9ec57ed1fcdbf14dcef7dfbe97b2010124a938b7
with:
images: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}
# This step uses the `docker/build-push-action` action to build the image, based on your repository's `Dockerfile`. If the build succeeds, it pushes the image to GitHub Packages.
# It uses the `context` parameter to define the build's context as the set of files located in the specified path. For more information, see "[Usage](https://github.com/docker/build-push-action#usage)" in the README of the `docker/build-push-action` repository.
# It uses the `tags` and `labels` parameters to tag and label the image with the output from the "meta" step.
- name: Build and push Docker image
uses: docker/build-push-action@v5
with:
context: .
push: true
tags: ${{ steps.meta.outputs.tags }}
labels: ${{ steps.meta.outputs.labels }}
cache-from: type=gha
You can also see the full example repo here.
Discussion
Investigating Multi-Stage Builds
In dissecting the encountered challenges during the Docker image build process, it became evident that the initial error, seemingly attributed to the pnpm
package manager, was, in fact, a consequence of the implementation of multi-stage builds in the Dockerfile. The chosen approach drew inspiration from the official pnpm documentation. This revelation clarified that the issue did not originate from pnpm
itself but rather from nuances in the Dockerfile structure.
Navigating Dockerfile Complexity
The Dockerfile's multi-stage build, designed to optimize the image creation process, unintentionally introduced complexities that led to an error in cache key calculation. Recognizing the root cause and understanding the intricacies of multi-stage builds proved pivotal in devising effective solutions.
Unveiling GitHub Actions Insights
In the quest for resolution, a deep dive into the documentation for the docker/build-push-action shed light on valuable configuration options. The discovery of the cache-from
parameter provided an avenue to refine the caching strategy, offering insights into optimizing the GitHub Actions workflow for Docker image builds.
Solutions Explored
Solution 1: Consolidating Multi-Stage Builds
The first solution involved consolidating the multi-stage build into a single stage, modifying the Dockerfile structure. However, this adjustment posed the risk of triggering another error, necessitating a careful balance between optimization and potential pitfalls.
Solution 2: Refining GitHub Actions Workflow
The second solution focused on refining the GitHub Actions workflow by leveraging insights from the docker/build-push-action documentation. The introduction of the cache-from
parameter, particularly using type=gha
(GitHub Actions Cache), aimed to enhance the caching strategy, addressing the secondary error and offering a more stable foundation for Docker image building within the GitHub Actions environment.
Conclusion
In navigating the complexities of multi-stage Docker builds and GitHub Actions, this exploration has illuminated the importance of meticulous configuration and adaptation. While inspired by best practices from the pnpm
documentation, it is crucial to recognize that each project's unique context may demand tailored solutions.
The refinement of the Dockerfile structure and the optimization of the GitHub Actions workflow showcase the iterative nature of development. By embracing insights from documentation and understanding the tools at our disposal, we can pave the way for smoother CI/CD pipelines and more resilient Docker image builds.