Docker OCI runtime error container_linux mkdir /proc in CI
Docker OCI runtime error container_linux mkdir /proc is a search people arrive with rather than a line the runtime prints, and knowing that is most of the fix. What runc actually emits when a container fails over /proc is a rootfs mount refusal, naming a path inside the container rootfs and a reason, and the reason is what decides whether you are looking at a privilege problem, a nesting problem or a mount you should not have asked for.

What this error means
A container will not start, the message mentions /proc somewhere in a long OCI runtime chain, and searching what you can see returns pages about unrelated mount failures. It comes up in Docker-in-Docker jobs, in Kubernetes-based runners and in workflows that bind-mount something under /proc into a container. The tell is that the path in the message is usually not /proc at all but a long path under the daemon data root ending in /merged/proc, which is the container rootfs and not the host.
docker: Error response from daemon: OCI runtime create failed: container_linux.go:370: starting container process caused: process_linux.go:459: container init caused: rootfs_linux.go:59: mounting "/proc/sys/net" to rootfs at "/proc/sys/net" caused: "/var/lib/docker/overlay2/9fd477a20091dd5d9babf5ce2bddb8d517349c89ba9a4e6c7f74f275f0c370c9/merged/proc/sys/net" cannot be mounted because it is inside /proc: unknown.The message you are searching for is not the message runc writes
It is worth saying plainly, because it saves an hour. We looked for a bare mkdir /proc in the runc source and in the reports that mention it, and the container runtimes do not produce one. What they produce over /proc is a mount check. runc has a guard called checkProcMount in libcontainer/rootfs_linux.go whose refusal is a literal you can read: a quoted path followed by cannot be mounted because it is inside /proc. That clause is stable from runc v1.0.0 through v1.3.0, and it is the thing to search for.
The rest of the line is assembled from four programs in turn, so the full message is a literal nowhere. container_linux.go and process_linux.go with line numbers are runc stack frames captured at failure time, not text anyone wrote. rootfs_linux.go:59 is the same. The clause about mounting something to the rootfs is runc, and the trailing : unknown is the daemon saying the error carried no type it recognized, which is a version artifact rather than a clue.
| What you searched for | What the runtime actually emits |
|---|---|
mkdir /proc | No such message in runc. The /proc failures are mount refusals |
container_linux.go:NNN | A runc stack frame captured at failure time, so the number varies |
A path that looks like /proc/... | Usually the rootfs path ending /merged/proc/..., inside the container |
oci runtime error | Older daemon wording. Current daemons say OCI runtime create failed |
Common causes
A bind mount lands inside the container /proc
The case the quoted line shows. Something in the run asks for a path inside the container procfs, runc checks it, and declines. The request usually exists to make a kernel parameter writable, and the refusal is the runtime doing its job rather than failing at it.
A nested runtime lacks the privileges to mount procfs
A daemon or runtime running inside a container needs capabilities that a default container does not grant in order to set up a new container rootfs. The chain then ends in operation not permitted on a proc or sysfs mount. In our experience this is the cause whenever the job uses a dind service or a Kubernetes-based runner.
A seccomp or AppArmor profile blocks the mount
A custom security profile that filters the mount syscall stops container setup at the same place, with the same clause at the end. This is worth suspecting when the same image starts on one machine and not another, because the profile is host state and the image is not.
The runtime is old enough to word this differently
Reports that say oci runtime error rather than OCI runtime create failed come from daemons several major versions back. The failure family is the same, the fix is the same, and the difference matters only when you are trying to match a log line against an answer written years apart from it.
How to fix it
Set kernel parameters as sysctls instead of mounting procfs
- Identify which parameter the mount existed to expose.
- Pass it with
--sysctlon run, or thesysctlskey in compose. - Drop the bind mount entirely, so the guard is never reached.
docker run --sysctl net.ipv4.ping_group_range="0 2147483647" myorg/app:ciRead the last clause and stop reading the rest
Everything before the final clause is four programs reporting each other. Cut the line at the last colon and search that fragment, with the distinguishing words in it, because a search for the wrapper text returns unrelated failures that share the same wrapper.
docker run --rm "$IMAGE" true 2>&1 | tr ':' '\n' | tail -3Stop nesting where the runner already provides a daemon
On a Linux CI runner the job can talk to the host daemon directly, which removes the whole privilege half of this family along with a second image cache. Keep a nested daemon only where isolation between builds is a real requirement rather than an inherited habit.
- run: docker info --format '{{.ServerVersion}}'
- run: docker build -t app .Give a nested runtime what it needs, explicitly and narrowly
When a nested daemon is unavoidable, grant the privileges and namespaces in the command that starts it, and confine that to the one job. This is a deliberate reduction in isolation, so it should be visible in the workflow rather than buried in an image.
docker run --privileged --cgroupns=host \
-v /sys/fs/cgroup:/sys/fs/cgroup:rw \
docker:28-dindWhat the guard is actually protecting
runc refuses to mount anything over or inside the container's own /proc because procfs is how the container sees itself, and letting an arbitrary mount land inside it is a well understood way to escape or confuse the sandbox. The check is deliberate and it fires on requests that look completely reasonable, which is why the error reads like a bug. Mounting the host /proc/sys/net into a container to make a sysctl writable is exactly the kind of request it declines.
So this one is usually answered by not asking. If you need a kernel parameter changed inside a container, set it as a sysctl rather than mounting the file that exposes it, and the runtime will apply it through the interface built for that.
# not this: a bind mount inside the container /proc
# docker run -v /proc/sys/net:/proc/sys/net:rw myorg/app:ci
# this: ask the runtime to set the parameter
docker run --sysctl net.ipv4.ip_local_port_range="10000 60000" myorg/app:ciThe other half of the family is nesting
When the clause at the end says operation not permitted rather than naming the /proc guard, you are looking at a privilege problem instead of a policy one, and it almost always means a runtime inside a container that was not given what it needs to build a container of its own. Mounting procfs and sysfs requires capabilities that a default container does not have.
The cheapest answer on a CI runner is to stop nesting, because a Linux runner already has a working daemon and a job that starts a second one is paying for it twice. When nesting is genuinely required, grant the privileges deliberately and scope them to the job that needs them rather than adding flags until something starts.
- name: Use the daemon the runner already has
run: docker build -t app .
# only when a nested daemon is genuinely required
# docker run --privileged --cgroupns=host docker:28-dindWhat the runner does about it
No repair, and no recorded run, and on this page the absence is the argument rather than an apology. The finding here is that the message people search for is not the message the runtime emits. If we answered that with a capture of our own, we would have chosen which line to show, and a page whose whole point is that the widely searched wording is wrong cannot settle the question with a line it selected itself. The runc source and a public report from a plain docker run settle it without us in the picture.
The practical position is the same either way. A Latchkey Linux runner gives the job a daemon on the host, which is the configuration where the nesting half of this family does not arise, and the /proc guard half is a deliberate refusal that no runner should be overriding on your behalf.
How to prevent it
- Use sysctls for kernel parameters and never bind-mount inside the container /proc.
- Prefer the runner daemon over a nested one wherever the job allows it.
- Keep custom seccomp and AppArmor profiles out of CI hosts unless they are tested there.
- Print the Docker and runc versions once per job, so an old wording is explained by the log.
Frequently asked questions
Why can I not find "mkdir /proc" in any Docker or runc source?
/proc failures in this family are mount refusals, and the literal to look for is a quoted path followed by cannot be mounted because it is inside /proc, which lives in runc libcontainer/rootfs_linux.go. If your search for a mkdir on /proc is finding nothing useful, that is why.Why is the path in the error so long?
/proc/sys/net is resolved to somewhere under the daemon data root ending in /merged/proc/sys/net, and that resolved path is what the guard reports as being inside /proc.What does the trailing ": unknown" mean?
How do I make a sysctl writable inside a container without mounting /proc?
--sysctl on docker run, or the sysctls key in compose, applies the parameter through the interface built for it, and the bind mount that triggers the guard becomes unnecessary. Namespaced parameters can be set this way; ones that are not namespaced belong on the host.Related guides
References
- runc v1.3.0: checkProcMount and its refusal literal, libcontainer/rootfs_linux.go
- opencontainers/runc#2826: the docker run and the full line quoted above
- Docker docs: docker run, the --sysctl option
- opencontainers/runc#5309: the privilege half of this family, on a nested runtime
- Docker documentation
- Docker build cache
- GitHub Actions documentation