使用 Docker 构建 Docker 镜像

您可以将 GitLab CI/CD 与 Docker 结合使用来创建 Docker 镜像。 例如,您可以创建应用程序的 Docker 镜像,对其进行测试,然后将其发布到容器镜像库。

要在 CI/CD 作业中运行 Docker 命令,您必须配置 GitLab Runner 以支持 docker 命令。

在 CI/CD 作业中启用 Docker 命令

要为 CI/CD 作业启用 Docker 命令,您可以使用:

如果不想在特权模式下执行 runner,而想使用 docker build,也可以使用 kanikobuildah

使用 shell executor

要在 CI/CD 作业中包含 Docker 命令,您可以将 runner 配置为使用 shell executor。在此配置中,gitlab-runner 用户运行 Docker 命令,但需要权限才能这样做。

  1. 安装 GitLab Runner。
  2. 注册 一个 runner。 选择 shell executor。例如:

    sudo gitlab-runner register -n \
      --url https://gitlab.com/ \
      --registration-token REGISTRATION_TOKEN \
      --executor shell \
      --description "My Runner"
    
  3. 在安装了 GitLab Runner 的服务器上,安装 Docker Engine。查看支持的平台列表。

  4. gitlab-runner 用户添加到 docker 组:

    sudo usermod -aG docker gitlab-runner
    
  5. 验证 gitlab-runner 是否可以访问 Docker:

    sudo -u gitlab-runner -H docker info
    
  6. 在 GitLab 中,为了验证一切正常,将 docker info 添加到 .gitlab-ci.yml

    before_script:
      - docker info
    
    build_image:
      script:
        - docker build -t my-docker-image .
        - docker run my-docker-image /script/to/run/tests
    

您现在可以使用 docker 命令(并在需要时安装 docker-compose)。

当您将 gitlab-runner 添加到 docker 组时,您实际上是在授予 gitlab-runner 完整的 root 权限。 了解更多关于 docker 组的安全性

使用 Docker-in-Docker

“Docker-in-Docker”(dind)意味着:

  • 您注册的 runner 使用 Docker executor 或 Kubernetes executor。
  • Executor 使用 Docker 提供的 Docker 容器镜像 来运行您的 CI/CD 作业。

Docker 镜像安装了所有的 docker 工具,并且可以在特权模式下,在镜像的上下文中运行作业脚本。

我们建议您使用 Docker-in-Docker with TLS enabled。

您应该始终指定镜像的特定版本,例如 docker:20.10.16。 如果使用像 docker:stable 这样的标签,你就无法控制使用哪个版本。 可能会导致不可预测的行为,尤其是在发布新版本时。

将 Docker executor 与 Docker-in-Docker 一起使用

您可以使用 Docker executor 在 Docker 容器中运行作业。

在 Docker executor 中启用 TLS 的 Docker-in-Docker

Docker 守护进程支持通过 TLS 的连接。在 Docker 20.10.16 及更高版本中,TLS 是默认设置。

caution此任务启用--docker-privileged。当您这样做时,您实际上是在禁用容器的所有安全机制并将您的主机暴露给特权升级。这样做会导致 container breakout。有关更多信息,请参阅有关运行时权限和 Linux 功能 的官方 Docker 文档。

要在启用 TLS 的情况下使用 Docker-in-Docker:

  1. 安装 GitLab Runner。
  2. 从命令行注册 GitLab Runner。使用 dockerprivileged 模式:

    sudo gitlab-runner register -n \
      --url https://gitlab.com/ \
      --registration-token REGISTRATION_TOKEN \
      --executor docker \
      --description "My Docker Runner" \
      --docker-image "docker:20.10.16" \
      --docker-privileged \
      --docker-volumes "/certs/client"
    
    • 这个命令注册一个新的 runner 来使用 docker:20.10.16 镜像。要启动构建和服务容器,它使用 privileged 模式。如果您想使用 Docker-in-Docker,您必须始终在 Docker 容器中使用 privileged = true
    • 此命令为服务和构建容器挂载 /certs/client,这是 Docker 客户端使用该目录中的证书所必需的。有关使用 TLS 的 Docker 如何工作的更多信息,请参阅 https://hub.docker.com/_/docker/#tls

    前面的命令创建了一个类似于这样的 config.toml 条目:

    [[runners]]
      url = "https://gitlab.com/"
      token = TOKEN
      executor = "docker"
      [runners.docker]
        tls_verify = false
        image = "docker:20.10.16"
        privileged = true
        disable_cache = false
        volumes = ["/certs/client", "/cache"]
      [runners.cache]
        [runners.cache.s3]
        [runners.cache.gcs]
    
  3. 您现在可以在作业脚本中使用 docker。注意包含了 docker:20.10.16-dind 服务:

    image: docker:20.10.16
    
    variables:
      # When you use the dind service, you must instruct Docker to talk with
      # the daemon started inside of the service. The daemon is available
      # with a network connection instead of the default
      # /var/run/docker.sock socket. Docker 19.03 does this automatically
      # by setting the DOCKER_HOST in
      # https://github.com/docker-library/docker/blob/d45051476babc297257df490d22cbd806f1b11e4/19.03/docker-entrypoint.sh#L23-L29
      #
      # The 'docker' hostname is the alias of the service container as described at
      # https://docs.gitlab.com/ee/ci/services/#accessing-the-services.
      #
      # Specify to Docker where to create the certificates. Docker
      # creates them automatically on boot, and creates
      # `/certs/client` to share between the service and job
      # container, thanks to volume mount from config.toml
      DOCKER_TLS_CERTDIR: "/certs"
    
    services:
      - docker:20.10.16-dind
    
    before_script:
      - docker info
    
    build:
      stage: build
      script:
        - docker build -t my-docker-image .
        - docker run my-docker-image /script/to/run/tests
    
在 Docker executor 中禁用 TLS 的 Docker-in-Docker

有时您可能有正当理由禁用 TLS。 例如,您无法控制正在使用的 GitLab Runner 配置。

假设 runner 的 config.toml 类似于:

[[runners]]
  url = "https://gitlab.com/"
  token = TOKEN
  executor = "docker"
  [runners.docker]
    tls_verify = false
    image = "docker:20.10.16"
    privileged = true
    disable_cache = false
    volumes = ["/cache"]
  [runners.cache]
    [runners.cache.s3]
    [runners.cache.gcs]

您现在可以在作业脚本中使用 docker。请注意包含 docker:20.10.16-dind 服务:

image: docker:20.10.16

variables:
  # When using dind service, you must instruct docker to talk with the
  # daemon started inside of the service. The daemon is available with
  # a network connection instead of the default /var/run/docker.sock socket.
  #
  # The 'docker' hostname is the alias of the service container as described at
  # https://docs.gitlab.com/ee/ci/docker/using_docker_images.html#accessing-the-services
  #
  # If you're using GitLab Runner 12.7 or earlier with the Kubernetes executor and Kubernetes 1.6 or earlier,
  # the variable must be set to tcp://localhost:2375 because of how the
  # Kubernetes executor connects services to the job container
  # DOCKER_HOST: tcp://localhost:2375
  #
  DOCKER_HOST: tcp://docker:2375
  #
  # This instructs Docker not to start over TLS.
  DOCKER_TLS_CERTDIR: ""

services:
  - docker:20.10.16-dind

before_script:
  - docker info

build:
  stage: build
  script:
    - docker build -t my-docker-image .
    - docker run my-docker-image /script/to/run/tests

将 Kubernetes executor 与 Docker-in-Docker 一起使用

您可以使用 Kubernetes executor 在 Docker 容器中运行作业。

在 Kubernetes 中启用了 TLS 的 Docker-in-Docker

要在 Kubernetes 中启用 TLS 的情况下使用 Docker-in-Docker:

  1. 使用 Helm chart,更新 values.yml 文件指定卷安装。

    runners:
      config: |
        [[runners]]
          [runners.kubernetes]
            image = "ubuntu:20.04"
            privileged = true
          [[runners.kubernetes.volumes.empty_dir]]
            name = "docker-certs"
            mount_path = "/certs/client"
            medium = "Memory"
    
  2. 您现在可以在作业脚本中使用 docker。注意包含了 docker:19.03.13-dind 服务:

    image: docker:19.03.13
    
    variables:
      # When using dind service, you must instruct Docker to talk with
      # the daemon started inside of the service. The daemon is available
      # with a network connection instead of the default
      # /var/run/docker.sock socket.
      DOCKER_HOST: tcp://docker:2376
      #
      # The 'docker' hostname is the alias of the service container as described at
      # https://docs.gitlab.com/ee/ci/services/#accessing-the-services.
      # If you're using GitLab Runner 12.7 or earlier with the Kubernetes executor and Kubernetes 1.6 or earlier,
      # the variable must be set to tcp://localhost:2376 because of how the
      # Kubernetes executor connects services to the job container
      # DOCKER_HOST: tcp://localhost:2376
      #
      # Specify to Docker where to create the certificates. Docker
      # creates them automatically on boot, and creates
      # `/certs/client` to share between the service and job
      # container, thanks to volume mount from config.toml
      DOCKER_TLS_CERTDIR: "/certs"
      # These are usually specified by the entrypoint, however the
      # Kubernetes executor doesn't run entrypoints
      # https://gitlab.com/gitlab-org/gitlab-runner/-/issues/4125
      DOCKER_TLS_VERIFY: 1
      DOCKER_CERT_PATH: "$DOCKER_TLS_CERTDIR/client"
    
    services:
      - docker:19.03.13-dind
    
    before_script:
      - docker info
    
    build:
      stage: build
      script:
        - docker build -t my-docker-image .
        - docker run my-docker-image /script/to/run/tests
    

Docker-in-Docker 限制

Docker-in-Docker 是推荐的配置,但并非没有挑战:

  • docker-compose 命令:默认情况下,此命令在此配置中不可用。要在您的作业脚本中使用 docker-compose,请遵循 docker-compose 安装说明
  • 缓存:每个作业都在新环境中运行。并发作业工作正常,因为每个构建都有自己的 Docker engine 实例,并且它们不会相互冲突。但是,作业可能会变慢,因为没有层缓存。
  • 存储驱动程序:默认情况下,早期版本的 Docker 使用 vfs 存储驱动程序,它为每个作业复制文件系统。Docker 17.09 及更高版本使用 --storage-driver overlay2,这是推荐的存储驱动程序。有关详细信息,请参阅使用 OverlayFS 驱动程序
  • 根文件系统:因为 docker:20.10.16-dind 容器和 runner 容器不共享它们的根文件系统,您可以使用作业的工作目录作为子容器的挂载点。例如,如果您想与子容器共享文件,您可以在 /builds/$CI_PROJECT_PATH 下创建一个子目录并将其用作挂载点。

    variables:
      MOUNT_POINT: /builds/$CI_PROJECT_PATH/mnt
    script:
      - mkdir -p "$MOUNT_POINT"
      - docker run -v "$MOUNT_POINT:/mnt" my-docker-image
    

使用 Docker 套接字绑定

要在 CI/CD 作业中使用 Docker 命令,您可以将 /var/run/docker.sock 绑定挂载到容器中。然后 Docker 在镜像的上下文中可用。

note如果你绑定了 Docker 套接字并且使用 GitLab Runner 11.11 或更高版本,就不能再使用 docker:19.03 .12-dind 作为服务。服务进行了卷绑定,使这些服务不兼容。

将 Docker executor 与 Docker 套接字绑定一起使用

要使 Docker 在镜像上下文中可用,您需要将 /var/run/docker.sock 挂载到启动的容器中。要使用 Docker executor 执行此操作,您需要将 "/var/run/docker.sock:/var/run/docker.sock" 添加到 [runners.docker] 部分的卷中。

您的配置应如下所示:

[[runners]]
  url = "https://gitlab.com/"
  token = RUNNER_TOKEN
  executor = "docker"
  [runners.docker]
    tls_verify = false
    image = "docker:20.10.16"
    privileged = false
    disable_cache = false
    volumes = ["/var/run/docker.sock:/var/run/docker.sock", "/cache"]
  [runners.cache]
    Insecure = false

您也可以在注册 runner 时通过提供以下选项来执行此操作:

sudo gitlab-runner register -n \
  --url https://gitlab.com/ \
  --registration-token REGISTRATION_TOKEN \
  --executor docker \
  --description "My Docker Runner" \
  --docker-image "docker:20.10.16" \
  --docker-volumes /var/run/docker.sock:/var/run/docker.sock

docker:dind 服务启用镜像库镜像

当 Docker 守护进程在服务容器内启动时,它使用默认配置。您可能需要配置镜像库镜像 以提高性能,并确保您不会达到 Docker Hub 速率限制。

.gitlab-ci.yml 文件中的服务

您可以将额外的 CLI 标志附加到 dind 服务以设置镜像库镜像:

services:
  - name: docker:19.03.13-dind
    command: ["--registry-mirror", "https://registry-mirror.example.com"]  # Specify the registry mirror to use
GitLab Runner 配置文件中的服务

如果您是 GitLab Runner 管理员,则可以指定 command 来为 Docker 守护程序配置镜像库镜像。必须为 Docker 或 Kubernetes executor 定义 dind 服务。

Docker:

[[runners]]
  ...
  executor = "docker"
  [runners.docker]
    ...
    privileged = true
    [[runners.docker.services]]
      name = "docker:19.03.13-dind"
      command = ["--registry-mirror", "https://registry-mirror.example.com"]

Kubernetes:

[[runners]]
  ...
  name = "kubernetes"
  [runners.kubernetes]
    ...
    privileged = true
    [[runners.kubernetes.services]]
      name = "docker:19.03.13-dind"
      command = ["--registry-mirror", "https://registry-mirror.example.com"]
GitLab Runner 配置文件中的 Docker executor

如果您是 GitLab Runner 管理员,您可以为每个 dind 服务使用镜像。更新配置以指定卷挂载。

例如,如果您有一个包含以下内容的 /opt/docker/daemon.json 文件:

{
  "registry-mirrors": [
    "https://registry-mirror.example.com"
  ]
}

更新 config.toml 文件,将文件挂载到 /etc/docker/daemon.json。这将为 GitLab Runner 创建的每个容器挂载文件。配置由 dind 服务获取。

[[runners]]
  ...
  executor = "docker"
  [runners.docker]
    image = "alpine:3.12"
    privileged = true
    volumes = ["/opt/docker/daemon.json:/etc/docker/daemon.json:ro"]
GitLab Runner 配置文件中的 Kubernetes executor

如果您是 GitLab Runner 管理员,您可以为每个 dind 服务使用镜像。更新配置以指定 ConfigMap 卷挂载。

例如,如果您有一个包含以下内容的 /tmp/daemon.json 文件:

{
  "registry-mirrors": [
    "https://registry-mirror.example.com"
  ]
}

使用该文件的内容创建一个 ConfigMap。您可以使用以下命令执行此操作:

kubectl create configmap docker-daemon --namespace gitlab-runner --from-file /tmp/daemon.json
note确保使用 GitLab Runner 的 Kubernetes executor,用于在其中创建作业 Pod 的命名空间。

创建 ConfigMap 后,您可以更新 config.toml 文件,将文件挂载到 /etc/docker/daemon.json。此更新为 GitLab Runner 创建的每个容器挂载文件。 配置由 dind 服务获取。

[[runners]]
  ...
  executor = "kubernetes"
  [runners.kubernetes]
    image = "alpine:3.12"
    privileged = true
    [[runners.kubernetes.volumes.config_map]]
      name = "docker-daemon"
      mount_path = "/etc/docker/daemon.json"
      sub_path = "daemon.json"
Docker 套接字绑定的限制

使用 Docker 套接字绑定时,可以避免在特权模式下运行 Docker。但是,这种方法:

  • 通过共享 Docker 守护进程,您可以有效地禁用容器的所有安全机制,并使您的主机暴露在权限提升中,这可能导致 container breakout。例如,如果一个项目运行 docker rm -f $(docker ps -a -q),它将删除 GitLab Runner 容器。
  • 并发作业可能不起作用;如果您的测试创建具有特定名称的容器,它们可能会相互冲突。
  • 任何由 Docker 命令生成的容器都是 runner 的 siblings,而不是 runner 的子节点。这可能具有不适合您的工作流程的复杂性和限制。
  • 将源仓库中的文件和目录共享到容器中可能无法按预期工作。卷挂载是在 host machine context 中完成的,而不是构建容器。例如:

     docker run --rm -t -i -v $(pwd)/src:/home/app/src test-image:latest run_app_tests
    

您不需要像使用 Docker-in-Docker executor 时那样包含 docker:20.10.16-dind 服务:

image: docker:20.10.16

before_script:
  - docker info

build:
  stage: build
  script:
    - docker build -t my-docker-image .
    - docker run my-docker-image /script/to/run/tests

使用 Docker-in-Docker 中的镜像库进行身份验证

当您使用 Docker-in-Docker 时,标准身份验证方法不起作用,因为新的 Docker 守护进程随服务一起启动。

选项 1:运行 docker login

before_script 中,运行 docker login

image: docker:19.03.13

variables:
  DOCKER_TLS_CERTDIR: "/certs"

services:
  - docker:19.03.13-dind

build:
  stage: build
  before_script:
    - echo "$DOCKER_REGISTRY_PASS" | docker login $DOCKER_REGISTRY --username $DOCKER_REGISTRY_USER --password-stdin
  script:
    - docker build -t my-docker-image .
    - docker run my-docker-image /script/to/run/tests

要登录 Docker Hub,请将 $DOCKER_REGISTRY 留空或将其删除。

选项 2:在每个作业上挂载 ~/.docker/config.json

如果您是 GitLab Runner 的管理员,您可以将带有身份验证配置的文件挂载到 ~/.docker/config.json。 然后,runner 选择的每个作业都已经过身份验证。如果您使用的是官方 docker:19.03.13 镜像,则主目录在 /root 下。

如果您挂载配置文件,任何修改 ~/.docker/config.jsondocker 命令都会失败。例如,docker login 失败,因为文件被挂载为只读。 不要更改,否则可能会出现问题。

以下是一个遵循 DOCKER_AUTH_CONFIG 文档的 /opt/.docker/config.json 示例:

{
    "auths": {
        "https://index.docker.io/v1/": {
            "auth": "bXlfdXNlcm5hbWU6bXlfcGFzc3dvcmQ="
        }
    }
}

Docker

更新卷挂载,包含该文件。

[[runners]]
  ...
  executor = "docker"
  [runners.docker]
    ...
    privileged = true
    volumes = ["/opt/.docker/config.json:/root/.docker/config.json:ro"]

Kubernetes

使用该文件的内容创建一个 ConfigMap。 您可以使用以下命令执行此操作:

kubectl create configmap docker-client-config --namespace gitlab-runner --from-file /opt/.docker/config.json

更新卷挂载,包含该文件。

[[runners]]
  ...
  executor = "kubernetes"
  [runners.kubernetes]
    image = "alpine:3.12"
    privileged = true
    [[runners.kubernetes.volumes.config_map]]
      name = "docker-client-config"
      mount_path = "/root/.docker/config.json"
      # If you are running GitLab Runner 13.5
      # or lower you can remove this
      sub_path = "config.json"

选项 3:使用 DOCKER_AUTH_CONFIG

如果您已经定义了 DOCKER_AUTH_CONFIG,则可以使用该变量并将其保存在~/.docker/config.json 中。

有多种方法可以定义此身份验证:

  • 在 runner 配置文件中的 pre_build_script 中。
  • before_script 中。
  • script 中。

以下示例显示了 before_script。 相同的命令适用于您实施的任何解决方案。

image: docker:19.03.13

variables:
  DOCKER_TLS_CERTDIR: "/certs"

services:
  - docker:19.03.13-dind

build:
  stage: build
  before_script:
    - mkdir -p $HOME/.docker
    - echo $DOCKER_AUTH_CONFIG > $HOME/.docker/config.json
  script:
    - docker build -t my-docker-image .
    - docker run my-docker-image /script/to/run/tests

使用 Docker 层缓存使 Docker-in-Docker 构建速度更快

使用 Docker-in-Docker 时,每次创建构建时,Docker 都会下载镜像的所有层。Docker 的最新版本(Docker 1.13 及更高版本)可以在 docker build 步骤中使用预先存在的镜像作为缓存。这大大加快了构建过程。

Docker 缓存工作原理

当运行 docker build 时,Dockerfile 中的每个命令都会生成一个层。这些层作为缓存保留,如果没有任何更改,可以重复使用。更改一层会导致重新创建所有后续层。

您可以使用 --cache-from 参数指定要用作 docker build 命令的缓存源的标签镜像。可以使用多个 --cache-from 参数将多个镜像指定为缓存源。任何与 --cache-from 参数一起使用的镜像必须首先被拉取(使用 docker pull),然后才能用作缓存源。

Docker 缓存示例

这是一个 .gitlab-ci.yml 文件,展示了如何使用 Docker 缓存:

image: docker:20.10.16

services:
  - docker:20.10.16-dind

variables:
  # Use TLS https://docs.gitlab.com/ee/ci/docker/using_docker_build.html#tls-enabled
  DOCKER_HOST: tcp://docker:2376
  DOCKER_TLS_CERTDIR: "/certs"

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

build 阶段的 script 部分:

  1. 第一个命令尝试从镜像库中拉取镜像,以便它可以用作 docker build 命令的缓存。
  2. 如果可用,第二个命令通过使用拉取的镜像作为缓存来构建 Docker 镜像(参见 --cache-from $CI_REGISTRY_IMAGE:latest 参数),并标记它。
  3. 最后两个命令将标记的 Docker 镜像推送到容器镜像库,以便它们也可以用作后续构建的缓存。

使用 OverlayFS 驱动程序

默认情况下,当使用 docker:dind 时,Docker 使用 vfs 存储驱动程序,它会在每次运行时复制文件系统。这是一个磁盘密集型操作,如果使用不同的驱动程序可以避免,例如 overlay2

要求

  1. 确保使用最新的内核,最好是 >= 4.2
  2. 检查是否加载了 overlay 模块:

    sudo lsmod | grep overlay
    

    如果看不到结果,则说明未加载。要加载它,请使用:

    sudo modprobe overlay
    

    如果一切顺利,您需要确保在重新启动时加载模块。在 Ubuntu 系统上,这是通过编辑 /etc/modules 来完成的。只需在其中添加以下行:

    overlay
    

每个项目使用 OverlayFS 驱动程序

您可以通过使用 .gitlab-ci.yml 中的 DOCKER_DRIVER CI/CD 变量 为每个项目单独启用驱动程序:

variables:
  DOCKER_DRIVER: overlay2

为每个项目使用 OverlayFS 驱动程序

如果您使用自己的 runners,则可以通过在 [[runners]] 部分中设置 DOCKER_DRIVER 环境变量来为每个项目启用驱动程序 config.toml 文件:

environment = ["DOCKER_DRIVER=overlay2"]

如果您正在运行多个 runner,则必须修改所有配置文件。

使用 GitLab Container Registry

构建 Docker 镜像后,您可以将其推送到内置的 GitLab Container Registry。

故障排查

docker: Cannot connect to the Docker daemon at tcp://docker:2375. Is the docker daemon running?

当您使用 Docker-in-Docker v19.03 或更高版本时,这是一个常见错误。

出现此问题是因为 Docker 自动在 TLS 上启动。

Docker no such host 错误

您可能会收到一条错误消息:docker: error during connect: Post https://docker:2376/v1.40/containers/create: dial tcp: lookup docker on x.x.x.x:53: no such host

当服务的镜像名称包括镜像库主机名时,可能会发生此问题。例如:

image: docker:20.10.16

services:
  - registry.hub.docker.com/library/docker:20.10.16-dind

服务的主机名是派生自完整镜像名称。 然而,预期是较短的服务主机名 docker。 要允许服务解析和访问,请为服务名称 docker 添加显式别名:

image: docker:20.10.16

services:
  - name: registry.hub.docker.com/library/docker:20.10.16-dind
    alias: docker