🚀 Executive Summary

TL;DR: Accessing volumes in a Forgejo CI runner with Docker-in-Docker (DinD) fails because the runner and DinD service are sibling containers, not parent-child, meaning DinD doesn’t see volumes mounted to the runner. The recommended solution is to use Docker-managed named volumes, attaching the same volume to both the runner and the DinD service for portable and secure shared data.

🎯 Key Takeaways

  • Docker-in-Docker (DinD) setups involve sibling containers (runner and DinD service), not nested ones, explaining why volumes mounted to the runner are invisible to the DinD service.
  • Docker-managed named volumes are the architecturally sound and portable solution for sharing data between a Forgejo runner and its DinD service, avoiding host-specific path dependencies.
  • Mounting the host’s Docker socket (Docker-out-of-Docker or DooD) provides direct host access but poses a severe security risk, making it unsuitable for untrusted or multi-tenant environments.

trying to access dind volumes from my forgejo runner?

Struggling to access volumes from a Docker-in-Docker (DinD) setup in your Forgejo CI runner? We’ll break down the “sibling container” problem and provide three real-world solutions, from quick hacks to the architecturally sound fix.

Wrestling with DinD: How to Actually Access Volumes from Your Forgejo Runner

It was 2 AM, the `prod-api-gateway` deployment was failing, and the error was maddeningly simple: file not found. The CI build was supposed to pick up a pre-populated dependency cache from a volume, but the container running the build swore it wasn’t there. Yet, I could `exec` into the main runner container and see the files, plain as day, sitting right in /cache. If you’ve ever stared at a CI log, knowing the files are right there but your container can’t see them, then you know the specific kind of headache that is Docker-in-Docker. It feels like you’re losing your mind, but I promise, you’re not—you’ve just stumbled into one of Docker’s most common “gotchas”.

The Root of the Problem: It’s Not “In” Docker, It’s “Next To” Docker

Here’s the mental model shift you need to make. When you use the standard Docker-in-Docker (DinD) setup for a CI runner, you’re not actually running Docker *inside* the runner container. You’re running two separate, distinct containers:

  • The Forgejo Runner Container: This is where your CI job’s instructions are executed.
  • The DinD Service Container: This is a completely separate container that runs the Docker daemon itself.

The runner container is configured to talk to the DinD container over the network (via the DOCKER_HOST: tcp://docker:2375 variable). They are siblings, not parent and child. So when you mount a volume like /path/on/host:/data/in/runner to your runner, the DinD container has absolutely no knowledge of it. When your runner tells the DinD service “Hey, run a new container and mount /data/in/runner into it,” the DinD service looks at its *own* empty filesystem, finds nothing at that path, and mounts an empty directory. The result? Maddening “file not found” errors.

Solution 1: The “Just Get It Working” Host Bind Mount

This is the fastest way to solve the problem, but it’s also the dirtiest. The idea is to mount the exact same host path into both the runner container and the DinD container. This forces them to share a piece of the host’s filesystem, making the files visible to both.

Here’s what your docker-compose.yml might look like:

version: '3.8'

services:
  forgejo-runner:
    image: code.forgejo.org/forgejo/runner:latest
    environment:
      - FORGEJO_INSTANCE_URL=https://your.forgejo.instance
      - FORGEJO_RUNNER_TOKEN=your-runner-token
      - DOCKER_HOST=tcp://docker:2375
    volumes:
      - /srv/forgejo-runner/cache:/cache # Mount for the runner
    depends_on:
      - docker

  docker:
    image: docker:dind
    privileged: true
    volumes:
      - /srv/forgejo-runner/cache:/cache # Mount the EXACT SAME PATH for DinD

Pro Tip: I call this the “hacky but effective” method. It works, but it tightly couples your CI setup to a specific host directory structure (/srv/forgejo-runner/cache). If you move this `docker-compose.yml` to another server, you have to ensure that path exists and has the right permissions. It’s not portable.

Solution 2: The Architect’s Choice – Named Volumes

This is the proper, Docker-native way to handle shared state between containers. Instead of binding to a path on the host, you create a Docker-managed named volume and attach it to both sibling containers. Docker handles where the data is actually stored on the host, and you just refer to it by name.

This approach is cleaner, more portable, and the recommended solution for any real environment.

version: '3.8'

services:
  forgejo-runner:
    image: code.forgejo.org/forgejo/runner:latest
    environment:
      - FORGEJO_INSTANCE_URL=https://your.forgejo.instance
      - FORGEJO_RUNNER_TOKEN=your-runner-token
      - DOCKER_HOST=tcp://docker:2375
    volumes:
      - runner-cache:/cache # Attach the named volume
    depends_on:
      - docker

  docker:
    image: docker:dind
    privileged: true
    volumes:
      - runner-cache:/cache # Attach the same named volume

volumes:
  runner-cache: {} # Declare the named volume

This setup achieves the same goal, but it’s self-contained. You can run docker-compose up on any machine with Docker, and it will work without any prior host setup. This is how we run all our CI infrastructure at TechResolve.

Approach Pros Cons
Host Bind Mount (Solution 1) Quick to implement; easy to inspect files on the host. Tied to host filesystem; poor portability; potential permission issues.
Named Volume (Solution 2) Portable; self-contained; managed by Docker; the “correct” way. Slightly more abstract; finding data on the host requires `docker volume inspect`.

Solution 3: The “Nuclear Option” – Docker-out-of-Docker (DooD)

There’s a third way, and it’s important to know about it, if only to understand the risks. This method ditches the DinD container entirely. Instead, you mount the host’s own Docker socket directly into your runner container. This is called “Docker-out-of-Docker” (DooD).

When your runner executes a docker command, it’s not talking to a separate DinD container; it’s talking directly to the Docker daemon running on the host machine. The containers it creates are not *children* of the runner, but full-blown *siblings* on the host.

# WARNING: Use with extreme caution due to security implications.
version: '3.8'

services:
  forgejo-runner:
    image: code.forgejo.org/forgejo/runner:latest
    environment:
      - FORGEJO_INSTANCE_URL=https://your.forgejo.instance
      - FORGEJO_RUNNER_TOKEN=your-runner-token
    volumes:
      - /var/run/docker.sock:/var/run/docker.sock # Mount the host's Docker socket
      - /srv/forgejo-runner/cache:/cache

With this setup, when a job in the runner needs to mount /cache, it tells the host’s Docker daemon to do so, and the host daemon knows exactly where that path is because the runner is also mounting it from the host. It “just works,” but it comes at a huge cost.

🚨 SECURITY WARNING 🚨: This is the ‘run as root’ of CI/CD. Giving a container direct, unmediated access to the host’s Docker socket is a massive security vulnerability. A compromised build script or dependency could use this access to take control of every container on your host, or even the host itself. Do not use this in a multi-tenant or untrusted environment.

My Final Take

For any serious, long-term, and secure setup, use Solution 2: Named Volumes. It’s the clean, portable, and architecturally sound way to manage shared data in a DinD environment. The host bind mount is a decent crutch for local testing, but it will bite you later. And as for DooD? Unless you are the only person running code on that machine and you trust every line of it implicitly, just stay away. Your security team will thank you.

Darian Vance - Lead Cloud Architect

Darian Vance

Lead Cloud Architect & DevOps Strategist

With over 12 years in system architecture and automation, Darian specializes in simplifying complex cloud infrastructures. An advocate for open-source solutions, he founded TechResolve to provide engineers with actionable, battle-tested troubleshooting guides and robust software alternatives.


🤖 Frequently Asked Questions

âť“ Why can’t my Forgejo runner’s Docker-in-Docker (DinD) service access volumes mounted to the runner container?

The Forgejo runner and the DinD service are separate, sibling containers. Volumes mounted to the runner are on its filesystem, while the DinD service operates on its own filesystem, unaware of the runner’s mounts, leading to ‘file not found’ errors.

âť“ How do Docker named volumes compare to host bind mounts for sharing data in a DinD setup?

Named volumes are Docker-managed, portable, and self-contained, making them the recommended ‘correct’ solution. Host bind mounts are quicker but tightly couple the setup to specific host paths, leading to poor portability and potential permission issues.

âť“ What is the primary security risk of using Docker-out-of-Docker (DooD) for CI runners?

DooD involves mounting the host’s Docker socket directly into the runner, granting the container full control over the host’s Docker daemon. A compromised build script could exploit this to take control of all host containers or the host itself, posing a massive security vulnerability.

Leave a Reply

Discover more from TechResolve - SaaS Troubleshooting & Software Alternatives

Subscribe now to keep reading and get access to the full archive.

Continue reading