7
votes

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.

3

3 Answers

6
votes

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}
# ...
3
votes

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

     ...
1
votes

There is no special steps involved to use local image during build. Docker always checks what you have locally before going into registry. If you do both steps in one session and didn't make a typo somewhere - it simply works.

Now if it doesn't there are only two possible sources of problem left: it is either the environment on which you build those images or the action you use. Try running on ubuntu-latest and see if anything changes to eliminate the first.

I can also offer you two options to solve this but you probably won't like them both:

1. Use temporary local registry. I found this guide on the readme page of the action that you use and slightly changed it for your code:

jobs:
  local-registry:
    runs-on: ubuntu-latest
    services:
      registry:
        image: registry:2
        ports:
          - 5000:5000
    steps:
      -
        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:
          load: 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

Of course you have to update the service/Dockerfile to point to localhost:5000/my-project/base instead of just my-project/base.

2. Use simple shell instead of build-push-action. If your old code works and that action does not, well may be the problem is in the action after all.