feat: add GITEA_DOCKER_WORKSPACE and container cleanup (#1204)

1. Add `GITEA_DOCKER_WORKSPACE` which holds the workspace path as the daemon sees it, enabling `${GITEA_DOCKER_WORKSPACE:-.}/data:/app/data` in a compose file without having to resort to `bind_workdir` (which causes much more problems like breaking `actions/cache` because of unstable workspace paths).
2. Add container/network/volume cleanup for containers started within jobs, for example via `docker compose` inside a job. It works by running a lightweight docker socket proxy and injecting a `com.gitea.runner.job` label into every container creation and that label is used to remove containers started by that job at the end. Perf impact of this is near-zero.

Docs: https://gitea.com/gitea/docs/pulls/535

Assisted by Claude (Fable 5.1).
Co-authored-by: bircni <bircni@icloud.com>
Reviewed-on: https://gitea.com/gitea/runner/pulls/1204
Reviewed-by: bircni <bircni@icloud.com>
Co-authored-by: silverwind <me@silverwind.io>
This commit is contained in:
silverwind
2026-09-05 08:58:23 +00:00
committed by bircni
co-authored by bircni
parent c158ac5472
commit 9e3647395a
11 changed files with 663 additions and 8 deletions
+40 -2
View File
@@ -96,6 +96,7 @@ type RunContext struct {
jobContainerID string
hasBash *bool // memoized implicit-shell probe, only set on the top-level RunContext
jobNetworkName string
dockerProxy *container.DockerProxy
// stepEnv is a copy of the running step's environment, so that workflow commands parsed out
// of the container's output can be judged against it. Written by runStepExecutor and read on
// the log-writer goroutine, hence unsecureCommandMu, which also guards unsecureCommandErr.
@@ -253,11 +254,38 @@ func (rc *RunContext) validVolumes() []string {
if rc.Config.BindWorkdir {
volumes = append(volumes, rc.Config.Workdir)
}
if rc.dockerProxy != nil {
volumes = append(volumes, rc.dockerProxy.Socket)
}
// TODO: add a new configuration to control whether the docker daemon can be mounted
return append(volumes, name, name+"-env",
getDockerDaemonSocketMountPath(rc.containerDaemonSocket()))
}
func (rc *RunContext) jobDockerSocket() string {
if rc.dockerProxy != nil {
return rc.dockerProxy.Socket
}
return getDockerDaemonSocketMountPath(rc.containerDaemonSocket())
}
func (rc *RunContext) startDockerProxy(ctx context.Context) {
daemonSocket := rc.containerDaemonSocket()
if daemonSocket == "-" || strings.HasPrefix(strings.ToLower(daemonSocket), "npipe://") {
return
}
dir := container.DockerProxyDir(ctx)
if dir == "" {
return
}
proxy, err := container.StartDockerProxy(getDockerDaemonSocketMountPath(daemonSocket), dir, rc.jobContainerName())
if err != nil {
common.Logger(ctx).Warnf("docker proxy not started, the job gets the daemon socket directly: %v", err)
return
}
rc.dockerProxy = proxy
}
// toolCache returns the tool cache path the job sees, relocatable through RUNNER_TOOL_CACHE.
func (rc *RunContext) toolCache(fallback string) string {
if path := rc.GetEnv()["RUNNER_TOOL_CACHE"]; path != "" {
@@ -327,8 +355,8 @@ func (rc *RunContext) GetBindsAndMounts() ([]string, map[string]string, error) {
// the runner's own mounts below yield to the targets the job claims
binds, mounts, claimed := splitVolumes(volumes)
if daemonSocket := rc.containerDaemonSocket(); daemonSocket != "-" && !claimed["/var/run/docker.sock"] {
binds = append(binds, getDockerDaemonSocketMountPath(daemonSocket)+":/var/run/docker.sock")
if rc.containerDaemonSocket() != "-" && !claimed["/var/run/docker.sock"] {
binds = append(binds, rc.jobDockerSocket()+":/var/run/docker.sock")
}
if rc.Config.SharedToolCache {
if toolCache := rc.toolCache(container.DefaultToolCache); !claimed[toolCache] {
@@ -455,6 +483,7 @@ func (rc *RunContext) startJobContainer() common.Executor {
// For gitea, to support --volumes-from <container_name_or_id> in options.
// We need to set the container name to the environment variable.
rc.Env["JOB_CONTAINER_NAME"] = name
rc.startDockerProxy(ctx)
envList := make([]string, 0)
@@ -651,6 +680,12 @@ func (rc *RunContext) cleanupJobResources(networkName string, createAndDeleteNet
logger.Errorf("Error while cleaning services: %v", err)
}
}
if rc.dockerProxy != nil {
if err := rc.dockerProxy.Close(ctx); err != nil {
logger.Errorf("Error while removing what the job created: %v", err)
}
rc.dockerProxy = nil
}
if removeJobContainer {
// after the containers using them, services can hold these via `--volumes-from`
name := rc.jobContainerName()
@@ -914,6 +949,9 @@ func (rc *RunContext) captureJobContainerInfo() common.Executor {
return nil
}
rc.jobContainerID = info.ID
if source := info.Mounts[rc.githubWorkspace()]; source != "" {
rc.Env["GITEA_DOCKER_WORKSPACE"] = source
}
return nil
}
}