<?xml version="1.0" encoding="utf-8" standalone="yes"?><rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom"><channel><title>Docker on Lessons learned</title><link>https://eghuro.cz/tags/docker/</link><description>Recent content in Docker on Lessons learned</description><generator>Hugo</generator><language>en-us</language><copyright>© Alexandr Mansurov · Licensed under [CC BY 4.0](https://creativecommons.org/licenses/by/4.0/)</copyright><lastBuildDate>Wed, 21 Oct 2026 13:13:00 +0200</lastBuildDate><atom:link href="https://eghuro.cz/tags/docker/index.xml" rel="self" type="application/rss+xml"/><item><title>Unprivileged LXC containers share the host's per-user kernel limits: 'Too many open files' and 'disk quota exceeded'</title><link>https://eghuro.cz/posts/lxc-containers-share-host-kernel-limits/</link><pubDate>Wed, 21 Oct 2026 13:13:00 +0200</pubDate><guid>https://eghuro.cz/posts/lxc-containers-share-host-kernel-limits/</guid><description>&lt;p&gt;One day, two containers on the same host failed with errors that seemed unrelated. systemd in one of them:&lt;/p&gt;&#10;&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-text" data-lang="text"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;systemd[4573]: Failed to create control group inotify object: Too many open files&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;systemd[4573]: Failed to allocate manager object: Too many open files&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;systemd[1]: Failed to start user@1000.service - User Manager for UID 1000.&#10;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;Docker in another, where every container sat at &lt;code&gt;Exited (128)&lt;/code&gt;:&lt;/p&gt;&#10;&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-text" data-lang="text"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;OCI runtime create failed: runc create failed: unable to start container process: error during container init: unable to join session keyring: unable to create session key: disk quota exceeded: unknown&#10;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;No file descriptors had run out, and no disk was full. Both errors come from per-user kernel quotas counted on the host, and every unprivileged container there was drawing from the same two.&lt;/p&gt;&#10;&#10;&lt;h2 id="the-setup"&gt;The setup &lt;/h2&gt;&#10;&lt;ul&gt;&#10;&lt;li&gt;Debian 13, kernel 6.12, LXC 6.0.4&lt;/li&gt;&#10;&lt;li&gt;More unprivileged containers than the hardware comfortably runs, all with the same idmap: container root is host UID 1000000&lt;/li&gt;&#10;&lt;li&gt;systemd in every container, Docker nested in some&lt;/li&gt;&#10;&lt;/ul&gt;&#10;&#10;&lt;h2 id="too-many-open-files-inotify-instances"&gt;Too many open files: inotify instances &lt;/h2&gt;&#10;&lt;p&gt;systemd opens inotify instances to watch cgroups and units, and &lt;code&gt;inotify_init1()&lt;/code&gt; fails with &lt;code&gt;EMFILE&lt;/code&gt; when the user&amp;rsquo;s instance limit is reached. strerror turns that into &amp;ldquo;Too many open files&amp;rdquo;. The limit is &lt;code&gt;fs.inotify.max_user_instances&lt;/code&gt;, 128 by default.&lt;/p&gt;&#10;&lt;p&gt;Count the instances per user on the host:&lt;/p&gt;&#10;&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-bash" data-lang="bash"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;sudo find /proc/&lt;span class="o"&gt;[&lt;/span&gt;0-9&lt;span class="o"&gt;]&lt;/span&gt;*/fd -lname &lt;span class="s1"&gt;&amp;#39;anon_inode:inotify&amp;#39;&lt;/span&gt; 2&amp;gt;/dev/null &lt;span class="se"&gt;\&#10;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="p"&gt;|&lt;/span&gt; cut -d/ -f3 &lt;span class="p"&gt;|&lt;/span&gt; xargs -I&lt;span class="o"&gt;{}&lt;/span&gt; ps -o &lt;span class="nv"&gt;user&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt; -p &lt;span class="o"&gt;{}&lt;/span&gt; &lt;span class="p"&gt;|&lt;/span&gt; sort &lt;span class="p"&gt;|&lt;/span&gt; uniq -c &lt;span class="p"&gt;|&lt;/span&gt; sort -rn&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;cat /proc/sys/fs/inotify/max_user_instances&#10;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;Host root held 23 instances, and uids in the container range (1000000 and up) held 104 between them: 127 of 128. On the first sighting, my coding agent got that far, then dropped it as a side effect of having started too many containers at once. That was half right, since each container&amp;rsquo;s systemd adds about five instances. A day later it came back.&lt;/p&gt;&#10;&lt;p&gt;The &lt;code&gt;lxc&lt;/code&gt; package already ships a higher limit, and it wasn&amp;rsquo;t in effect:&lt;/p&gt;&#10;&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-text" data-lang="text"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;$ cat /etc/sysctl.d/30-lxc-inotify.conf | grep -v &amp;#39;^#&amp;#39;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;fs.inotify.max_user_instances = 1024&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;$ cat /proc/sys/fs/inotify/max_user_instances&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;128&#10;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;The file was dated three days after the last boot. &lt;code&gt;/etc/sysctl.d&lt;/code&gt; is applied by &lt;code&gt;systemd-sysctl&lt;/code&gt; at boot, and the package&amp;rsquo;s install hadn&amp;rsquo;t applied it, so the setting sat on disk doing nothing.&lt;/p&gt;&#10;&#10;&lt;h2 id="disk-quota-exceeded-kernel-keys"&gt;Disk quota exceeded: kernel keys &lt;/h2&gt;&#10;&lt;p&gt;runc gives each container it starts a new session keyring. &lt;code&gt;add_key()&lt;/code&gt; fails with &lt;code&gt;EDQUOT&lt;/code&gt; when the user&amp;rsquo;s key quota is full, and strerror calls that &amp;ldquo;Disk quota exceeded&amp;rdquo;. The quota is &lt;code&gt;kernel.keys.maxkeys&lt;/code&gt;, 200 keys for any uid other than root. The host shows usage per UID:&lt;/p&gt;&#10;&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-text" data-lang="text"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;$ cat /proc/key-users&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; 0: 127 126/126 107/1000000 2283/25000000&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; 1000: 12 12/12 12/200 86/20000&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;1000000: 198 198/198 198/200 4189/20000&#10;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;&lt;code&gt;198/200&lt;/code&gt; are keys used against the quota. UID 1000000 is root in every container, so all the containers shared those 200 keys, and Docker&amp;rsquo;s restart attempts had nothing left.&lt;/p&gt;&#10;&#10;&lt;h2 id="the-fix"&gt;The fix &lt;/h2&gt;&#10;&lt;p&gt;Raise the limits and apply them right away, not at the next boot. Incus recommends 2000 keys and 2 MB in its &lt;a&#10; href="https://linuxcontainers.org/incus/docs/main/reference/server_settings"&#10; &#10; target="_blank" rel="noopener"&#10; &#10; &#10;&gt;server settings&lt;/a&gt;&#10;; 1024 inotify instances matches the &lt;code&gt;lxc&lt;/code&gt; package.&lt;/p&gt;&#10;&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-bash" data-lang="bash"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;sudo tee /etc/sysctl.d/60-lxc-limits.conf &lt;span class="s"&gt;&amp;lt;&amp;lt;&amp;#39;EOF&amp;#39;&#10;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="s"&gt;fs.inotify.max_user_instances = 1024&#10;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="s"&gt;kernel.keys.maxkeys = 2000&#10;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="s"&gt;kernel.keys.maxbytes = 2000000&#10;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="s"&gt;EOF&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;sudo sysctl -p /etc/sysctl.d/60-lxc-limits.conf&#10;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;With Ansible, &lt;code&gt;ansible.posix.sysctl&lt;/code&gt; with &lt;code&gt;sysctl_set: true&lt;/code&gt; writes the file and sets the live value in one task.&lt;/p&gt;&#10;&lt;p&gt;Then run &lt;code&gt;systemctl reset-failed&lt;/code&gt; in the affected container, and &lt;code&gt;docker start&lt;/code&gt; for the dead Docker containers. Right after, uid 1000000 held 226 keys, already over the old limit.&lt;/p&gt;&#10;&#10;&lt;h2 id="why-the-two-quotas-are-counted-differently"&gt;Why the two quotas are counted differently &lt;/h2&gt;&#10;&lt;p&gt;The two quotas look similar, but separate idmaps would fix only one.&lt;/p&gt;&#10;&lt;p&gt;Keys are counted per host UID, against global sysctls. Containers that share an idmap share one quota. A separate idmap per container (&lt;code&gt;security.idmap.isolated&lt;/code&gt; in LXD and Incus) would give each its own.&lt;/p&gt;&#10;&lt;p&gt;inotify instances are counted per user namespace and charged up the chain of parent namespaces, at each level against the uid that created the child namespace. A new namespace&amp;rsquo;s own limit is &lt;code&gt;INT_MAX&lt;/code&gt;, so the limit that bites is the host&amp;rsquo;s, charged to whoever started the containers. LXC started as root charges host root, which is why the 23 instances of host root&amp;rsquo;s own daemons counted toward the same 128. Separate idmaps don&amp;rsquo;t change that.&lt;/p&gt;&#10;&#10;&lt;h2 id="takeaway"&gt;Takeaway &lt;/h2&gt;&#10;&lt;p&gt;On a container host, &amp;ldquo;Too many open files&amp;rdquo; and &amp;ldquo;disk quota exceeded&amp;rdquo; can mean that every container together hit a per-user kernel quota on the host. Check the host&amp;rsquo;s counters before you debug the container that complained.&lt;/p&gt;&#10;&lt;p&gt;Run the &lt;code&gt;find&lt;/code&gt; count and &lt;code&gt;cat /proc/key-users&lt;/code&gt; on your own container host and compare them with the limits before a container restart finds out for you.&lt;/p&gt;&#10;</description></item><item><title>A background agent Docker build: 'open Dockerfile: no such file or directory'</title><link>https://eghuro.cz/posts/background-terminal-jobs-do-not-inherit-cwd/</link><pubDate>Sun, 30 Aug 2026 00:00:00 +0000</pubDate><guid>https://eghuro.cz/posts/background-terminal-jobs-do-not-inherit-cwd/</guid><description>&lt;p&gt;A background Docker build failed immediately with:&lt;/p&gt;&#10;&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-text" data-lang="text"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;ERROR: failed to solve: failed to read dockerfile: open Dockerfile: no such file or directory&#10;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;The agent had supplied the project directory through the terminal tool&amp;rsquo;s &lt;code&gt;workdir&lt;/code&gt; argument. The retry put &lt;code&gt;cd&lt;/code&gt; inside the command itself. For an agent runner whose background path handling is uncertain, bind the directory in the command or make the Dockerfile and build context absolute.&lt;/p&gt;&#10;&#10;&lt;h2 id="the-failed-call-already-supplied-workdir"&gt;The failed call already supplied workdir &lt;/h2&gt;&#10;&lt;p&gt;In the historical runner, the background invocation had this shape, with project and image names generalized:&lt;/p&gt;&#10;&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-json" data-lang="json"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="p"&gt;{&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="nt"&gt;&amp;#34;background&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="nt"&gt;&amp;#34;workdir&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;&amp;#34;/srv/app&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="nt"&gt;&amp;#34;command&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;&amp;#34;docker build --progress=plain -t local/app:dev -f Dockerfile .&amp;#34;&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="p"&gt;}&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;Docker could not open &lt;code&gt;Dockerfile&lt;/code&gt;. The retry started with an explicit &lt;code&gt;cd&lt;/code&gt; to the project before running the build. That corrected the launch form; whether the rest of that session succeeded is outside this lesson.&lt;/p&gt;&#10;&lt;p&gt;The error alone does not identify the actual launch directory. The record supports a runner-specific path-handling failure, not a rule that background processes generally discard their parent&amp;rsquo;s directory. Ordinary shell backgrounding with &lt;code&gt;&amp;amp;&lt;/code&gt; inherits the shell&amp;rsquo;s working directory. An agent tool can launch its jobs through a different execution path.&lt;/p&gt;&#10;&lt;p&gt;A later check on the runner used to prepare this post found that background jobs preserved the preceding foreground directory, and an explicit background &lt;code&gt;workdir&lt;/code&gt; was honored. The historical failure is therefore not a current universal behavior claim.&lt;/p&gt;&#10;&#10;&lt;h2 id="bind-both-docker-paths-deliberately"&gt;Bind both Docker paths deliberately &lt;/h2&gt;&#10;&lt;p&gt;For an existing project at &lt;code&gt;/srv/app&lt;/code&gt;, put the directory change and build in the same command:&lt;/p&gt;&#10;&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-sh" data-lang="sh"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="nb"&gt;cd&lt;/span&gt; /srv/app &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; docker build --progress&lt;span class="o"&gt;=&lt;/span&gt;plain -t local/app:dev -f Dockerfile .&#10;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;The &lt;code&gt;&amp;amp;&amp;amp;&lt;/code&gt; prevents the build from running if the directory change fails. Alternatively, remove the directory dependency from both path arguments:&lt;/p&gt;&#10;&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-sh" data-lang="sh"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;docker build --progress&lt;span class="o"&gt;=&lt;/span&gt;plain -t local/app:dev &lt;span class="se"&gt;\&#10;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; -f /srv/app/Dockerfile /srv/app&#10;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;Making only &lt;code&gt;-f&lt;/code&gt; absolute still leaves &lt;code&gt;.&lt;/code&gt; as an accidental build context. Likewise, when using Compose, check its file selection and relative-path behavior rather than assuming a successful Docker build proves the next command has the right context.&lt;/p&gt;&#10;&#10;&lt;h2 id="probe-the-actual-execution-mode"&gt;Probe the actual execution mode &lt;/h2&gt;&#10;&lt;p&gt;Before a multi-minute job, use a harmless foreground command to change to a known scratch directory:&lt;/p&gt;&#10;&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-sh" data-lang="sh"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="nb"&gt;cd&lt;/span&gt; &lt;span class="s2"&gt;&amp;#34;&lt;/span&gt;&lt;span class="si"&gt;${&lt;/span&gt;&lt;span class="nv"&gt;TMPDIR&lt;/span&gt;&lt;span class="p"&gt;:?Set TMPDIR to your scratch directory&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;&amp;#34;&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="nb"&gt;pwd&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;Then submit &lt;code&gt;pwd&lt;/code&gt; as a separate background tool call without &lt;code&gt;workdir&lt;/code&gt;, and read that job&amp;rsquo;s captured output. Repeat with &lt;code&gt;workdir&lt;/code&gt; set to the project root. Those two checks distinguish remembered foreground state from an explicit launch directory without building an image or starting services.&lt;/p&gt;&#10;&lt;p&gt;For a real build, print &lt;code&gt;pwd&lt;/code&gt; and check &lt;code&gt;test -f Dockerfile&lt;/code&gt; inside the same command that launches it. A foreground check in an earlier call cannot establish the background job&amp;rsquo;s directory.&lt;/p&gt;&#10;&#10;&lt;h2 id="separate-service-startup-from-readiness-checks"&gt;Separate service startup from readiness checks &lt;/h2&gt;&#10;&lt;p&gt;The same historical session hit another runner boundary. A foreground command combining &lt;code&gt;docker compose up -d&lt;/code&gt; with a health-check loop was rejected with:&lt;/p&gt;&#10;&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-text" data-lang="text"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;This foreground command appears to start a long-lived server/watch process.&#10;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;The runner directed the agent to launch it with &lt;code&gt;background=true&lt;/code&gt;, verify readiness, and run tests separately. That refusal came from the runner, even though Compose&amp;rsquo;s &lt;code&gt;-d&lt;/code&gt; requests detached containers.&lt;/p&gt;&#10;&lt;p&gt;Keep service startup in its own call, following the runner&amp;rsquo;s execution rules. Check readiness in a separate bounded command, and fail explicitly if the deadline expires. Starting a service and proving it healthy are separate results.&lt;/p&gt;&#10;&lt;p&gt;Before your next background build, compare its reported directory with the intended build context using the harmless probe above.&lt;/p&gt;&#10;</description></item></channel></rss>