BACK TO ENTRIES
LOG_ENTRY: 2026.03.05 · 11 TELEMETRY_HITS

The Day I Learned localhost Isn't Always localhost: A Docker Networking Journey

B

Omobayonle Ogundele

MAIN_NODE: DEVOPS_ENGINEER

I spent 3 hours debugging why my CI/CD pipeline couldn't connect to my Git server on localhost:3001. The answer changed how I think about containers forever.

The Setup

Day 3 of building my DevOps homelab. Everything was working perfectly — Gitea running on localhost:3001, Prometheus collecting metrics, Grafana showing dashboards. Then I tried to set up Drone CI for automated builds.

That's when everything broke.

The Error That Wouldn't Go Away

fatal: unable to access 'http://localhost:3001/bayo/sample-app.git/': 
Failed to connect to localhost port 3001: Connection refused

I stared at this error for what felt like an eternity. But Gitea was running. I could open localhost:3001 in my browser. I could clone the repository from my terminal without any issues. So why couldn't Drone see it?

What I Tried (Nothing Worked)

Like any developer facing a mysterious error I did what we all do — I Googled. Every Stack Overflow answer said the same thing: use host.docker.internal instead of localhost.

Easy fix:

DRONE_GITEA_SERVER=http://host.docker.internal:3001

Restarted everything. Tried again.

ERR_NAME_NOT_RESOLVED: host.docker.internal's server IP could not be found.

Wait, what? More Googling. Found out host.docker.internal is a Docker Desktop (Mac/Windows) feature. I was on Linux. It doesn't exist on Linux.

At this point I'd been debugging for 2 hours. I tried different localhost variations — 127.0.0.1, 0.0.0.0. Nothing worked. I checked if Gitea was actually running. It was. I checked for port conflicts. None found. I disabled the firewall entirely. Still broken. I restarted everything — Docker, services, my entire laptop.

Still broken. I was starting to question my life choices.

The Breakthrough

Finally, exhausted and frustrated, I did something I should have done from the beginning — I actually read the Docker networking documentation. Buried in the docs I found this sentence:

"From the perspective of a container, localhost refers to the container itself, not the host machine."

Wait. You mean when the Drone container tries to connect to localhost:3001 it's looking inside itself for a Git server? Of course it can't find it. The Git server is running on my host machine, not inside the Drone container.

My mind was blown.

The Solution

The answer was simpler than I thought — use the Docker bridge network IP. Every Docker installation creates a bridge network and the host is accessible at a specific IP on that network.

# Find the Docker bridge IP
ip addr show docker0

# Output showed:
inet 172.17.0.1/16 brd 172.17.255.255 scope global docker0

There it was: 172.17.0.1

Updated the Drone configuration:

DRONE_GITEA_SERVER=http://172.17.0.1:3001

Restarted Drone. Held my breath. Clicked activate repository. It worked. The webhook connected. The build triggered. The clone succeeded. After 3 hours of debugging the fix was literally changing one IP address.

What I Actually Learned

This wasn't just about fixing an IP address. This taught me fundamental concepts about how containers work.

Containers Are Isolated

When you run a container it gets its own network namespace. From inside that container:

  • localhost means the container itself
  • 127.0.0.1 also means the container
  • The host machine is a completely different entity

Container Networking Has Layers

To communicate between containers and host you need to understand the network topology. Your host machine has the Docker bridge IP at 172.17.0.1. Each container gets its own IP like 172.17.0.2, 172.17.0.3 and so on.

From inside a container:

  • To reach the host use 172.17.0.1
  • To reach another container use the service name or its IP
  • To reach itself use localhost

Different Operating Systems Handle This Differently

Mac and Windows with Docker Desktop provide a magic DNS name called host.docker.internal. Linux doesn't have this magic — you use the bridge IP directly. If you're using Docker Compose you use service names for container-to-container communication.

The Fix in Practice

Here's what my working Drone pipeline looks like now:

kind: pipeline
type: docker
name: default

clone:
  disable: true

steps:
  - name: clone
    image: alpine/git
    commands:
      - git clone http://172.17.0.1:3001/bayo/sample-app.git .
      - git checkout $DRONE_COMMIT

  - name: test
    image: node:18-alpine
    commands:
      - npm install
      - npm test

  - name: build
    image: plugins/docker
    settings:
      registry: 172.16.18.128:8888
      repo: 172.16.18.128:8888/library/sample-app
      tags:
        - latest
        - build-${DRONE_BUILD_NUMBER}
      username: admin
      password:
        from_secret: harbor_password
      insecure: true

The key changes — disabled default clone because it was using the wrong URL, created a custom clone step with the correct IP, and used 172.17.0.1 instead of localhost.

Quick Reference

If you're building something similar here's what to remember:

When to use what:

  • Container trying to reach host machine: use bridge IP 172.17.0.1
  • Container trying to reach another container on same network: use service name like gitea:3000
  • Container trying to reach itself: use localhost
  • Host machine trying to reach container: use localhost with the mapped port

Quick commands:

# Find your Docker bridge IP
ip addr show docker0 | grep "inet "

# Inspect a Docker network
docker network inspect bridge

# Test connectivity from inside a container
docker run --rm alpine ping -c 1 172.17.0.1

# Check what networks exist
docker network ls

The Bigger Lesson

This 3-hour debugging session taught me something more important than Docker networking — read error messages carefully but don't trust them blindly. The error said "Connection refused" which made me think it was a firewall or port issue. But the real problem was that I was connecting to the wrong place entirely. The container was refusing the connection because there was nothing listening on localhost:3001 inside the container.

What's Next

This was just one problem in my DevOps homelab journey. I've documented 11 major issues I faced while building a complete infrastructure platform — self-hosted Git server, CI/CD pipeline, container registry, monitoring stack, and cloud deployment. Each problem taught me something new. Each solution made me a better engineer.

Summary

  • Problem: Drone CI couldn't clone from Gitea running on localhost:3001
  • Why: Inside a container localhost means the container itself not the host machine
  • Solution: Use Docker bridge IP 172.17.0.1 instead of localhost
  • Lesson: Containers are isolated. Network namespaces are real. Read the docs first.
  • Time spent: 3 hours
  • Time saved in the future: Countless

Did you hit this same issue? How long did it take you to figure it out? Building your own homelab? What problems are you facing? Drop a comment below.

B

Omobayonle Ogundele

DevOps Engineer based in Lagos, Nigeria. Building reliable infrastructure and sharing logs from the edge of production.

Comments (0)

No comments yet. Be the first!

LEAVE_RESPONSE