Help us learn about your current experience with the documentation. Take the survey.

使用 Docker 构建 Docker 镜像

  • Tier: Free, Premium, Ultimate
  • Offering: GitLab.com, GitLab Self-Managed, GitLab Dedicated

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

要在 CI/CD 作业中运行 Docker 命令,您必须配置 GitLab Runner 以支持 docker 命令。此方法需要 privileged 模式。

如果您想在 Runner 上不启用 privileged 模式的情况下构建 Docker 镜像, 可以使用 Docker 替代方案

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

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

使用 Shell 执行器

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

  1. 安装 GitLab Runner。

  2. 注册 一个 Runner。 选择 shell 执行器。例如:

    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 以验证 Docker 是否正常工作:

    default:
      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) 表示:

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

您应该使用启用 TLS 的 Docker-in-Docker, 这受到 GitLab.com 实例 Runner 的支持。

您应该始终固定镜像的特定版本,例如 docker:24.0.5。 如果您使用 docker:latest 这样的标签,您无法控制使用的版本。 这在新版本发布时可能会导致兼容性问题。

使用 Docker 执行器与 Docker-in-Docker

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

Docker 执行器中启用 TLS 的 Docker-in-Docker

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

此任务启用 --docker-privileged,这实际上禁用了容器的安全机制,并使您的主机面临权限 提升的风险。此操作可能导致容器逃逸。有关更多信息,请参阅 运行时权限和 Linux 功能

要使用启用 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:24.0.5" \
      --docker-privileged \
      --docker-volumes "/certs/client"
    • 此命令注册一个新的 Runner 来使用 docker:24.0.5 镜像(如果在作业级别未指定)。 它使用 privileged 模式来启动构建和服务容器。 如果您想使用 Docker-in-Docker, 您必须在 Docker 容器中始终使用 privileged = true
    • 此命令为服务容器和构建容器挂载 /certs/client, 这是 Docker 客户端使用该目录中证书所必需的。有关更多信息,请参阅 Docker 镜像文档

    前面的命令创建一个类似于以下示例的 config.toml 条目:

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

    default:
      image: docker:24.0.5
      services:
        - docker:24.0.5-dind
      before_script:
        - docker info
    
    variables:
      # 当您使用 dind 服务时,您必须指示 Docker 与
      # 在服务内部启动的守护进程通信。守护进程通过
      # 网络连接而不是默认的 /var/run/docker.sock 套接字可用。
      # Docker 19.03 会自动执行此操作
      # 通过设置 DOCKER_HOST 在
      # https://github.com/docker-library/docker/blob/d45051476babc297257df490d22cbd806f1b11e4/19.03/docker-entrypoint.sh#L23-L29
      #
      # 'docker' 主机是服务容器的别名,如
      # https://docs.gitlab.com/ee/ci/services/#accessing-the-services 中所述。
      #
      # 指定 Docker 创建证书的位置。Docker
      # 在启动时自动创建它们,并创建
      # `/certs/client` 以在服务和作业容器之间共享,
      # 这要归功于 config.toml 中的卷挂载
      DOCKER_TLS_CERTDIR: "/certs"
    
    build:
      stage: build
      script:
        - docker build -t my-docker-image .
        - docker run my-docker-image /script/to/run/tests
在 Docker-in-Docker 和构建容器之间使用共享卷上的 Unix 套接字

Docker 执行器中启用 TLS 的 Docker-in-Docker 方法中,volumes = ["/certs/client", "/cache"] 中定义的目录是 在构建之间持久化的。 如果多个使用 Docker 执行器 Runner 的 CI/CD 作业启用了 Docker-in-Docker 服务,则每个作业 都会写入目录路径。这种方法可能会导致冲突。

要解决此冲突,请在 Docker-in-Docker 服务和构建容器之间使用共享卷上的 Unix 套接字。 这种方法提高了性能,并在服务和客户端之间建立了安全连接。

以下是构建容器和服务容器之间临时共享的示例 config.toml

[[runners]]
  url = "https://gitlab.com/"
  token = TOKEN
  executor = "docker"
  [runners.docker]
    image = "docker:24.0.5"
    privileged = true
    volumes = ["/runner/services/docker"] # 构建和服务容器之间临时共享的卷。

Docker-in-Docker 服务创建一个 docker.sock。Docker 客户端通过 Docker Unix 套接字卷连接到 docker.sock

job:
  variables:
    # 此变量由 DinD 服务和 Docker 客户端共享。
    # 对于服务,它将指示 DinD 在此处创建 `docker.sock`。
    # 对于客户端,它告诉 Docker 客户端连接到哪个 Docker Unix 套接字。
    DOCKER_HOST: "unix:///runner/services/docker/docker.sock"
  services:
    - docker:24.0.5-dind
  image: docker:24.0.5
  script:
    - docker version
Docker 执行器中禁用 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:24.0.5"
    privileged = true
    disable_cache = false
    volumes = ["/cache"]
  [runners.cache]
    [runners.cache.s3]
    [runners.cache.gcs]

您现在可以在作业脚本中使用 docker。您应该包含 docker:24.0.5-dind 服务:

default:
  image: docker:24.0.5
  services:
    - docker:24.0.5-dind
  before_script:
    - docker info

variables:
  # 使用 dind 服务时,您必须指示 docker 与
  # 在服务内部启动的守护进程通信。守护进程通过
  # 网络连接而不是默认的 /var/run/docker.sock 套接字可用。
  #
  # 'docker' 主机是服务容器的别名,如
  # https://docs.gitlab.com/ee/ci/docker/using_docker_images.html#accessing-the-services 中所述
  #
  DOCKER_HOST: tcp://docker:2375
  #
  # 这指示 Docker 不要通过 TLS 启动。
  DOCKER_TLS_CERTDIR: ""

build:
  stage: build
  script:
    - docker build -t my-docker-image .
    - docker run my-docker-image /script/to/run/tests
Docker 执行器中启用代理的 Docker-in-Docker

您可能需要配置代理设置才能使用 docker push 命令。

有关更多信息,请参阅 使用 dind 服务时的代理设置

使用 Kubernetes 执行器与 Docker-in-Docker

您可以使用 Kubernetes 执行器 在 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:24.0.5-dind 服务:

    default:
      image: docker:24.0.5
      services:
        - name: docker:24.0.5-dind
          variables:
            HEALTHCHECK_TCP_PORT: "2376"
      before_script:
        - docker info
    
    variables:
      # 使用 dind 服务时,您必须指示 Docker 与
      # 在服务内部启动的守护进程通信。守护进程通过
      # 网络连接而不是默认的 /var/run/docker.sock 套接字可用。
      DOCKER_HOST: tcp://docker:2376
      #
      # 'docker' 主机是服务容器的别名,如
      # https://docs.gitlab.com/ee/ci/services/#accessing-the-services 中所述。
      #
      # 指定 Docker 创建证书的位置。Docker
      # 在启动时自动创建它们,并创建
      # `/certs/client` 以在服务和作业容器之间共享,
      # 这要归功于 config.toml 中的卷挂载
      DOCKER_TLS_CERTDIR: "/certs"
      # 这些通常由入口点指定,但是
      # Kubernetes 执行器不运行入口点
      # https://gitlab.com/gitlab-org/gitlab-runner/-/issues/4125
      DOCKER_TLS_VERIFY: 1
      DOCKER_CERT_PATH: "$DOCKER_TLS_CERTDIR/client"
    
    build:
      stage: build
      script:
        - docker build -t my-docker-image .
        - docker run my-docker-image /script/to/run/tests
Kubernetes 中禁用 TLS 的 Docker-in-Docker

要在 Kubernetes 中使用禁用 TLS 的 Docker-in-Docker,您必须将前面的示例调整为:

  • values.yml 文件中删除 [[runners.kubernetes.volumes.empty_dir]] 部分。
  • 使用 DOCKER_HOST: tcp://docker:2375 将端口从 2376 更改为 2375
  • 使用 DOCKER_TLS_CERTDIR: "" 指示 Docker 启动时禁用 TLS。

例如:

  1. 使用 Helm chart,更新 values.yml 文件

    runners:
      config: |
        [[runners]]
          [runners.kubernetes]
            image = "ubuntu:20.04"
            privileged = true
  2. 您现在可以在作业脚本中使用 docker。您应该包含 docker:24.0.5-dind 服务:

    default:
      image: docker:24.0.5
      services:
        - name: docker:24.0.5-dind
          variables:
            HEALTHCHECK_TCP_PORT: "2375"
      before_script:
        - docker info
    
    variables:
      # 使用 dind 服务时,您必须指示 Docker 与
      # 在服务内部启动的守护进程通信。守护进程通过
      # 网络连接而不是默认的 /var/run/docker.sock 套接字可用。
      DOCKER_HOST: tcp://docker:2375
      #
      # 'docker' 主机是服务容器的别名,如
      # https://docs.gitlab.com/ee/ci/services/#accessing-the-services 中所述。
      #
      # 这指示 Docker 不要通过 TLS 启动。
      DOCKER_TLS_CERTDIR: ""
    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 实例,所以并发作业不会导致冲突。 但是,由于没有层缓存,作业可能会变慢。请参阅 Docker 层缓存

  • 存储驱动:默认情况下,早期版本的 Docker 使用 vfs 存储驱动, 它为每个作业复制文件系统。Docker 17.09 及更高版本使用 --storage-driver overlay2,这是 推荐的存储驱动。有关详细信息,请参阅 使用 OverlayFS 驱动

  • 根文件系统:由于 docker:24.0.5-dind 容器和 Runner 容器不共享其 根文件系统,您可以使用作业的工作目录作为 子容器的挂载点。例如,如果您有要与 子容器共享的文件,您可以在 /builds/$CI_PROJECT_PATH 下创建 一个子目录,并将其用作挂载点。有关更详细的解释,请参阅 问题 #41227

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

使用 Docker 执行器与 Docker socket 绑定

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

如果您绑定 Docker 套接字,您不能使用 docker:24.0.5-dind 作为服务。卷绑定也会影响服务, 使它们不兼容。

要在镜像上下文中使 Docker 可用,您需要挂载 /var/run/docker.sock 到启动的容器中。要使用 Docker 执行器执行此操作,将 "/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:24.0.5"
    privileged = false
    disable_cache = false
    volumes = ["/var/run/docker.sock:/var/run/docker.sock", "/cache"]
  [runners.cache]
    Insecure = false

要在注册 Runner 时挂载 /var/run/docker.sock,包含以下选项:

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

对于 docker-windows 执行器,使用类似于以下示例的配置:

[[runners]]
  url = "https://gitlab.example.com/"
  token = RUNNER_TOKEN
  executor = "docker-windows"
  [runners.docker]
    tls_verify = false
    image = "docker:windowsservercore-ltsc2022"
    privileged = false
    disable_cache = false
    volumes = ["//./pipe/docker_engine://./pipe/docker_engine", "/cache"]
  [runners.cache]
    Insecure = false

对于像 使用 CodeQuality 进行代码质量扫描 这样的复杂 Docker-in-Docker 设置,您必须匹配主机和容器路径才能正确执行。有关更多详细信息,请参阅 使用私有 Runner 进行基于 CodeClimate 的扫描

docker:dind 服务启用注册表镜像

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

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

您可以向 dind 服务添加额外的 CLI 标志来设置注册表 镜像:

services:
  - name: docker:24.0.5-dind
    command: ["--registry-mirror", "https://registry-mirror.example.com"]  # 指定要使用的注册表镜像
GitLab Runner 配置文件中的服务

如果您是 GitLab Runner 管理员,您可以指定 command 来配置 Docker 守护进程的注册表镜像。 dind 服务必须为 DockerKubernetes 执行器 定义。

Docker:

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

Kubernetes:

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

如果您是 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 执行器

如果您是 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

您必须使用 Kubernetes 执行器用于创建作业 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 socket 绑定的已知问题

当您使用 Docker socket 绑定时,您避免了在特权模式下运行 Docker。但是, 此方法的含义是:

  • 通过共享 Docker 守护进程,您实际上禁用了所有 容器的安全机制,并使您的主机面临权限 提升的风险。这可能导致容器逃逸。例如,如果某个项目 运行了 docker rm -f $(docker ps -a -q),它将删除 GitLab Runner 容器。

  • 并发作业可能无法工作。如果您的测试 创建具有特定名称的容器,它们可能会相互冲突。

  • 任何由 Docker 命令创建的容器都是 Runner 的兄弟, 而不是 Runner 的子级。这可能会给您的 workflow 带来复杂性。

  • 从源代码仓库将文件和目录共享到容器中可能无法 按预期工作。卷挂载是在主机 上下文中完成的,而不是在构建容器中。例如:

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

您不需要包含 docker:24.0.5-dind 服务,就像您使用 Docker-in-Docker 执行器时那样:

default:
  image: docker:24.0.5
  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 守护进程。您应该注册表认证

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

使用 Docker-in-Docker 时,Docker 每次创建构建时都会下载镜像的所有层。您可以通过 使用 Docker 层缓存使构建更快

使用 OverlayFS 驱动

GitLab.com 上的实例 Runner 默认使用 overlay2 驱动。

默认情况下,当使用 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, 您可以通过在 config.toml 文件的 [[runners]] 部分 中设置 DOCKER_DRIVER 环境变量为每个项目启用驱动:

environment = ["DOCKER_DRIVER=overlay2"]

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

有关更多信息,请参阅 runner 配置使用 OverlayFS 存储驱动

Docker 替代方案

您可以在不启用 Runner 上的特权模式的情况下构建容器镜像:

  • BuildKit:包含无根 BuildKit 选项,消除了 Docker 守护进程依赖。
  • Buildah:无需 Docker 守护进程即可构建 OCI 兼容镜像。

Buildah 示例

要将 Buildah 与 GitLab CI/CD 一起使用,您需要一个 runner, 它具有以下执行器之一:

在此示例中,您使用 Buildah 来:

  1. 构建 Docker 镜像。
  2. 将其推送到 GitLab 容器注册表

在最后一步,Buildah 使用项目根目录下的 Dockerfile 构建 Docker 镜像。最后,它将镜像推送到 项目的容器注册表:

build:
  stage: build
  image: quay.io/buildah/stable
  variables:
    # 使用 buildah 的 vfs。Docker 默认提供 overlayfs,但 Buildah
    # 无法在另一个 overlayfs 文件系统上堆叠 overlayfs。
    STORAGE_DRIVER: vfs
    # 以 docker 格式而不是标准 OCI 格式写入所有镜像元数据。
    # 较新版本的 docker 可以处理 OCI 格式,但较旧版本,如
    # Fedora 30 附带的版本,无法处理该格式。
    BUILDAH_FORMAT: docker
    FQ_IMAGE_NAME: "$CI_REGISTRY_IMAGE/test"
  before_script:
    # 从[预定义的 CI/CD 变量](../variables/_index.md#predefined-cicd-variables)
    # 中获取 GitLab 容器注册表凭据以注册到注册表。
    - echo "$CI_REGISTRY_PASSWORD" | buildah login -u "$CI_REGISTRY_USER" --password-stdin $CI_REGISTRY
  script:
    - buildah images
    - buildah build -t $FQ_IMAGE_NAME
    - buildah images
    - buildah push $FQ_IMAGE_NAME

如果您使用部署到 OpenShift 集群的 GitLab Runner Operator,请尝试 使用无根容器构建镜像的 Buildah 教程

使用 GitLab 容器注册表

构建 Docker 镜像后,您可以将其推送到 GitLab 容器注册表