The problem
On a recent server migration, the requirement was clear: "the DNS change goes last — everything must be verified on the new server before that." Reasonable. But it raises an obvious question: the domain still points at the old server, so how do you test the new one under real production conditions?
My first naive attempt was hitting the new server by IP: open https://203.0.113.50/ in a browser. Result: a certificate warning, and after clicking through it, Apache's 400 Bad Request. This post covers why that happens, the two methods I used instead (curl --resolve and /etc/hosts), and the traps I hit along the way.
Why IP access doesn't reach "your site"
Two separate things go wrong:
-
Certificate mismatch: the cert is issued for
shop.example.com. With an IP in the URL, the browser rightly complains the name doesn't match. Expected. -
400, or a different site entirely: even past the warning, you don't get your site. Apache selects the VirtualHost by the
Host:header (and TLS SNI). A request withHost: 203.0.113.50matches no site vhost and falls into the default vhost — which in this setup returned 400.
In other words, "testing by IP" often means you never reached the site you wanted to test. What you actually need is: keep the name as the production domain, redirect only the connection target.
Fix 1 — automated checks with curl --resolve
curl can be told "this hostname resolves to this IP" without touching DNS:
NEW_IP=203.0.113.50
curl --resolve shop.example.com:443:$NEW_IP \
-sS -o /dev/null -w '%{http_code} %{remote_ip}\n' \
https://shop.example.com/
# 200 203.0.113.50
The URL is still https://shop.example.com/, so SNI, the Host header, and certificate validation all run against the real domain — only the TCP connection goes to the new IP. Loop this over your key URLs (top page, login, cart) and you have an automated verification step for the migration script.
Trap 1: putting --resolve in a zsh variable breaks it
I bundled the common options into a variable:
R="--resolve shop.example.com:443:$NEW_IP"
curl $R https://shop.example.com/
# curl: option --resolve shop.example.com:443:203.0.113.50: is unknown
Unlike bash, zsh does not word-split an unquoted $R, so the whole string is passed as one giant option name. Use an array:
R=(--resolve shop.example.com:443:$NEW_IP)
curl "${R[@]}" https://shop.example.com/
Fix 2 — human verification with /etc/hosts
Even with curl returning 200 everywhere, a human still needs to click through the site: login, cart, checkout, admin panel. One line in /etc/hosts on my Mac:
sudo sh -c 'echo "203.0.113.50 shop.example.com" >> /etc/hosts'
Now name resolution is overridden only on my machine, and the browser shows the new server at the usual URL, with the real certificate fully valid. I could walk through login, cart, the payment screen, and the admin UI — all under genuine-domain conditions.
Trap 2: forgetting the hosts entry
This was the most dangerous one, in both directions:
- Mid-test confusion: "I haven't changed DNS — why am I seeing the new server?!" (ten minutes lost; it was my own hosts line)
- Worse, after a rollback, your machine alone keeps seeing the new server, and you misdiagnose the rollback as "not working"
Rule adopted: delete the line the moment testing ends.
Trap 3: checkout tests leave real orders
The new server was connected to the production DB during verification, so the checkout test (one bank-transfer order) persists as real data. Cancel it from the admin panel afterward — cleanup is part of the test plan.
Verifying the "only I see the new server" state
With the hosts entry in place, confirm the rest of the world still resolves to the old IP:
dig +short shop.example.com @8.8.8.8
# 203.0.113.10 ← the world still sees the old server
dig queries the resolver directly and ignores /etc/hosts, so this one-liner proves exactly the state you want: public DNS → old IP, my browser → new IP.
And on switchover day, the question I asked myself most often was: "which server am I looking at right now?" One command answers it:
curl -sS -o /dev/null -w '%{remote_ip}\n' https://shop.example.com/
Takeaways
- Testing by raw IP fails twice: certificate mismatch, and vhost mismatch (→ 400). You never reach the site.
- "Testing by IP" really means "keep the domain, swap only name resolution."
- Automate with
curl --resolve(SNI, Host, and cert all under production conditions); eyeball with/etc/hosts. - hosts entries are powerful and dangerous — removing them is part of the test.
- A checkout test against the production DB leaves a real order; cancellation is part of the plan.
- Keep
curl -w '%{remote_ip}'handy for "which server am I on?"
The real lesson: more than the DNS switch itself, what determines how calm migration day feels is being able to verify everything before it.
Top comments (0)