Utilizing Docker within Actions

Forgejo Runner 使用容器环境运行 Action,但要让 Action 本身也能访问容器环境,还需要额外配置。

例如,下面这样一个简单的 Forgejo 工作流:

name: Docker Build

on:
  push:
    branches: [main]

jobs:
  build:
    runs-on: docker

    steps:
      - uses: actions/checkout@v6
      - run: docker build -t my-app .

通常会出现这样的错误:

Cannot connect to the Docker daemon at unix:///var/run/docker.sock. Is the docker daemon running?

为什么不能直接工作?

Forgejo Runner 执行的作业容器,无法访问 Docker 套接字。

Runner 收到作业后,会创建一个用于执行该作业的容器,即作业容器。其配置通常由 Runner 的标签定义,也可以通过作业中的 jobs.<job_id>.container.image 及相关属性配置。

工作流的每一步都在这个作业容器内执行。没有特殊配置时,在其中运行 docker build,会尝试通过默认 UNIX 套接字 /var/run/docker.sock 连接一个正在运行的 Docker daemon。这次访问会失败:虽然作业容器本身由 Docker 运行和管理,但容器内部并没有可用的 Docker daemon。

Runner 创建作业容器后,容器内 Docker CLI 尝试打开不存在的 docker.sock,构建失败。
图 1:Runner 向宿主 Docker 创建容器并在其中执行命令,但容器里的 Docker CLI 无法连接默认套接字。原图中的 abcdef 是示例容器标识。

本文涉及多个系统,为避免混淆,统一使用以下术语:

  • 宿主机(Host):运行 Forgejo Runner 进程的机器。
  • 作业容器(Job container):执行作业时,例如前面工作流的 build 部分,Runner 通常通过 Docker、Podman 或 LXC 等容器环境启动作业容器,并在其中运行作业命令。Runner 也能通过 ...:host 标签直接在宿主机执行命令;这种配置不受本文问题影响,不在讨论范围内。
  • 服务容器(Service container):有些作业定义额外容器,让它们与作业容器同时启动。例如,为数据库集成测试启动 MySQL 或 PostgreSQL。服务容器在工作流文件的 jobs.<job_id>.services 部分定义。

安全方面的考虑

允许 Forgejo Actions 工作流访问 Docker daemon,会增加 Forgejo Runner 的安全配置复杂度。管理员选择配置时,需要谨慎确认:授予对某个 Docker 系统的访问,是否也意外授予了对同一系统中其他资源的访问。

例如,假设 Forgejo 和 Runner 使用同一个 Docker daemon,直接开放对该 daemon 的访问,就可能让执行中的 Action 关闭 Forgejo 实例,或者执行更危险的操作,例如创建能够无限制访问实例的管理员账户。管理员可能愿意接受这种风险,也可能希望阻止这种访问。

作业容器访问宿主 Docker 后,可以检查其他容器并在 Forgejo 容器内执行命令。
图 2:共享 Docker daemon 意味着工作流可能用 docker ps 查看其他容器,用 docker inspect 读取其环境变量,用 docker exec 在其中执行命令。

解决方案

Docker-in-Docker

可以把 Forgejo Runner 配置为通过 Docker-in-Docker(简称 DinD;原配置说明中写作 DIND)让 Action 访问 Docker。方法是在宿主 Docker daemon 上运行一个单独的 DinD 容器,由该容器提供自己的隔离 Docker daemon。这样,对 Docker 的访问会转向 DinD 容器,在那里完成操作。

Runner 和作业容器使用 DinD daemon,镜像构建在 DinD 环境中完成。
图 3:Runner 让 DinD 创建作业容器;容器中的 docker build 交由 DinD 处理,构建结果返回工作流。

这种配置会阻止作业容器访问宿主机的 Docker 资源,因为这些资源在作业容器所连接的环境中不可见。

DinD 中的作业尝试停止宿主机 Forgejo 容器,因为 DinD 没有该容器而失败。
图 4:在 DinD 中执行 docker kill forgejo 时,DinD daemon 找不到宿主 Docker 中的 Forgejo 容器,因此无法停止它。

配置

这种配置分为三个步骤:

  1. 运行一个 Docker-in-Docker 容器。
  2. 配置 Runner,使它在启动作业、服务和步骤容器时访问 DinD 容器。
  3. 给这些容器设置 DOCKER_HOST 环境变量,让容器内的命令也能访问 DinD 容器。

下面是一个配置示例。每步具体如何应用,会随 Runner 的安装方式和现有配置而变化。

第一步,用 docker run 基于 docker:dind 镜像创建容器,并通过本地网络端口 2376 提供访问:

docker run \
  -p 127.0.0.1:2376:2375 \
  --detach \
  --privileged \
  --restart always \
  --name dind_container \
  docker:dind \
  dockerd -H tcp://0.0.0.0:2375 --tls=false

这会创建一个可以通过 dind_container:2375 和 127.0.0.1:2376 访问的 DinD 实例,它没有加密(--tls=false),也没有访问控制。宿主机可以访问这个实例,局域网其他机器不能通过这项端口绑定访问它;如果宿主机上还有不可信用户,这种设置可能不合适。

第二、三步修改 Runner 配置文件,细节见代码中的原始注释:

runner:
  # ... skipping other configuration values you may have ...
  envs:
    # Step 3 (Part 1):
    # A container will be setup for each job, and each step will
    # be executed in it.
    # This DOCKER_HOST environment variable will be used at each exec
    # in that container, like docker exec -e DOCKER_HOST=tcp://dind_container.docker.internal:2375 <command>
    # When you run the docker CLI or related
    # commands, they'll find this variable and reach out to the DIND
    # container to perform work there.
    DOCKER_HOST: tcp://dind_container.docker.internal:2375
  # ... skipping ...

container:
  # ... skipping ...

  # Step 2:
  # Use the Docker-in-Docker host to run any job, service, and step
  # containers. You can get a pretty functional step if you skip this
  # step, as containers will still be able to access docker through
  # the `runner.envs.DOCKER_HOST` setting -- but containers created
  # by a job won't be able to access services. For example, having a
  # `postgresql` service in the job, and then invoking `docker run -e
  # DB_HOST=postgresql some-app:testing`, the two containers will both
  # be in the Docker-in-Docker environment and will communicate freely.
  docker_host: 'tcp://127.0.0.1:2376'
  #
  # Step 3 (Part 2):
  # DNS resolve `dind_container` in the context of the container's
  # host (the DIND container) to allow tcp://... to find it.
  options: '--add-host=dind_container.docker.internal:host-gateway'
  # ... skipping ...

如果使用的是 Runner 安装指南中的 OCI 镜像安装配置,可以将 runner.envs.DOCKER_HOST 指向指南中已经存在的同一个 DinD 容器。另有完整的 docker-compose.yml 示例,会写入合适的配置文件,并更新 forgejo-runner daemon,使其使用该配置。

管理员需要注意什么?

DinD 配置相对容易设置,通过阻止工作流访问宿主 Docker 系统中的其他资源,提供一定隔离。不过,管理员仍应注意:

  • 如果 Runner 的 runner.capacity 大于 1,允许多个任务并发,或者多个 Runner 共享同一个 DinD container.docker_host,作业容器就能通过 Docker CLI 看见并操作彼此。一个容器可能读取另一个运行中作业容器的秘密,或操作其文件。
  • DinD 容器有自己的存储,位于容器内部的 /var/lib/docker。因此:
    • 宿主机已有的镜像,只有在 DinD 中拉取后,才会在 DinD 环境可用。
    • 相同镜像可能分别存储在宿主机和 DinD 中,额外占用空间。
    • 重新创建 DinD 容器,例如升级 docker:dind 镜像时,如果没有把 /var/lib/docker 映射到持久卷,该目录就会丢失。依赖镜像缓存时,这可能影响后续工作流的性能。
    • 如果把它映射到持久卷,就需要定期维护,例如使用 docker system prune,避免过时或无用的镜像、卷不断增长。
  • 工作流创建的产物,例如 docker build 创建的镜像或 docker run 创建的容器,会留在 DinD 中。同一 Runner 上未来的工作流可以检查这些产物,并可能从中提取机密信息。
  • Runner 配置的资源限制不会影响工作流 Action 创建的容器。不过,这些容器仍受外层 DinD 容器资源限制的约束。

将 container.docker_host 配置为 automount

可以配置 Runner,将宿主机的 UNIX 套接字挂载到作业容器中的 /var/run/docker.sock,从而向作业容器开放宿主 Docker daemon。

宿主 docker.sock 挂入作业容器后,容器内 Docker CLI 的操作直接交给宿主 Docker。
图 5:作业容器共享宿主 /var/run/docker.sock,Docker CLI 通过该套接字把构建请求交给宿主 daemon,并收到结果。

配置

container:
  # ... skipping ...
  #
  # The "automount" option makes Forgejo Runner automatically find the
  # appropriate docker socket in order to start job containers, and then
  # also direct new job containers to have that docker socket mounted
  # into them as `/var/run/docker.sock`.
  docker_host: 'automount'
  # ... skipping ...

这个选项的有效值,以及它们与 Action 内访问 Docker 的关系如下:

  • docker_host: "-"
    • 这是默认值。
    • Runner 尝试使用环境变量 DOCKER_HOST 连接 Docker;没有这个变量时,使用 UNIX 套接字 /var/run/docker.sock。
    • Runner 不会把套接字共享给容器。
    • 这使容器无法访问 Docker。默认采用这一行为,是为了让管理员在明确选择其他配置前,不会承担意料之外的安全风险。
  • docker_host: "automount"
    • Runner 同样先尝试 DOCKER_HOST,再回退到 UNIX 套接字 /var/run/docker.sock。
    • 如果连接的是 UNIX 套接字,Runner 会将它挂载到作业、服务和步骤容器的 /var/run/docker.sock。如果 DOCKER_HOST 是 tcp://... 地址,就无法这样共享,"automount" 的行为与 "-" 相同。
    • 在 OCI 镜像安装方式中,DOCKER_HOST 是 tcp://... 地址,因此 "automount" 不起作用。这种套接字共享方式与该安装方案不兼容。
    • 它会让容器获得访问 Docker 的能力,安全细节见下文管理员说明。
  • docker_host: "unix://..." 或 docker_host: "tcp://..."
    • 可以用另一个 URL 指定 Docker daemon。
    • 只要它是 UNIX 套接字或命名管道,Runner 就会把该套接字共享给容器。
    • 它也会让容器获得访问 Docker 的能力,安全细节见下文。

管理员需要注意什么?

这种配置最容易让作业容器访问 Docker,但没有安全隔离。Runner 自动发现的 Docker daemon——通常是宿主机 /var/run/docker.sock 指向的 daemon——会向工作流开放,所有使用该 daemon 的服务都可能被检查和修改。只有采取措施确保仅可信用户能访问 Forgejo 实例时,才适合选择这种配置。

应注意的风险包括:

  • Docker 中运行的任何容器,都可能被作业、服务和步骤容器访问。例如,Forgejo 或 Runner 如果作为容器在同一宿主机运行,作业容器就可能对其执行 docker kill、docker inspect 或 docker exec。
  • Action 可以给新容器挂载卷,从而访问宿主机存储。例如,docker run -v /:/host-mount ubuntu 会让容器访问宿主机的全部存储,包括磁盘上的秘密。
  • 工作流创建的镜像和容器会留在宿主 Docker daemon 中。以后在同一 Runner 上运行的工作流可以检查这些产物,并可能提取机密信息。
  • Runner 中配置的资源限制不会影响工作流 Action 创建的容器。
  • Runner 的 container.network 设置不会应用到工作流创建的容器。工作流可以选择 --network=host 等配置,访问宿主操作系统的原生网络,使这些容器能够接触本地网络资源,带来新的安全风险。

LXC

LXC 是轻量虚拟化方式,可以创建能够独立运行 Docker 的 Linux 容器。当 Runner 的标签配置为通过 LXC 运行作业容器时,作业容器可以提供与宿主机资源隔离的 Docker 构建环境。

Runner 通过 LXC 启动作业环境,Docker daemon 在独立 LXC 容器内运行。
图 6:Runner 通过 LXC 启动作业环境,其中的 Docker CLI 与该环境自己的 daemon 通信,构建不交给宿主 Docker。

配置

Runner 安装指南中的设置容器环境一节介绍了配置 LXC 访问的要求。

把 Runner 的某个标签配置为使用 LXC 环境,例如 bookworm-lxc:lxc://debian:bookworm 后,作业容器就会在 LXC 中运行。接下来便可以在 Action 内安装并使用 Docker:

name: Test Action

on:
  pull_request:

jobs:
  check-1:
    runs-on: bookworm-lxc
    steps:
      - run: apt update && apt --quiet install --yes -qq docker.io
      - run: docker ps -a
      - run: docker run --rm -it ubuntu echo "Hello, world!"

管理员需要注意什么?

相比 DinD 和 automount,LXC 配置提供了更好的隔离:访问作业容器中的 Docker 资源,与 Runner 宿主机及其他作业容器隔离。

LXC 作业容器也有功能缺口:使用这种运行方式时,Action 不支持服务容器。

每个 LXC 容器会创建自己的独立 Docker daemon 和存储,因此不同运行之间不共享镜像缓存。这在安全隔离方面更强,但后续工作流没有可用缓存镜像,可能影响性能。

没有一种方案能保证安全风险为零。多个 LXC 轻量虚拟环境仍共享同一个 Linux 内核。由于曾发现容器逃逸机制,建议持续安装操作系统安全补丁。

让 Forgejo Runner 运行在虚拟机中

如果 Runner 宿主机还共享其他重要或机密的服务和数据,可以考虑把 Runner 完全隔离在单独的虚拟机中。隔离后,可在虚拟机内应用前面的任一种配置,让 Action 访问 Docker 服务。由于虚拟机不能直接访问底层宿主操作系统,前面讨论的许多风险便不再属于同一范围。


来源:Utilizing Docker within Actions | Forgejo – Beyond coding. We forge.。原文作者:Forgejo authors。许可:CC-BY-SA-4.0。中文内容和版式作了调整。

Copyright © 2026 Forgejo authors. 本中文改编及所用原文图示继续采用 CC BY-SA 4.0。

© 版权声明
THE END
喜欢就支持一下吧
点赞0 分享
评论 抢沙发

请登录后发表评论

    暂无评论内容