Build docker image locally in GitHub Actions using docker/build-push-action

Viewed 13404

I have several Dockerfiles in my project. One is for building basic image, which contains some business-level abstractions. Others are building services, based on the basic image.

So in my services' Dockerfiles I have something like

FROM my-project/base
# Adding some custom logic around basic stuff

I am using GitHub Actions as my CI/CD tool. At first I had a step to install docker into my workers, and then ran something like:

- name: Build base image
  working-directory: business
  run: docker build -t my-project/base .

- name: Build and push service
  working-directory: service
  run: |
    docker build -t my-ecr-repo/service .
    docker push my-ecr-repo/service

But then I've found docker/build-push-action and decided to use it in my pipeline:

- name: Build business-layer container
  uses: docker/build-push-action@v2
  with:
    load: true
    tags: my-project/base
    context: business
    file: business/Dockerfile

- name: Build service
  uses: docker/build-push-action@v2
  with:
    push: true
    tags: my-ecr-repo/service
    context: service
    file: service/Dockerfile

As for now, the second step tries to download docker.io/my-project/base, and obviously cannot do it, because I never push base image:

ERROR: pull access denied, repository does not exist or may require authorization: server message: insufficient_scope: authorization failed

The question is: What is the correct way to build an image, so it is accessible by the following building steps locally?

PS: I don't want to push my naked basic image anywhere.

2 Answers

I believe you'll need to set load: true in both your base image and the final image. This changes the behavior to use the local docker engine for images. I believe you'll need to run a separate push if you do this, e.g.:

- name: Build business-layer container
  uses: docker/build-push-action@v2
  with:
    load: true
    tags: my-project/base
    context: business
    file: business/Dockerfile

- name: Build service
  uses: docker/build-push-action@v2
  with:
    load: true
    tags: my-ecr-repo/service
    context: service
    file: service/Dockerfile

- name: push service
  run: |
    docker push my-ecr-repo/service

The other option is to use a local registry. This has the advantage of supporting multi-platform builds. But you'll want to switch from load to push with your base image, and I'd pass the base image as a build arg to make it easier for use cases outside of Github actions, e.g.:

jobs:
  local-registry:
    runs-on: ubuntu-latest
    services:
      registry:
        image: registry:2
        ports:
          - 5000:5000
    steps:
      - name: Login to DockerHub
        uses: docker/login-action@v1 
        with:
          username: ${{ secrets.DOCKERHUB_USERNAME }}
          password: ${{ secrets.DOCKERHUB_TOKEN }}
      # qemu should only be needed for multi-platform images
      - name: Set up QEMU
        uses: docker/setup-qemu-action@v2
      - name: Set up Docker Buildx
        uses: docker/setup-buildx-action@v1
        with:
          driver-opts: network=host
      - name: Build business-layer container
        uses: docker/build-push-action@v2
        with:
          push: true
          tags: localhost:5000/my-project/base
          context: business
          file: business/Dockerfile
      - name: Build service
        uses: docker/build-push-action@v2
        with:
          push: true
          tags: my-ecr-repo/service
          context: service
          file: service/Dockerfile
          build-args: |
            BASE_IMAGE=localhost:5000/my-project/base

And then your Dockerfile would allow the base image to be specified as a build arg:

ARG BASE_IMAGE=my-project/base
FROM ${BASE_IMAGE}
# ...

The github action docker/setup-buildx-action@v1 defaults to driver docker-container as documented.
This means builds will run, by default, in a container and thus images won't be available outside of the action.
The solution is to set the driver to docker:

     ...

      - name: Set up Docker Buildx
        uses: docker/setup-buildx-action@v1
        with:
          driver: docker # defaults to "docker-containerized"

      - name: Build business-layer container
        uses: docker/build-push-action@v2
        with:
          # using "load: true" forces the docker driver
          # not necessary here, because we set it before
          #load: true
          tags: my-project/base:latest
          context: business
          file: business/Dockerfile

      - name: Build service
        uses: docker/build-push-action@v2
        with:
          # using "push: true" will lead to error:
          # Error: buildx call failed with: auto-push is currently not implemented for docker driver
          # so you will have to follow the solution outlined here:
          # https://github.com/docker/build-push-action/issues/100#issuecomment-715352826
          # and push the image manually following the build
          #push: true
          tags: my-ecr-repo/service:latest
          context: service
          file: service/Dockerfile

      # list all images, you will see my-ecr-repo/service and my-project/base
      - name: Look up images
        run: docker image ls

      # push the image manually, see above comment
      - name: Push image
        run: docker push my-ecr-repo/service:latest

     ...
Related