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。
本文涉及多个系统,为避免混淆,统一使用以下术语:
- 宿主机(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-in-Docker
可以把 Forgejo Runner 配置为通过 Docker-in-Docker(简称 DinD;原配置说明中写作 DIND)让 Action 访问 Docker。方法是在宿主 Docker daemon 上运行一个单独的 DinD 容器,由该容器提供自己的隔离 Docker daemon。这样,对 Docker 的访问会转向 DinD 容器,在那里完成操作。
这种配置会阻止作业容器访问宿主机的 Docker 资源,因为这些资源在作业容器所连接的环境中不可见。
配置
这种配置分为三个步骤:
- 运行一个 Docker-in-Docker 容器。
- 配置 Runner,使它在启动作业、服务和步骤容器时访问 DinD 容器。
- 给这些容器设置
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 共享同一个 DinDcontainer.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。
配置
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 的能力,安全细节见下文管理员说明。
- Runner 同样先尝试
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 访问的要求。
把 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。











暂无评论内容