В CI билдить приложение можно как в окружении runner, так и в docker in docker.

Если в первом случае мы можем сделать service specific конфигурацию, настроить кеши и т.д. То во втором случае мы будем в меньшей степени зависить от конкретного CI сервиса и не придется для теста билда гонять CI.

Но при билде docker-in-docker возникает вопрос производительности и кеширования.

Универсальный способ - спулить последний образ и использовать его как кеш.

Вот пример из документации gitlab https://docs.gitlab.com/ee/ci/docker/using_docker_build.html

before_script:
  - docker login -u $CI_REGISTRY_USER -p $CI_REGISTRY_PASSWORD $CI_REGISTRY

build:
  stage: build
  script:
    - docker pull $CI_REGISTRY_IMAGE:latest || true
    - docker build --cache-from $CI_REGISTRY_IMAGE:latest --tag $CI_REGISTRY_IMAGE:$CI_COMMIT_SHA --tag $CI_REGISTRY_IMAGE:latest .
    - docker push $CI_REGISTRY_IMAGE:$CI_COMMIT_SHA
    - docker push $CI_REGISTRY_IMAGE:latest

Но если Dockerfile содержит несколько stage, то данный вариант не будет работать, так как кешироваться будет только последняя стадия.

К счастью есть способ, который позволяет обойти эту проблему. Нам нужны образы каждой стадии.

Вот пример для образа из двух стадий

variables:
	DOCKER_BUILDKIT: 1

.build_script: &build_script
  script:
    - docker pull $IMAGE_BUILD_TAG || true
    - docker build  --target build-stage --build-arg BUILDKIT_INLINE_CACHE=1 -t $IMAGE_BUILD_TAG --cache-from $IMAGE_BUILD_TAG .
    - docker push $IMAGE_BUILD_TAG
    - docker pull $IMAGE_TAG1 || true
    - docker build  --target deploy-stage  --build-arg BUILDKIT_INLINE_CACHE=1 --cache-from $IMAGE_BUILD_TAG --cache-from $IMAGE_TAG1 -t $IMAGE_TAG1 -t $IMAGE_TAG2 .
    - docker push $IMAGE_TAG1
    - docker push $IMAGE_TAG2

Для этого нам понадобится

  1. активировать buildkit и сохранить cache metadata в образ путем использования переменной BUILDKIT_INLINE_CACHE
  2. билдить каждую stage отдельно и пушить в репозиторий
  3. перед билдами пулить образ для stage