56.11 Reaching the host from a container: host.docker.internal and extra_hosts
Friends, sometimes the app inside a container must call something running on your own machine, not in another container: a database you installed on the laptop, a mock server in your IDE, or a metrics agent on the server. Inside a container, localhost means the container itself, not your machine. Today, step by step: why localhost fails, host.docker.internal on Docker Desktop, the host-gateway trick on Linux, extra_hosts in Compose, host networking, firewalls and listen addresses, and the safe way to use all of it.
मित्रांनो, कधी कधी container मधल्या app ला दुसऱ्या container मधलं नाही, तर तुमच्या स्वतःच्या machine वर चालणारं काहीतरी call करावं लागतं: laptop वर install केलेला database, IDE मधला mock server, किंवा server वरचा metrics agent. Container मध्ये localhost म्हणजे container स्वतः, तुमचं machine नाही. आज step by step: localhost का fail होतं, Docker Desktop वर host.docker.internal, Linux वर host-gateway trick, Compose मध्ये extra_hosts, host networking, firewalls आणि listen addresses, आणि हे सगळं सुरक्षितपणे कसं वापरायचं.
दोस्तों, कभी कभी container के app को किसी दूसरे container का नहीं, बल्कि आपकी अपनी machine पर चल रहा कुछ call करना होता है: laptop पर install किया database, IDE का mock server, या server पर metrics agent. Container के अंदर localhost का मतलब container खुद है, आपकी machine नहीं. आज step by step: localhost क्यों fail होता है, Docker Desktop पर host.docker.internal, Linux पर host-gateway trick, Compose में extra_hosts, host networking, firewalls और listen addresses, और यह सब सुरक्षित तरीके से कैसे इस्तेमाल करें.
1. Start from a story
At Freshworks, a developer named Aditya ran PostgreSQL directly on his Ubuntu laptop for years. When he moved the API into a container, it failed with connection refused to localhost:5432.
He tried host.docker.internal because a blog said so. It worked for his colleague on a Mac but not for him on Linux, where the name did not exist.
His lead explained three things. localhost inside a container is the container. On Docker Desktop, host.docker.internal is built in. On Linux you add it yourself with --add-host host.docker.internal:host-gateway, and Postgres must listen on the Docker bridge, not only on 127.0.0.1.
Ten minutes later the same compose file worked on Mac, Windows and Linux. Today we do the same.
2. Why localhost fails
Step 1: localhost inside a container
Why: Each container has its own network.
What: A container has its own network namespace with its own loopback. Calling localhost:5432 from inside reaches port 5432 of the container, where nothing listens.
Example 1: Host service works
Type this:
psql -h localhost -U app -c 'select 1' -t
You will see:
1
What it means: Postgres on the laptop.
Example 2: Inside fails
Type this:
docker run --rm postgres:16-alpine psql -h localhost -U app -c 'select 1'
You will see:
psql: error: connection to server at "localhost" (127.0.0.1), port 5432 failed: Connection refused
What it means: Nothing listens in the container.
Example 3: See the separate loopback
Type this:
docker run --rm alpine ip -4 addr show lo | grep inet
You will see:
inet 127.0.0.1/8 scope host lo
What it means: Its own 127.0.0.1.
Step 2: Find the host from the container
Why: The bridge gateway.
What: On Linux the default bridge gateway, usually 172.17.0.1, is the host's address as seen from containers. A user-defined network has its own gateway.
Example 4: Default route
Type this:
docker run --rm alpine ip route | head -1
You will see:
default via 172.17.0.1 dev eth0
What it means: The gateway is the host.
Example 5: On the host
Type this:
ip -4 addr show docker0 | grep inet
You will see:
inet 172.17.0.1/16 brd 172.17.255.255 scope global docker0
What it means: Same address.
Example 6: A user network gateway
Type this:
docker network create devnet >/dev/null && docker network inspect devnet --format '{{(index .IPAM.Config 0).Gateway}}'
You will see:
172.18.0.1
What it means: Different per network, so do not hardcode IPs.
3. host.docker.internal step by step
Step 3: On Docker Desktop
Why: Built in.
What: Docker Desktop on Mac and Windows adds the name host.docker.internal automatically. It points at your machine, whatever the network.
Example 7: Resolve it
Type this:
docker run --rm alpine getent hosts host.docker.internal
You will see:
192.168.65.254 host.docker.internal
What it means: Desktop's special address.
Example 8: Call a host service
Type this:
docker run --rm curlimages/curl -s http://host.docker.internal:8000/health
You will see:
{"status":"ok"}
What it means: A server running in your IDE.
Example 9: Database
Type this:
docker run --rm -e PGPASSWORD=example-only postgres:16-alpine psql -h host.docker.internal -U app -t -c 'select 1'
You will see:
1
What it means: The laptop database from a container.
Step 4: On Linux with host-gateway
Why: Add the name yourself.
What: Plain Docker Engine on Linux does not create the name. --add-host host.docker.internal:host-gateway adds it, and Docker fills in the right gateway address for that network.
Example 10: Without it
Type this:
docker run --rm alpine getent hosts host.docker.internal ; echo exit=$?
You will see:
exit=2
What it means: Name not found on Linux.
Example 11: With --add-host
Type this:
docker run --rm --add-host host.docker.internal:host-gateway alpine getent hosts host.docker.internal
You will see:
172.17.0.1 host.docker.internal
What it means: Now it resolves to the bridge gateway.
Example 12: In /etc/hosts
Type this:
docker run --rm --add-host host.docker.internal:host-gateway alpine tail -1 /etc/hosts
You will see:
172.17.0.1 host.docker.internal
What it means: Docker writes it into the hosts file.
Step 5: extra_hosts in Compose
Why: Same on every OS.
What: Put extra_hosts in the compose file so the same file works on Mac, Windows and Linux. On Desktop it simply repeats what is already there.
Example 13: The file
Type this:
services:
api:
image: shop-api:dev
environment:
DB_HOST: host.docker.internal
extra_hosts:
- "host.docker.internal:host-gateway"
You will see:
(compose.yaml)
What it means: Portable for the whole team.
Example 14: Run
Type this:
docker compose up -d && docker compose exec api getent hosts host.docker.internal
You will see:
172.20.0.1 host.docker.internal
What it means: The gateway of the project network.
Example 15: Other custom names
Type this:
docker run --rm --add-host api.partner.test:10.0.5.20 alpine getent hosts api.partner.test
You will see:
10.0.5.20 api.partner.test
What it means: extra_hosts also maps any name to any IP.
4. Make the host service reachable
Step 6: Listen address on the host
Why: The common second failure.
What: If the host service listens only on 127.0.0.1, traffic from the Docker bridge is refused. It must also listen on the bridge address, or on all addresses with a firewall.
Example 16: Check
Type this:
sudo ss -tlnp | grep 5432
You will see:
LISTEN 0 244 127.0.0.1:5432 ...
What it means: Only loopback.
Example 17: Error from the container
Type this:
docker run --rm --add-host host.docker.internal:host-gateway -e PGPASSWORD=example-only postgres:16-alpine psql -h host.docker.internal -U app -c 'select 1'
You will see:
connection to server at "host.docker.internal" (172.17.0.1), port 5432 failed: Connection refused
What it means: Name works; listen address is the problem.
Example 18: Listen on the bridge too
Type this:
sudo sed -i "s/^#\?listen_addresses.*/listen_addresses = 'localhost,172.17.0.1'/" /etc/postgresql/16/main/postgresql.conf && sudo systemctl restart postgresql && sudo ss -tlnp | grep 5432 | wc -l
You will see:
2
What it means: Loopback and the bridge, not the whole world.
Step 7: Allow the client in the app's own rules
Why: Postgres has its own access list.
What: Many services have their own allow list. For Postgres, pg_hba.conf must allow the Docker subnet with a password method.
Example 19: Add a rule
Type this:
printf '%s\n' 'host appdb app 172.16.0.0/12 scram-sha-256' | sudo tee -a /etc/postgresql/16/main/pg_hba.conf && sudo systemctl reload postgresql
You will see:
host appdb app 172.16.0.0/12 scram-sha-256
What it means: Docker networks only, password required.
Example 20: Try again
Type this:
docker run --rm --add-host host.docker.internal:host-gateway -e PGPASSWORD=example-only postgres:16-alpine psql -h host.docker.internal -U app -d appdb -t -c 'select 1'
You will see:
1
What it means: Connected.
Example 21: From a user network
Type this:
docker run --rm --network devnet --add-host host.docker.internal:host-gateway -e PGPASSWORD=example-only postgres:16-alpine psql -h host.docker.internal -U app -d appdb -t -c 'select 2'
You will see:
2
What it means: 172.18.0.0/16 is inside the rule too.
Step 8: The host firewall
Why: ufw and firewalld can block it.
What: A host firewall may drop traffic from docker0 to the host. Allow only the Docker subnet to the one port you need.
Example 22: Blocked
Type this:
docker run --rm --add-host host.docker.internal:host-gateway alpine nc -zv -w 3 host.docker.internal 5432
You will see:
nc: host.docker.internal (172.17.0.1:5432): Operation timed out
What it means: A timeout, not a refusal, points to a firewall.
Example 23: Allow just this
Type this:
sudo ufw allow from 172.16.0.0/12 to any port 5432 proto tcp
You will see:
Rule added
What it means: Narrow rule.
Example 24: Works
Type this:
docker run --rm --add-host host.docker.internal:host-gateway alpine nc -zv -w 3 host.docker.internal 5432
You will see:
host.docker.internal (172.17.0.1:5432) open
What it means: Reachable.
Step 9: Debug the path step by step
Why: Name, then port, then app.
What: When a call to the host fails, test in order: does the name resolve, is the port open, does the app answer. Each step points to a different fix, so do not skip ahead.
Example 25: Step 1: name
Type this:
docker run --rm --add-host host.docker.internal:host-gateway alpine getent hosts host.docker.internal
You will see:
172.17.0.1 host.docker.internal
What it means: If empty, fix extra_hosts.
Example 26: Step 2: port
Type this:
docker run --rm --add-host host.docker.internal:host-gateway alpine nc -zv -w 3 host.docker.internal 8000
You will see:
host.docker.internal (172.17.0.1:8000) open
What it means: Refused means listen address; timeout means firewall.
Example 27: Step 3: app
Type this:
docker run --rm --add-host host.docker.internal:host-gateway curlimages/curl -s http://host.docker.internal:8000/health
You will see:
{"status":"ok"}
What it means: The whole path works.
5. Alternatives and safety
Step 10: Host networking
Why: Share the host network completely.
What: --network host removes network isolation: localhost inside is the host's localhost. Simple for some agents on Linux, but ports clash and isolation is gone. Port publishing with -p is ignored.
Example 28: localhost works
Type this:
docker run --rm --network host -e PGPASSWORD=example-only postgres:16-alpine psql -h localhost -U app -d appdb -t -c 'select 3'
You will see:
3
What it means: Same network as the host.
Example 29: Ports are the host's
Type this:
docker run -d --name hn --network host nginx:1.27-alpine && sudo ss -tlnp | grep ':80 '
You will see:
LISTEN 0 511 0.0.0.0:80 ... nginx
What it means: nginx bound straight to host port 80.
Example 30: Clean up
Type this:
docker rm -f hn
You will see:
hn
What it means: Use host networking only when you understand the trade-off.
Step 11: Better: put the dependency in a container
Why: Often the cleanest answer.
What: For development, run the database in Compose next to the app instead of on the host. Then every developer gets the same version and nobody edits pg_hba.conf.
Example 31: Compose database
Type this:
services:
db:
image: postgres:16-alpine
environment: { POSTGRES_PASSWORD: example-only }
api:
image: shop-api:dev
environment: { DB_HOST: db }
You will see:
(compose.yaml)
What it means: The API reaches db by name.
Example 32: Up
Type this:
docker compose up -d && docker compose exec api getent hosts db
You will see:
172.20.0.2 db
What it means: Container to container.
Example 33: Same for everyone
Type this:
docker compose exec db psql -U postgres -t -c 'show server_version'
You will see:
16.4
What it means: Identical version on every laptop.
Step 12: Reach a container from the host, the other direction
Why: For completeness.
What: To call a container from the host, publish its port with -p, and bind to 127.0.0.1 if only the host should reach it.
Example 34: Publish to loopback only
Type this:
docker run -d --name w -p 127.0.0.1:8080:80 nginx:1.27-alpine && curl -s -o /dev/null -w '%{http_code}\n' localhost:8080
You will see:
200
What it means: Reachable from the host only.
Example 35: Not from the network
Type this:
curl -s -m 3 -o /dev/null -w '%{http_code}\n' http://192.168.1.20:8080 ; echo
You will see:
000
What it means: Other machines cannot reach it.
Example 36: Clean up
Type this:
docker rm -f w && docker network rm devnet
You will see:
w
devnet
What it means: Done.
6. Common mistakes and how Aditya avoids them
Mistake one: using localhost inside the container to reach the host. He uses host.docker.internal.
Mistake two: hardcoding 172.17.0.1. Different networks have different gateways, so he uses host-gateway.
Mistake three: making the host database listen on all addresses with no firewall. He listens on the bridge only and allows only Docker subnets.
Mistake four: using host networking for everything. He keeps it for special cases.
Check yourself
Q1. What does localhost mean inside a container?
The container itself.
Q2. How do you get host.docker.internal on Linux?
--add-host host.docker.internal:host-gateway or extra_hosts in Compose.
Q3. Name resolves but you get connection refused. Why?
The host service listens only on 127.0.0.1.
Q4. A timeout instead of refused suggests what?
A firewall dropping the traffic.
Friends, inside a container localhost is the container; reach the host through host.docker.internal, add it with host-gateway on Linux, put it in extra_hosts for teams, and open the host service only to Docker subnets. Did you understand? Let us try it once more. Run a small Python web server on your host and call it from a container on Linux and, if you have it, on Docker Desktop, with the same compose file.
मित्रांनो, container मध्ये localhost म्हणजे container; host पर्यंत host.docker.internal ने पोहोचा, Linux वर host-gateway ने तो जोडा, teams साठी extra_hosts मध्ये ठेवा, आणि host service फक्त Docker subnets साठी उघडा. तुमच्या host वर एक छोटा Python web server चालवा आणि त्याच compose file ने Linux वरच्या container मधून आणि असल्यास Docker Desktop वरून call करा.
दोस्तों, container में localhost मतलब container; host तक host.docker.internal से पहुँचिए, Linux पर host-gateway से उसे जोड़िए, teams के लिए extra_hosts में रखिए, और host service सिर्फ Docker subnets के लिए खोलिए. अपने host पर एक छोटा Python web server चलाइए और उसी compose file से Linux के container से और हो सके तो Docker Desktop से call कीजिए.
Ravindra Bagale's Tip
Always add the extra_hosts host-gateway line to team compose files. Mac users will not notice it, and Linux users will thank you.
Team compose files मध्ये नेहमी extra_hosts host-gateway ची ओळ जोडा. Mac users ना ती जाणवणार नाही, आणि Linux users तुमचे आभार मानतील.
Team compose files में हमेशा extra_hosts host-gateway की line जोड़िए. Mac users को पता भी नहीं चलेगा, और Linux users आपका शुक्रिया करेंगे.
Lab: try at home
- Show that localhost inside a container is not the host.
- Find the bridge gateway from inside a container.
- Resolve host.docker.internal on Desktop or with host-gateway on Linux.
- Add extra_hosts to a compose file and test it.
- Fix a host service that listens only on 127.0.0.1.
- Allow only Docker subnets in the firewall.
- Move the dependency into Compose and compare.
Recap
- localhost in a container is the container.
- host.docker.internal is built into Docker Desktop.
- On Linux add it with host-gateway or extra_hosts.
- The host service must listen on the bridge and allow the subnet.
- Prefer putting dependencies in containers.