Ravindra BagaleCourses & study guides मराठी

Chapter 56: Docker CI/CD with Jenkins and ECR, Deploy to Amazon ECS, Troubleshooting, Best Practices and Interview Questions

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.

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.

Your machinelocalhost = the laptopDocker bridge docker0gateway 172.17.0.1Containerlocalhost = the containerhost.docker.internala name for the hostHost servicemust listen on the bridge too
Picture 1: what localhost means where

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.

Docker Desktop?use host.docker.internalLinux?add host-gatewayCompose for a team?extra_hosts in the fileService refuses?listen address, pg_hba, firewallCan it be a container?put it in Compose instead
Picture 2: choosing the method

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.

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.

Lab: try at home

  1. Show that localhost inside a container is not the host.
  2. Find the bridge gateway from inside a container.
  3. Resolve host.docker.internal on Desktop or with host-gateway on Linux.
  4. Add extra_hosts to a compose file and test it.
  5. Fix a host service that listens only on 127.0.0.1.
  6. Allow only Docker subnets in the firewall.
  7. 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.