# Docker cannot enter cgroupv2, and cgroup mount errors in CI

> Fix Docker cannot enter cgroupv2 and the cgroup mount errors in GitHub Actions: delegate the controllers a nested daemon needs, or stop nesting.

Source: https://latchkey.dev/learn/docker/docker-cgroup-cannot-enter-cgroupv2  
Updated: 2026-09-20

Docker cannot enter cgroupv2, and the cgroup mount errors beside it, mean the runtime could not place your container into a control group, which on a CI runner nearly always means a daemon running nested inside another container without the v2 controllers delegated to it. Your image and your command are not involved: the container has nowhere to be accounted.

## What this error means

A container refuses to start and the message points at `/sys/fs/cgroup` rather than at anything you wrote, often with the phrase "with domain controllers" and a note that the state is invalid. It appears in Docker-in-Docker jobs, in Kubernetes-in-Docker tools, and in build engines that run their own daemon in a container, and it does not appear on a plain hosted runner doing a plain `docker run`. We did not reproduce it: it needs a nested daemon in a particular broken delegation state, and building one to photograph it would teach you less than the delegation rules below. The line below is quoted in full from containerd/containerd#6659, `ctr` prefix and line numbers included, because the clause that names the cause is in the middle of it: applying cgroup configuration for process. The Go file and line numbers move between runc releases, so match on that clause rather than on them.

```The line containerd/containerd#6659 reports (illustrative, not a recorded run)
ctr: failed to create shim task: OCI runtime create failed: container_linux.go:380: starting container process caused: process_linux.go:385: applying cgroup configuration for process caused: cannot enter cgroupv2 "/sys/fs/cgroup/default" with domain controllers -- it is in an invalid state: unknown
```

## Common causes

### Controllers were never delegated to the nested environment

The dominant cause. A daemon inside a container gets a cgroup subtree, and the controllers it needs have to be enabled in that subtree by whatever created it. When they are not, the runtime cannot create the child cgroup for a new container, and reports that it cannot enter the cgroup it was given.

### The cgroup mount is missing or read-only inside the container

A nested daemon needs `/sys/fs/cgroup` present and writable for its own subtree. A hardened container, a read-only mount, or a sandbox that hides the hierarchy produces mount errors in the same family, with the same effect: nothing can be placed in a cgroup.

### The cgroup driver does not match the host

On a v2 host the default driver is systemd. A nested runtime or a tool configured for cgroupfs will fight the systemd hierarchy, and in our experience this is the cause when the error changes shape between runs rather than being stable.

### The runtime predates proper cgroup v2 support

Older runc and containerd releases handle the unified hierarchy badly, which is what containerd/containerd#6659 is about. A pinned old runtime in a custom image, or a base image nobody has refreshed, is enough to reproduce a bug that current versions do not have.

## How to fix it

### Use the daemon the runner already has

1. Drop the `docker:dind` service and run Docker commands directly in the job.
2. If a tool insists on its own engine, check whether it has a mode that reuses the host daemon.
3. Keep nesting for cases that genuinely need isolation between builds.

```.github/workflows/ci.yml
- run: docker info --format '{{.CgroupVersion}} {{.CgroupDriver}}'
- run: docker build -t app .
```

### Give the nested daemon the cgroup environment it needs

When you do nest, run the inner daemon with the host cgroup namespace and the privileges to manage its subtree, and mount the hierarchy so it is writable. This is a deliberate reduction in isolation, so scope it to the job that needs it rather than making it the default.

```Terminal
docker run --privileged --cgroupns=host \
  -v /sys/fs/cgroup:/sys/fs/cgroup:rw \
  docker:29-dind
```

### Align the cgroup driver end to end

Pick one driver and configure every layer for it. On a v2 host that is systemd unless you have a reason. Docker accepts only cgroupfs or systemd and refuses to start when systemd is requested and unavailable, so a mismatch fails loudly rather than silently.

```/etc/docker/daemon.json
{ "exec-opts": ["native.cgroupdriver=systemd"] }
```

### Update runc and containerd before debugging further

If the stack is pinned to a release from the early cgroup v2 era, upgrade first and retest. A large share of reports against this error resolve into a runtime that was fixed years ago, which is cheaper to rule out than to reason about.

```Terminal
runc --version
containerd --version
docker info --format '{{.ServerVersion}}'
```

## How to prevent it

- Use the runner daemon rather than a nested one wherever the job allows it.
- If you nest, pin the cgroup namespace and mounts explicitly in the workflow.
- Keep runc and containerd current in any custom runner image.
- Print the cgroup version and driver once per job that runs containers.

## What changes between cgroup v1 and v2

Docker documents the hierarchy as living under `/sys/fs/cgroup` on modern distributions, in the page we read on 2026-09-20, and the defaults differ by version: the cgroup driver, set with `dockerd --exec-opt native.cgroupdriver`, is systemd on v2 and cgroupfs on v1, and the container cgroup namespace mode is private on v2 and host on v1. Ubuntu has defaulted to v2 since 21.10, so every current hosted Linux runner image is a v2 host.

On a v2 host the hierarchy is unified and a process can only manage the part of the tree it was delegated. That is the whole story of this error: a daemon inside a container is handed a subtree, and if the controllers it needs were not enabled there, it cannot create the child cgroup a new container requires.

| Setting | cgroup v1 | cgroup v2 |
| --- | --- | --- |
| Default cgroup driver | `cgroupfs` | `systemd` |
| Default `--cgroupns` | `host` | `private` |
| Where the hierarchy is | Per-controller trees under `/sys/fs/cgroup` | One unified tree under `/sys/fs/cgroup` |
| How to tell | No `cgroup.controllers` file | `/sys/fs/cgroup/cgroup.controllers` exists |

> Docker only accepts `cgroupfs` or `systemd` as the driver, and errors out at start if you ask for systemd where it is not available.

## This error is a nesting error

The wording that names domain controllers and an invalid state comes from a runtime trying to move a process into a cgroup that is a domain with controllers enabled, which the v2 rules do not allow. containerd/containerd#6659 is the canonical report: containerd 1.6.x running inside a Docker container, unable to run containers. Dagger and Earthly hit the same wall from their own engines, in dagger/dagger#6364 and earthly/earthly#3205.

It is also rare, which is itself diagnostic. A GitHub issue search for the phrase together with "github actions" returned 11 results when we ran it on 2026-09-20, against 467 for "OCI runtime exec failed" the same day. If you are seeing it, you are almost certainly running a daemon inside a container rather than using the runner daemon.

```Terminal
# which version, and what is delegated here
cat /sys/fs/cgroup/cgroup.controllers 2>&1 || echo "cgroup v1 host"
docker info --format '{{.CgroupDriver}} {{.CgroupVersion}}'
```

## The cheapest fix is to stop nesting

A hosted Linux runner already has a working daemon. A job that runs `docker:dind` as a service container to get one is paying for a second daemon, a second image cache and this class of failure, usually because the workflow was ported from a platform where the runner had no Docker at all.

When nesting is genuinely required, give the nested daemon the environment it needs rather than hoping: the host cgroup namespace, a writable cgroup mount, and enough privilege to create the subtree. Then align the drivers, because a nested runtime expecting cgroupfs on a systemd host produces the neighboring mount errors.

```.github/workflows/ci.yml
# prefer this on a runner that already has Docker
- run: docker build -t app .
# instead of a dind service plus DOCKER_HOST gymnastics
```

## What the runner does about it

No repair, and no reproduction to quote. A Latchkey `latchkey-small` runner gives a job a daemon on the host, which is the configuration where this failure does not arise, so there is no honest run for us to record here. Nothing in the self-heal library targets cgroup delegation, and it should not: changing a delegation boundary underneath a running job is not a safe automatic action.

The practical advice is the same as the section above. Use the daemon the runner gives you; if you need a nested one, set its cgroup namespace and privileges explicitly, and print `docker info` once so the log records which driver and version were in play when it failed.

## FAQ

### Which cgroup version do GitHub Actions runners use?

Version 2. Docker documents that Ubuntu has defaulted to cgroup v2 since 21.10, so the ubuntu-22.04 and ubuntu-24.04 images are unified-hierarchy hosts, with systemd as the default cgroup driver and a private cgroup namespace for containers.

### How do I tell which cgroup version a runner is on?

Check for `/sys/fs/cgroup/cgroup.controllers`: it exists on a v2 host and does not on v1. `docker info` reports the version and the driver together, which is the line worth printing in any job that runs containers, because it makes the next failure readable.

### Do I need --cgroupns=host for Docker-in-Docker?

Often yes on a v2 host, because the default namespace mode for containers is private, and a nested daemon in a private namespace may not see the part of the hierarchy it needs. Set it deliberately, together with the privileges and the writable mount, rather than adding flags until something works.

### Is this error common in CI?

No, and that is useful. A GitHub issue search for the phrase together with "github actions" returned 11 results on 2026-09-20, while "OCI runtime exec failed" returned 467 the same day. A rare error with a specific shape usually means a specific configuration, and here it is nesting.

## References

- [Docker docs: runtime metrics, cgroup v1 and v2 defaults](https://docs.docker.com/engine/containers/runmetrics/)
- [Docker docs: dockerd, native.cgroupdriver](https://docs.docker.com/reference/cli/dockerd/)
- [containerd/containerd#6659: cannot enter cgroupv2 with domain controllers](https://github.com/containerd/containerd/issues/6659)
- [dagger/dagger#6364: the same failure from an engine running in a container](https://github.com/dagger/dagger/issues/6364)

---

Latchkey runs CI/CD that repairs its own failures. Agent entry points: https://latchkey.dev/agent.txt, https://latchkey.dev/openapi.json, https://latchkey.dev/llms.txt
