The Day I Learned localhost Isn't Always localhost: A Docker Networking Journey
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:
localhostmeans the container itself127.0.0.1also 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
localhostwith 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
localhostmeans the container itself not the host machine - Solution: Use Docker bridge IP
172.17.0.1instead oflocalhost - 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.
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!