It is common practice to build Docker containers in CI pipelines using tools like Kaniko. It is also common practice to version Docker images with tags like 1.0.1, 1.0, 1, latest. At one point in time, all tags probably pointed to the same image. Is it possible to write a CI/CD job that retags images without downloading the full image first?

TL/DR: docker buildx imagetools create --tag NEW EXISTING

Yes. Adding a new tag doesn’t require access to a Docker daemon. It’s a matter of sending the correct API requests to the Docker registry. docker buildx has all the features we need.

  • Assuming the original image is tagged as $CI_REGISTRY_IMAGE:$CI_COMMIT_TAG using GitLab CI/CD variables, the following lines retag an image with its major and minor version, e.g. 1.0.

    export newtag=$(echo $CI_COMMIT_TAG | cut -d. -f 1-2)
    docker buildx imagetools create --tag $CI_REGISTRY_IMAGE:$newtag $CI_REGISTRY_IMAGE:$CI_COMMIT_TAG 
    
  • To retag the image just with the major version, e.g. 1, use the following lines.

    export newtag=$(echo $CI_COMMIT_TAG | cut -d. -f 1)
    docker buildx imagetools create --tag $CI_REGISTRY_IMAGE:$newtag $CI_REGISTRY_IMAGE:$CI_COMMIT_TAG 
    
  • To retag the image just with latest use the following lines.

    docker buildx imagetools create --tag $CI_REGISTRY_IMAGE:latest $CI_REGISTRY_IMAGE:$CI_COMMIT_TAG 
    

The full GitLab pipeline could look like this.


stages:
- build
- tag

build:
  stage: build
  allow_failure: false
  image:
    name: gcr.io/kaniko-project/executor:v1.20.0-debug
    entrypoint: [""]
  rules:
    - if: $CI_COMMIT_TAG
  script:
    - echo "{\"auths\":{\"$CI_REGISTRY\":{\"auth\":\"$(printf "%s:%s" "${CI_REGISTRY_USER}" "${CI_REGISTRY_PASSWORD}" | base64 | tr -d '\n')\"}}}" > /kaniko/.docker/config.json
    - /kaniko/executor
      --context "${CI_PROJECT_DIR}"
      --dockerfile "${CI_PROJECT_DIR}/Dockerfile"
      --destination "${CI_REGISTRY_IMAGE}:${CI_COMMIT_TAG}"


.tag-template:
  stage: tag
  image: docker:24.0.7
  rules:
    - if: $CI_COMMIT_TAG
      when: manual
  before_script:
  - mkdir -p $HOME/.docker
  - echo "{\"auths\":{\"$CI_REGISTRY\":{\"auth\":\"$(printf "%s:%s" "${CI_REGISTRY_USER}" "${CI_REGISTRY_PASSWORD}" | base64 | tr -d '\n')\"}}}" > $HOME/.docker/config.json

tag-latest:
  extends: .tag-template
  script:
  - docker buildx imagetools create --tag $CI_REGISTRY_IMAGE:latest $CI_REGISTRY_IMAGE:$CI_COMMIT_TAG 

tag-major:
  extends: .tag-template
  script:
  - export newtag=$(echo $CI_COMMIT_TAG | cut -d. -f 1)
  - docker buildx imagetools create --tag $CI_REGISTRY_IMAGE:$newtag $CI_REGISTRY_IMAGE:$CI_COMMIT_TAG 

tag-minor:
  extends: .tag-template
  script:
  - export newtag=$(echo $CI_COMMIT_TAG | cut -d. -f 1-2)
  - docker buildx imagetools create --tag $CI_REGISTRY_IMAGE:$newtag $CI_REGISTRY_IMAGE:$CI_COMMIT_TAG 

Have a look at the end-to-end example.