GitLab教程(9): 构建Docker镜像与容器镜像仓库

GitLab提供了内置的容器镜像仓库(Container Registry),可以与CI/CD无缝集成。本文将介绍如何在GitLab中构建和管理Docker镜像。

GitLab Container Registry

# 镜像仓库地址格式
registry.gitlab.com//
registry.gitlab.com///
registry.gitlab.com///:

# 示例
registry.gitlab.com/mygroup/myproject
registry.gitlab.com/mygroup/myproject:latest
registry.gitlab.com/mygroup/myproject/backend:v1.0.0
registry.gitlab.com/mygroup/myproject/frontend:v1.0.0

# 自托管GitLab的地址
registry.example.com/mygroup/myproject:latest

# 查看镜像
# Project > Packages & Registries > Container Registry

登录镜像仓库

# 使用个人访问令牌登录
docker login registry.gitlab.com
Username: your-username
Password: your-personal-access-token

# 输出
Login Succeeded

# 非交互式登录
echo $CI_REGISTRY_PASSWORD | docker login -u $CI_REGISTRY_USER --password-stdin $CI_REGISTRY

# 在CI/CD中自动登录(使用预定义变量)
# $CI_REGISTRY = registry.gitlab.com
# $CI_REGISTRY_USER = gitlab-ci-token
# $CI_REGISTRY_PASSWORD = $CI_JOB_TOKEN
# $CI_REGISTRY_IMAGE = registry.gitlab.com/group/project

在CI/CD中构建镜像

使用Docker-in-Docker

# .gitlab-ci.yml

variables:
  DOCKER_TLS_CERTDIR: "/certs"

stages:
  - build
  - push

build-image:
  stage: build
  image: docker:24
  services:
    - docker:24-dind
  before_script:
    - docker login -u $CI_REGISTRY_USER -p $CI_REGISTRY_PASSWORD $CI_REGISTRY
  script:
    - docker build -t $CI_REGISTRY_IMAGE:$CI_COMMIT_SHA .
    - docker push $CI_REGISTRY_IMAGE:$CI_COMMIT_SHA
  
# 执行输出
$ docker login -u gitlab-ci-token -p [MASKED] registry.gitlab.com
Login Succeeded
$ docker build -t registry.gitlab.com/mygroup/myproject:abc123 .
Sending build context to Docker daemon  15.36MB
Step 1/10 : FROM node:18-alpine
 ---> 7a3c3e7d0e5a
Step 2/10 : WORKDIR /app
...
Successfully built 1a2b3c4d5e6f
Successfully tagged registry.gitlab.com/mygroup/myproject:abc123
$ docker push registry.gitlab.com/mygroup/myproject:abc123
The push refers to repository [registry.gitlab.com/mygroup/myproject]
abc123: Pushed
latest: digest: sha256:xxx size: 1234

使用Kaniko(无需特权模式)

# Kaniko不需要特权模式,更安全

build-image:
  stage: build
  image:
    name: gcr.io/kaniko-project/executor:v1.19.2-debug
    entrypoint: [""]
  script:
    - mkdir -p /kaniko/.docker
    - 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:-latest}" \
        --destination "${CI_REGISTRY_IMAGE}:${CI_COMMIT_SHA}"

# Kaniko构建输出
INFO[0000] Retrieving image manifest node:18-alpine     
INFO[0002] Retrieving image manifest node:18-alpine     
INFO[0004] Built cross stage deps: map[]                
INFO[0004] Retrieving image manifest node:18-alpine     
INFO[0006] Executing 0 build triggers                   
INFO[0006] Building stage 'node:18-alpine' [idx: '0', base-idx: '-1']
INFO[0006] Unpacking rootfs as cmd COPY package*.json ./ requires it.
INFO[0010] WORKDIR /app                                  
INFO[0010] Cmd: workdir                                  
INFO[0010] COPY package*.json ./                         
INFO[0010] RUN npm ci                                    
...
INFO[0045] Pushing image to registry.gitlab.com/mygroup/myproject:latest
INFO[0050] Pushed registry.gitlab.com/mygroup/myproject:latest

多阶段构建

# Dockerfile 多阶段构建

# 构建阶段
FROM node:18-alpine AS builder
WORKDIR /app
COPY package*.json ./
RUN npm ci
COPY . .
RUN npm run build

# 生产阶段
FROM node:18-alpine AS production
WORKDIR /app
COPY --from=builder /app/dist ./dist
COPY --from=builder /app/node_modules ./node_modules
COPY package*.json ./
EXPOSE 3000
CMD ["node", "dist/main.js"]

# .gitlab-ci.yml
build:
  stage: build
  image: docker:24
  services:
    - docker:24-dind
  script:
    - docker login -u $CI_REGISTRY_USER -p $CI_REGISTRY_PASSWORD $CI_REGISTRY
    - docker build --target production -t $CI_REGISTRY_IMAGE:$CI_COMMIT_SHA .
    - docker push $CI_REGISTRY_IMAGE:$CI_COMMIT_SHA

缓存Docker层

# 使用--cache-from加速构建

build:
  stage: build
  image: docker:24
  services:
    - docker:24-dind
  variables:
    DOCKER_BUILDKIT: 1
  script:
    - docker login -u $CI_REGISTRY_USER -p $CI_REGISTRY_PASSWORD $CI_REGISTRY
    # 拉取上次构建的镜像作为缓存
    - 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

# 构建输出(使用缓存)
Step 1/10 : FROM node:18-alpine
 ---> Using cache
 ---> 7a3c3e7d0e5a
Step 2/10 : WORKDIR /app
 ---> Using cache
 ---> 8b4c5d6e7f8g
...

多架构镜像

# 使用buildx构建多架构镜像

build-multiarch:
  stage: build
  image: docker:24
  services:
    - docker:24-dind
  variables:
    DOCKER_BUILDKIT: 1
  before_script:
    - docker login -u $CI_REGISTRY_USER -p $CI_REGISTRY_PASSWORD $CI_REGISTRY
    - docker buildx create --use --name multiarch-builder
    - docker buildx inspect --bootstrap
  script:
    - |
      docker buildx build \
        --platform linux/amd64,linux/arm64 \
        --tag $CI_REGISTRY_IMAGE:$CI_COMMIT_SHA \
        --tag $CI_REGISTRY_IMAGE:latest \
        --push \
        .

# 输出
[+] Building 120.5s (18/18) FINISHED
 => [linux/amd64] FROM node:18-alpine
 => [linux/arm64] FROM node:18-alpine
 => ...
 => pushing layers
 => pushing manifest for registry.gitlab.com/mygroup/myproject:latest

镜像标签策略

# 完整的标签策略示例

variables:
  DOCKER_TLS_CERTDIR: "/certs"

build:
  stage: build
  image: docker:24
  services:
    - docker:24-dind
  before_script:
    - docker login -u $CI_REGISTRY_USER -p $CI_REGISTRY_PASSWORD $CI_REGISTRY
  script:
    # 基础镜像名
    - IMAGE_NAME="$CI_REGISTRY_IMAGE"
    
    # 构建镜像
    - docker build -t $IMAGE_NAME:$CI_COMMIT_SHA .
    
    # 推送commit SHA标签
    - docker push $IMAGE_NAME:$CI_COMMIT_SHA
    
    # 如果是tag,推送版本标签
    - |
      if [ -n "$CI_COMMIT_TAG" ]; then
        docker tag $IMAGE_NAME:$CI_COMMIT_SHA $IMAGE_NAME:$CI_COMMIT_TAG
        docker push $IMAGE_NAME:$CI_COMMIT_TAG
      fi
    
    # 如果是main分支,推送latest
    - |
      if [ "$CI_COMMIT_BRANCH" == "main" ]; then
        docker tag $IMAGE_NAME:$CI_COMMIT_SHA $IMAGE_NAME:latest
        docker push $IMAGE_NAME:latest
      fi
    
    # 如果是develop分支,推送dev标签
    - |
      if [ "$CI_COMMIT_BRANCH" == "develop" ]; then
        docker tag $IMAGE_NAME:$CI_COMMIT_SHA $IMAGE_NAME:dev
        docker push $IMAGE_NAME:dev
      fi

# 结果
# main分支: :abc123, :latest
# develop分支: :abc123, :dev
# v1.0.0 tag: :abc123, :v1.0.0

镜像扫描

# GitLab内置容器扫描

include:
  - template: Security/Container-Scanning.gitlab-ci.yml

variables:
  CS_IMAGE: $CI_REGISTRY_IMAGE:$CI_COMMIT_SHA

# 或使用Trivy
container-scan:
  stage: test
  image:
    name: aquasec/trivy:latest
    entrypoint: [""]
  script:
    - trivy image --exit-code 1 --severity HIGH,CRITICAL $CI_REGISTRY_IMAGE:$CI_COMMIT_SHA
  allow_failure: true

# Trivy扫描输出
registry.gitlab.com/mygroup/myproject:abc123 (alpine 3.18.4)

Total: 2 (HIGH: 1, CRITICAL: 1)

┌──────────────┬────────────────┬──────────┬────────────────┐
│   Library    │ Vulnerability  │ Severity │ Fixed Version  │
├──────────────┼────────────────┼──────────┼────────────────┤
│ openssl      │ CVE-2024-1234  │ CRITICAL │ 3.1.4-r1       │
│ curl         │ CVE-2024-5678  │ HIGH     │ 8.4.0-r0       │
└──────────────┴────────────────┴──────────┴────────────────┘

清理镜像

# 配置清理策略
# Project > Settings > Packages & Registries > Container Registry

# 清理规则示例
- 保留最近10个标签
- 删除超过30天的未标记镜像
- 保护带有语义版本的标签 (v*.*.*)

# 使用API清理
curl --request DELETE \
  --header "PRIVATE-TOKEN: your-token" \
  "https://gitlab.example.com/api/v4/projects/42/registry/repositories/1/tags/old-tag"

# CI/CD中清理旧镜像
cleanup:
  stage: cleanup
  image: alpine:latest
  script:
    - apk add --no-cache curl jq
    - |
      # 获取所有标签
      TAGS=$(curl -s --header "PRIVATE-TOKEN: $REGISTRY_TOKEN" \
        "$CI_API_V4_URL/projects/$CI_PROJECT_ID/registry/repositories/1/tags" | jq -r '.[].name')
      
      # 删除旧标签(保留最近10个)
      echo "$TAGS" | tail -n +11 | while read tag; do
        echo "Deleting tag: $tag"
        curl --request DELETE \
          --header "PRIVATE-TOKEN: $REGISTRY_TOKEN" \
          "$CI_API_V4_URL/projects/$CI_PROJECT_ID/registry/repositories/1/tags/$tag"
      done
  only:
    - schedules

完整示例

stages:
  - build
  - scan
  - push
  - deploy

variables:
  DOCKER_TLS_CERTDIR: "/certs"
  IMAGE_TAG: $CI_REGISTRY_IMAGE:$CI_COMMIT_SHA

build:
  stage: build
  image: docker:24
  services:
    - docker:24-dind
  script:
    - docker login -u $CI_REGISTRY_USER -p $CI_REGISTRY_PASSWORD $CI_REGISTRY
    - docker pull $CI_REGISTRY_IMAGE:latest || true
    - docker build --cache-from $CI_REGISTRY_IMAGE:latest -t $IMAGE_TAG .
    - docker push $IMAGE_TAG

scan:
  stage: scan
  image:
    name: aquasec/trivy:latest
    entrypoint: [""]
  script:
    - trivy image --exit-code 0 --severity HIGH,CRITICAL $IMAGE_TAG
  allow_failure: true

push-latest:
  stage: push
  image: docker:24
  services:
    - docker:24-dind
  script:
    - docker login -u $CI_REGISTRY_USER -p $CI_REGISTRY_PASSWORD $CI_REGISTRY
    - docker pull $IMAGE_TAG
    - docker tag $IMAGE_TAG $CI_REGISTRY_IMAGE:latest
    - docker push $CI_REGISTRY_IMAGE:latest
  rules:
    - if: $CI_COMMIT_BRANCH == "main"

deploy:
  stage: deploy
  image: bitnami/kubectl:latest
  script:
    - kubectl set image deployment/myapp myapp=$IMAGE_TAG
  rules:
    - if: $CI_COMMIT_BRANCH == "main"
      when: manual

总结

本文介绍了GitLab Container Registry的使用方法,包括镜像构建、推送、扫描和清理。内置的镜像仓库与CI/CD无缝集成,简化了容器化应用的开发流程。

下一篇我们将学习GitLab与Kubernetes的集成。

发表回复

后才能评论