<?xml version="1.0" encoding="UTF-8"?><rss xmlns:dc="http://purl.org/dc/elements/1.1/" xmlns:content="http://purl.org/rss/1.0/modules/content/" xmlns:atom="http://www.w3.org/2005/Atom" version="2.0"><channel><title><![CDATA[Nguyễn Mạnh Hà]]></title><description><![CDATA[Nguyễn Mạnh Hà]]></description><link>https://manhhanguyen.work</link><generator>RSS for Node</generator><lastBuildDate>Thu, 30 Apr 2026 13:48:25 GMT</lastBuildDate><atom:link href="https://manhhanguyen.work/rss.xml" rel="self" type="application/rss+xml"/><language><![CDATA[en]]></language><ttl>60</ttl><item><title><![CDATA[Gitlab runner and Docker compose trap]]></title><description><![CDATA[I use Gitlab Runner in docker and I registered a GitLab Runner successfully. Felt smart.
Then I ran docker compose up -d… and the runner immediately face-planted with:
ERROR: Failed to load config stat /etc/gitlab-runner/config.toml: no such file or ...]]></description><link>https://manhhanguyen.work/gitlab-runner-and-docker-compose-trap</link><guid isPermaLink="true">https://manhhanguyen.work/gitlab-runner-and-docker-compose-trap</guid><category><![CDATA[Docker]]></category><category><![CDATA[GitLab-CI]]></category><category><![CDATA[gitlab-runner]]></category><category><![CDATA[Docker compose]]></category><category><![CDATA[Devops]]></category><category><![CDATA[troubleshooting]]></category><category><![CDATA[debugging]]></category><category><![CDATA[Linux]]></category><dc:creator><![CDATA[Nguyen Manh Ha]]></dc:creator><pubDate>Thu, 29 Jan 2026 11:18:40 GMT</pubDate><enclosure url="https://cdn.hashnode.com/res/hashnode/image/upload/v1769016004438/93d16000-7526-4e4e-a8c6-118106138e3c.jpeg" length="0" type="image/jpeg"/><content:encoded><![CDATA[<p>I use Gitlab Runner in docker and I registered a GitLab Runner successfully. Felt smart.</p>
<p>Then I ran docker compose up -d… and the runner immediately face-planted with:</p>
<pre><code class="lang-plaintext">ERROR: Failed to load config stat /etc/gitlab-runner/config.toml: no such file or directory
builds=0 max_builds=1
</code></pre>
<p>The container was <em>up</em>. The file was <em>gone</em>. My confidence? Also gone.</p>
<p>Two hours later, I learned (again) that Docker Compose will happily create a <em>different</em> volume than the one you think you’re using — and won’t warn you. Classic.</p>
<h1 id="heading-what-was-happening-and-why-it-mattered"><strong>What was happening and why it mattered</strong></h1>
<p>GitLab Runner stores its registration and config in:</p>
<ul>
<li><pre><code class="lang-plaintext">            /etc/gitlab-runner/config.toml (inside the container)
</code></pre>
</li>
</ul>
<p>So you typically mount a volume there so the runner keeps its config across restarts. My workflow was:</p>
<ol>
<li><p>Register the runner using docker run ... register</p>
</li>
<li><p>Start the runner using docker compose up -d</p>
</li>
</ol>
<p>Sounds normal, right?</p>
<p>Except I accidentally created <strong>two different volumes</strong>.</p>
<ul>
<li><p>docker run used: <strong>gitlab-runner-config</strong></p>
</li>
<li><p>docker compose created/used: <strong>gitlabrunner_gitlab-runner-config</strong></p>
</li>
</ul>
<p>So:</p>
<ul>
<li><p>Registration wrote config.toml into Volume A</p>
</li>
<li><p>Runner started with Volume B</p>
</li>
<li><p>Runner: “config file doesn’t exist”</p>
</li>
<li><p>Me: “but I literally saw it earlier???” 🤡</p>
</li>
</ul>
<h2 id="heading-looking-for-trouble-with-a-slight-chance-of-survival-follow-me">Looking for trouble with a slight chance of survival? Follow me</h2>
<h3 id="heading-step-1-reproduce-the-it-registered-but-cant-start-symptom"><strong>Step 1 — Reproduce the “it registered but can’t start” symptom</strong></h3>
<p>You register like this:</p>
<pre><code class="lang-plaintext">docker run --rm -it \
  -v gitlab-runner-config:/etc/gitlab-runner \
  gitlab/gitlab-runner:latest register
</code></pre>
<p>This writes config.toml into the named Docker volume gitlab-runner-config.</p>
<p>Then you start via Compose:</p>
<pre><code class="lang-plaintext">docker compose up -d
</code></pre>
<p>Runner container starts… but can’t find /etc/gitlab-runner/config.toml.</p>
<h3 id="heading-step-2-the-key-debugging-move-compare-what-volume-each-container-actually-mounted"><strong>Step 2 — The key debugging move: compare what volume each container actually mounted</strong></h3>
<p>Check volumes:</p>
<pre><code class="lang-plaintext">docker volume ls | grep gitlab
docker inspect &lt;runner_container_name&gt; --format '{{ json .Mounts }}' | jq
</code></pre>
<p>Or without jq:</p>
<pre><code class="lang-plaintext">docker inspect &lt;runner_container_name&gt; | grep -A5 Mounts
</code></pre>
<p>This is where the “oh no” moment happens:</p>
<ul>
<li><p>your manual register container wrote to <strong>gitlab-runner-config</strong></p>
</li>
<li><p>your compose runner container mounted <strong>&lt;project&gt;_gitlab-runner-config</strong></p>
</li>
</ul>
<p>Compose prefixes volume names with the <strong>project name</strong> unless you tell it not to. That’s the “name mangling” you hit.</p>
<h3 id="heading-step-3-understand-why-compose-does-that"><strong>Step 3 — Understand why Compose does that</strong></h3>
<p>Docker Compose has this concept of a “project”. By default, the project name is derived from the directory name (or overridden with -p / COMPOSE_PROJECT_NAME).</p>
<p>When you define a named volume in docker-compose.yml like:</p>
<pre><code class="lang-plaintext">volumes:
  gitlab-runner-config:
</code></pre>
<p>Compose will create it as:</p>
<ul>
<li>&lt;project&gt;_gitlab-runner-config</li>
</ul>
<p>So your folder name gitlabrunner/ becomes:</p>
<ul>
<li>gitlabrunner_gitlab-runner-config</li>
</ul>
<p>This is expected behavior… and also a great way to lose two hours of your life.</p>
<h3 id="heading-step-4-fix-it-properly-the-clean-way"><strong>Step 4 — Fix it properly (the clean way)</strong></h3>
<p>You have two solid paths.</p>
<h3 id="heading-multiple-approaches-proscons-what-id-pick"><strong><mark>Multiple approaches (pros/cons + what I’d pick)</mark></strong></h3>
<h3 id="heading-approach-a-make-compose-use-the-exact-same-existing-volume-recommended"><strong>Approach A — Make Compose use the exact same existing volume (recommended)</strong></h3>
<p>Mark the volume as <strong>external</strong> and explicitly name it:</p>
<pre><code class="lang-plaintext">services:
  gitlab-runner:
    image: gitlab/gitlab-runner:latest
    restart: always
    volumes:
      - gitlab-runner-config:/etc/gitlab-runner
      - /var/run/docker.sock:/var/run/docker.sock

volumes:
  gitlab-runner-config:
    external: true
    name: gitlab-runner-config
</code></pre>
<p><strong>Pros</strong></p>
<ul>
<li><p>No surprises.</p>
</li>
<li><p>Same volume used by docker run and Compose.</p>
</li>
<li><p>Works even if the project name changes.</p>
</li>
</ul>
<p><strong>Cons</strong></p>
<ul>
<li>You must create the volume first (or your register command already created it).</li>
</ul>
<p>✅ <strong>This is my final choice</strong>. It’s explicit. DevOps loves explicit.</p>
<hr />
<h3 id="heading-approach-b-register-the-runner-using-compose-so-everything-lives-in-the-compose-world"><strong>Approach B — Register the runner using Compose so everything lives in the Compose world</strong></h3>
<p>Instead of docker run ... register, do:</p>
<pre><code class="lang-plaintext">docker compose run --rm gitlab-runner register
</code></pre>
<p>Now the registration happens inside the same Compose project context, so it writes to the same prefixed volume.</p>
<p><strong>Pros</strong></p>
<ul>
<li><p>One tool (Compose) owns the workflow.</p>
</li>
<li><p>Less naming confusion.</p>
</li>
</ul>
<p><strong>Cons</strong></p>
<ul>
<li><p>Slightly more “Compose-y”.</p>
</li>
<li><p>If you later run with a different project name, you can still get split-brain volumes.</p>
</li>
</ul>
<p>This is great if you want “everything is Compose, always”.</p>
<h3 id="heading-approach-c-set-a-fixed-compose-project-name-band-aid-but-useful"><strong>Approach C — Set a fixed Compose project name (band-aid, but useful)</strong></h3>
<p>In .env:</p>
<pre><code class="lang-plaintext">COMPOSE_PROJECT_NAME=gitlabrunner
</code></pre>
<p>Or run:</p>
<pre><code class="lang-plaintext">docker compose -p gitlabrunner up -d
</code></pre>
<p><strong>Pros</strong></p>
<ul>
<li>Predictable prefix names.</li>
</ul>
<p><strong>Cons</strong></p>
<ul>
<li><p>Still not as explicit as external: true.</p>
</li>
<li><p>Someone else running it differently can recreate the problem.</p>
</li>
</ul>
<h2 id="heading-common-mistakes-gotchas"><strong>Common mistakes / gotchas</strong></h2>
<ul>
<li><p><strong>Assuming</strong> gitlab-runner-config means the same thing across docker run and Compose.</p>
</li>
<li><p>Forgetting Compose adds a <strong>project prefix</strong> to volumes by default.</p>
</li>
<li><p>Registering with docker run, running with docker compose up, then wondering why the config “disappears”.</p>
</li>
<li><p>Not checking what is actually mounted via docker inspect.</p>
</li>
<li><p>Having multiple folders with different Compose project names = multiple “almost identical” volumes.</p>
</li>
<li><p>Typo trap: error mentions config.toml, but your brain reads it as config.yml because YAML is everywhere ^=^</p>
</li>
</ul>
<h2 id="heading-quick-checklist"><strong>Quick checklist</strong></h2>
<ul>
<li><p>Confirm the runner expects /etc/gitlab-runner/config.toml</p>
</li>
<li><p>List volumes: <code>docker volume ls</code></p>
</li>
<li><p>Inspect mounts: <code>docker inspect &lt;container&gt; | grep -A5 Mounts</code></p>
</li>
<li><p>Ensure registration and runner use the <strong>same</strong> named volume</p>
</li>
<li><p>Prefer external: true + explicit name: in Compose for shared volumes</p>
</li>
<li><p>Or register via <code>docker compose run --rm gitlab-runner register</code></p>
</li>
</ul>
<h2 id="heading-conclusion"><strong>Conclusion</strong></h2>
<p>One takeaway: <strong>Docker Compose didn’t lose your config — you put it in a different universe.</strong></p>
<p>Volumes are state. State needs boring, explicit naming. Your future self will thank you.</p>
<p>If you’ve had your own “container is running but reality isn’t” moment, I’d genuinely love to hear it.</p>
<p>Misery loves company. Especially in DevOps.</p>
<h1 id="heading-tldr">TL;DR</h1>
<p><code>docker run</code> and <code>docker compose</code> can end up using different named volumes.</p>
<p>Compose prefixes volume names with the project name unless you force it not to.</p>
<p>Fix it with external: true + name: gitlab-runner-config, or register using docker compose run</p>
]]></content:encoded></item></channel></rss>