Fall 2026brianmars.comBuilt at EGBT Technologies

The Devbox Build, Part 4: Source Control and CI

Forgejo goes in easily. The runner is where every network decision from parts 2 and 3 gets tested at once.

Forgejo is the reason I built the box. It was also the easiest part to install: I run one container, pinned to a major version, with its data on /srv. I kept SQLite, created the admin account on the first-run screen, and turned off open registration.

Two Ports for Git

I bound the web interface to 127.0.0.1 so only Caddy can reach it, as git.dev.tailnet.example.1 Git over SSH goes straight to the box on port 2222, since the host's own sshd already listens on 22.

That split costs one surprise. The short git@host:owner/repo form always uses port 22, and the host's sshd has no git user.

I added a host entry to my ssh config that sets the port, and after that the short form works too.

Reference card · Clip and keepForgejo

A private git host in one container, behind Caddy for the web and on its own port for git over SSH.

  1. HTTP bound to 127.0.0.1:3000
  2. ROOT_URL on the private name
  3. git over SSH on host port 2222
  4. Data on /srv/data/forgejo
  5. Mail through the shared relay
Verified 17 Oct 2026 · Forgejo 11 (codeberg.org/forgejo/forgejo:11) · 5 changes · 2 seamsRead the full card

The Runner

The Actions runner connects out to Forgejo and runs each job in a container on its own Docker daemon, docker-in-docker. That second daemon is where my earlier choices came back.

My first notes registered the runner against Forgejo's internal address, http://forgejo:3000. The runner itself could reach that, so it showed up as online. Then the first job tried to check out code, from inside dind, where the name forgejo doesn't exist.

The runner and every job container have to reach Forgejo at the same address, and only the public name works from inside dind. Getting there took fixes in three places. Docker's pinned DNS from part 3 covers the runner's own lookups. dind needs its own DNS settings for its pulls and for the job containers it starts. And the firewall from part 2 needs one narrow rule, so containers can reach Caddy on 443 at the box's tailnet address.

The compose file has a few other traps. The dind service has to be named docker, because its TLS certificate is issued for that name. A registration token pasted with angle brackets around it turns into a shell redirect. And each failed registration uses up the token, so I needed a fresh one each time.

Reference card · Clip and keepForgejo Actions runner with dind

A CI runner whose job containers reach Forgejo at the same address the runner does, from inside a nested Docker daemon.

  1. Registered against the public URL
  2. Runs as root
  3. An explicit command
  4. The dind service is named docker
  5. DNS set twice for dind
  6. One firewall rule for the hairpin
Verified 17 Oct 2026 · Forgejo runner 6 (code.forgejo.org/forgejo/runner:6) · 6 changes · 5 seamsRead the full card

Online in the runner list only means the runner connected. I pushed a small workflow to a test repository and waited for a green run through checkout. Until then I didn't count it as done.

No comments yet