BACK TO ENTRIES
LOG_ENTRY: 2026.03.11 · 20 TELEMETRY_HITS

How a DHCP IP Change Silently Broke My Entire CI/CD Pipeline

B

Omobayonle Ogundele

MAIN_NODE: DEVOPS_ENGINEER

It was a regular Tuesday. I had just finished building a new feature for my portfolio
site, wrote a clean commit message, and pushed to Gitea. I leaned back, expecting
Drone CI to do its thing — build the Docker image, push to Harbor, SSH into the Oracle
Cloud server and deploy.

Instead, I got this:

Error response from daemon: Get "http://10.0.0.2/v2/": net/http: request canceled 
while waiting for connection (Client.Timeout exceeded while awaiting headers)

The pipeline was dead. And I had no idea why.


My Setup

Before I get into the debugging, here's a quick overview of my CI/CD setup so this
makes sense:

  • Gitea — self-hosted Git server running locally on my machine
  • Drone CI — self-hosted CI/CD runner, also running locally
  • Harbor — self-hosted private Docker registry, running locally
  • Oracle Cloud — where the actual portfolio site is deployed
  • WireGuard — VPN tunnel connecting my local machine to the Oracle server privately

The flow looks like this:

Push to Gitea → Drone CI triggers → Builds Docker image → 
Pushes to Harbor → SSHs into Oracle Cloud → Pulls image → Deploys

Everything runs on my local homelab machine except the final deployment target
which is Oracle Cloud. The pipeline had been working perfectly for weeks.


The Error

The build step was failing at the Docker push stage with a connection timeout trying
to reach Harbor at http://10.0.0.2/v2/.

My first instinct was that Harbor had crashed. I checked:

docker ps | grep harbor

All Harbor containers were up and healthy. So Harbor was running fine. Something
else was wrong.


Digging Deeper

I tried to curl Harbor directly:

curl -I http://10.0.0.2/v2/

The terminal just hung. No response. Nothing.

That's when it hit me — what if 10.0.0.2 is no longer the right IP?

I checked my machine's current IP:

ip addr show | grep inet

The output showed my machine's IP was now 172.16.18.128. Not 10.0.0.2.

I tested Harbor on the new IP:

curl -I http://172.16.18.128/v2/
HTTP/1.1 401 Unauthorized
Www-Authenticate: Bearer realm="http://10.0.0.2/service/token"

Got a response — but Harbor was still identifying itself as 10.0.0.2 internally.
That meant two things needed to be fixed:

  1. harbor.yml — the hostname config for Harbor itself
  2. .drone.yml — the registry IP hardcoded in the pipeline

Why Did the IP Change?

My machine gets its IP address from the router via DHCP — Dynamic Host
Configuration Protocol. DHCP automatically assigns IP addresses to devices on a
network, but it doesn't guarantee the same IP every time.

For weeks my router had been consistently giving my machine 10.0.0.2. Then one
day — probably after a router restart or a lease expiry — it assigned a different
IP. My entire pipeline was hardcoded to 10.0.0.2 so it all broke silently.

This is a classic infrastructure mistake: relying on a dynamic IP as if it were static.


The First Fix Attempt (That Broke Everything)

My first instinct was to set a static IP on my machine so it would always be
10.0.0.2. I tried:

nmcli connection modify "Showmax" ipv4.method manual \
  ipv4.addresses 10.0.0.2/24 \
  ipv4.gateway 172.16.16.16 \
  ipv4.dns "8.8.8.8,8.8.4.4"
nmcli connection down "Showmax"
nmcli connection up "Showmax"

The connection came back up — but the internet stopped working completely. Websites
wouldn't load, DNS wasn't resolving, nothing.

The problem was that my network is managed by an external router I don't control.
Setting a manual IP that conflicted with the router's DHCP range broke routing
entirely. I had to revert immediately:

nmcli connection modify "Showmax" ipv4.method auto \
  ipv4.addresses "" \
  ipv4.gateway "" \
  ipv4.dns ""
nmcli connection down "Showmax"
nmcli connection up "Showmax"

Internet restored. Back to square one.


The Actual Fix — An Automation Script

Since I couldn't control the router, I needed a different approach. Instead of
fighting the dynamic IP, I decided to embrace it — write a script that detects
the current IP and automatically updates everything that needs to know about it.

Here's the script I wrote at ~/update-harbor-ip.sh:

#!/bin/bash

# Get current machine IP
CURRENT_IP=$(ip route get 1 | grep -oP 'src \K\S+')

echo "🔍 Current IP: $CURRENT_IP"

# Update harbor.yml
sed -i "s/^hostname: .*/hostname: $CURRENT_IP/" \
  /home/bayo/homelab/cicd/harbor/harbor.yml
echo "✅ Updated harbor.yml hostname to $CURRENT_IP"

# Restart Harbor
cd /home/bayo/homelab/cicd/harbor
docker compose down
sudo ./prepare
docker compose up -d
echo "✅ Harbor restarted at http://$CURRENT_IP"

# Update .drone.yml
cd /home/bayo/my-portfolio

sed -i "s|registry: .*|registry: $CURRENT_IP|g" .drone.yml
sed -i "s|repo: .*/library/portfolio|repo: $CURRENT_IP/library/portfolio|g" .drone.yml
sed -i "s|docker login -u admin -p Harbor12345 .*|docker login -u admin -p Harbor12345 $CURRENT_IP|g" .drone.yml
sed -i "s|docker pull .*/library/portfolio:latest|docker pull $CURRENT_IP/library/portfolio:latest|g" .drone.yml

echo "✅ Updated .drone.yml with new IP"

# Commit and push
git add .drone.yml
git commit -m "update harbor IP to $CURRENT_IP"
git push

echo ""
echo "🚀 Done! Pipeline will trigger automatically."
echo "🌐 Harbor is at http://$CURRENT_IP"

The key line is this one:

CURRENT_IP=$(ip route get 1 | grep -oP 'src \K\S+')

This gets the machine's actual outbound IP address regardless of what the router
assigned. No hardcoding, no assumptions.

Now whenever my IP changes I just run:

~/update-harbor-ip.sh

And in about 60 seconds Harbor is reconfigured, restarted and the pipeline config
is updated and pushed automatically.


The Pipeline Worked Again

After running the script and waiting for Harbor to restart:

curl -I http://172.16.18.128/v2/
# HTTP/1.1 401 Unauthorized ✅
# Www-Authenticate: Bearer realm="http://172.16.18.128/service/token" ✅

Retriggered the pipeline:

git commit --allow-empty -m "retrigger pipeline"
git push

Build passed. Image pushed to Harbor. Deployed to Oracle Cloud. Site was live. ✅


Lessons Learned

1. Never treat a DHCP IP as permanent infrastructure.
If a service needs to be reachable by name or address, it needs a fixed address —
either a static IP, a DHCP reservation on the router, or a hostname that resolves
consistently.

2. Self-hosted infrastructure on a laptop is fragile.
Running Harbor, Gitea and Drone on a personal machine means any network change,
reboot or router restart can silently break your pipeline. This is fine for learning
but not for anything production-critical.

3. Automation beats manual intervention every time.
Instead of manually editing three config files every time the IP changes, a single
script handles everything in under a minute.

4. The proper long-term fix.
The right solution here is either:
- A DHCP reservation on the router — tell it to always give this machine the
same IP
- Move Harbor to a cloud server with a fixed public IP so it's always reachable
regardless of what happens to the local machine

I'll be moving Harbor to a dedicated server eventually. For now the script does the job.


What's Next

This whole incident reminded me how much I still have to learn about networking and
infrastructure. I'm planning to:

  • Move the registry to a proper cloud server
  • Set up Nginx as a proper reverse proxy for Harbor with a domain name
  • Add health checks to the pipeline so it fails fast with a clear error instead of
    timing out silently

If you're running a similar homelab setup and have run into this before — or have a
better solution — I'd love to hear about it in the comments below.

Thanks for reading. Keep building. 🚀

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