Dash0 Raises $110M Series B at $1B Valuation

  • 10 min read

How to Access a Host Port from Inside a Docker Container

Reaching a service running on your host machine from inside a Docker container looks like it should just work. It doesn't. You run curl http://localhost:5432 from the container, expecting to hit Postgres on the host, and instead get connection refused.

This happens because every container has its own network namespace with its own loopback interface. When you say localhost inside the container, you're talking to the container itself, not the machine it's running on. Docker provides a few ways to reach across that boundary, and the right one depends on your platform and how you're starting the container.

This article covers host.docker.internal on Docker Desktop, the host-gateway workaround for Linux, the extra_hosts directive in Docker Compose, and host networking mode for cases where you want to skip Docker's network stack entirely.

On Docker Desktop: host.docker.internal works out of the box

On macOS and Windows, Docker Desktop ships with a special DNS name, host.docker.internal, that resolves to the host machine's IP from inside any container. No flags, no configuration.

Start a container and reach a host service like this:

bash
12
docker run --rm alpine \
wget -qO- http://host.docker.internal:8080

The same hostname works in any application code or environment variable. If your container runs a Node.js app that needs to connect to a Postgres database running on the host, set DB_HOST=host.docker.internal and it just works.

This hostname has been available on Docker Desktop since version 18.03 (March 2018), and it's the recommended approach for any local development workflow on Mac or Windows.

On Linux: enable host.docker.internal with host-gateway

Linux Docker doesn't ship host.docker.internal by default, because the engine runs directly on the host instead of inside a virtual machine. Starting from Docker Engine 20.10 (December 2020), you can map it explicitly with the --add-host flag and the special value host-gateway:

bash
123
docker run --rm \
--add-host=host.docker.internal:host-gateway \
alpine wget -qO- http://host.docker.internal:8080

The host-gateway value is replaced at container start with the IP of the host gateway (typically 172.17.0.1 on the default bridge network), and Docker writes the mapping into the container's /etc/hosts file. Once that's set up, the hostname works the same way as on Docker Desktop.

You can verify the mapping from inside the container:

bash
123
docker run --rm \
--add-host=host.docker.internal:host-gateway \
alpine cat /etc/hosts

The output will include a line like this:

text
1
172.17.0.1 host.docker.internal

If you've configured a custom bridge network, host-gateway resolves to whatever the actual gateway IP is at runtime, so you don't have to hardcode it. If you need to override the resolution explicitly, the dockerd daemon also supports a --host-gateway-ip flag that changes what host-gateway resolves to.

In Docker Compose: the extra_hosts directive

The Compose equivalent of --add-host is the extra_hosts directive. Add it to any service that needs to reach the host:

yaml
12345678
services:
app:
image: myapp:latest
extra_hosts:
- "host.docker.internal:host-gateway"
environment:
DB_HOST: host.docker.internal
DB_PORT: "5432"

On Docker Desktop the directive is harmless but unnecessary, because the mapping already exists. On Linux it's required. Including it unconditionally is the cleanest way to get identical behavior across all three platforms, which is what you want for a setup that developers on different machines will share.

One gotcha: host-gateway works in extra_hosts at runtime, but historically didn't resolve correctly in the build: section of Compose files. If you need to access the host during image build, use docker buildx build --add-host=host.docker.internal=host-gateway directly (buildx accepts both = and : as separators), or pass the host argument through the build configuration.

Host networking mode: skip the namespace entirely

The other approach is to skip the container's network namespace and run the container directly on the host's network stack. With host networking, there's nothing to cross between container and host. The container sees localhost, 127.0.0.1, and all the host's interfaces exactly as the host does.

The command itself is a single flag:

bash
12
docker run --rm --network host alpine \
wget -qO- http://localhost:8080

Host networking has a few significant tradeoffs.

It works natively on Linux only. Docker Desktop added an opt-in version in 4.34 (September 2024), but it operates at layer 4, supports TCP and UDP only, and doesn't give the container direct access to the host's network interfaces. You also have to enable it from Settings > Resources > Network.

The container shares the host's port space, so -p flags are ignored and any port the container tries to bind will conflict with host processes already using it. Docker will print a warning: Published ports are discarded when using host network mode.

You also lose Docker's network isolation. Anything the container does is visible on the host's network stack, which has real security implications for production workloads.

It's the right tool for high-throughput workloads where the network address translation (NAT) overhead of bridge mode matters, or for services that need to bind to dynamic or wide port ranges. For most development cases, host.docker.internal is the better default.

Common pitfalls

The hostname mapping itself rarely breaks. What catches most people out is the host service on the other end. A lot of databases and dev servers bind to 127.0.0.1 by default, and from the host's point of view the bridge gateway is a different interface entirely. The container correctly routes to the host machine, but the host service refuses the connection because it's not listening on the bridge interface.

The fix is to bind the service to 0.0.0.0 (all interfaces) or to the Docker bridge IP specifically. For Postgres, set listen_addresses = '*' in postgresql.conf and add a matching pg_hba.conf entry allowing the Docker subnet. For a Node.js dev server, pass --host 0.0.0.0. For MySQL, set bind-address = 0.0.0.0 in my.cnf and remember that MySQL also tracks the source host in user permissions, so 'root'@'localhost' won't accept connections from 172.17.0.x even after you fix the bind address. Binding to 0.0.0.0 exposes the service to your local network, so on shared networks you'll want firewall rules to restrict access to the Docker subnet.

Firewall rules can also intercept traffic before it reaches the host service. UFW and firewalld both block forwarded traffic by default on Linux, and Docker bridge traffic counts as forwarded. If host.docker.internal resolves but connections time out instead of being refused, check whether your firewall is allowing traffic from 172.17.0.0/16, or whichever subnet your bridge network is using.

One more thing worth knowing: if you've defined a custom bridge network in Compose or with docker network create, the gateway IP will be different from 172.17.0.1. The host-gateway value handles this automatically, which is why hardcoding 172.17.0.1 in extra_hosts is brittle compared to using the host-gateway keyword.

Final thoughts

Once your container can talk to host services, the next question is which connections are happening and where the latency comes from. That visibility matters more once you move beyond local development, where a container talking to a host process is just one of many cross-boundary calls in your system.

Dash0's infrastructure monitoring tracks container resource usage alongside real-time logs and distributed traces, so you can see exactly which services your containers are reaching and how the network is behaving. The platform is built on OpenTelemetry, so you instrument once and see traffic across containers, hosts, and the services running on both. Start a free trial to see your containerized workloads in one view. No credit card required.