Tech Digest

Guide

How does a Docker container reach a service on the host in Linux?

Three things have to be true at once: the name resolves, the host service listens on an address the container can reach, and the host firewall lets the packet in. Each failure has its own error, so read the error before you change anything.

Last reviewed

How does a Docker container reach a service running on the Linux host?

Add extra_hosts: ["host.docker.internal:host-gateway"] to the service, because Docker Engine on Linux does not define that name by itself. It then resolves to the default bridge's address (docker0, often 172.17.0.1 but not always). The host service must listen on that address or on 0.0.0.0, not on 127.0.0.1, and the host firewall must accept traffic from the container subnets, because this traffic goes through INPUT. If you can, run the service in a container on the same network instead.

Add host.docker.internal:host-gateway to the container's extra_hosts, make the host service listen on the Docker bridge address instead of 127.0.0.1, and let the container subnets through the host firewall. All three are needed, and each one fails with a different error. On Linux, Docker Engine Engine does not define host.docker.internal at all, so a compose file copied from a Mac tutorial fails at the first step. If the service can run in a container instead, put it on the same compose network and use the service name, which needs none of this.

Two answers circulate, and each fixes one layer#

The first answer is "use host.docker.internal". It is right on Docker Desktop, whose documentation says the name "resolves to the internal IP address of your host", and that is where most tutorials are written. On a Linux server the same container gets no such name. We started a plain container on Docker Engine 28.1.1 and looked it up: getent exited with status 2, wget printed bad address 'host.docker.internal:51234', and Python's urllib gave Name does not resolve.

The second answer is "use 172.17.0.1". That is the usual address of the docker0 bridge, and it often works, which is why it spreads. But it is a default, not a constant. On the same test server, daemon.json set default-address-pools to 10.201.0.0/16, docker0 came up as 10.201.0.1, and a request to 172.17.0.1 timed out after five seconds.

Both answers fix only the name. Even with the right address, the request still fails if the service listens on loopback or the firewall drops it, and that is where most people get stuck after the first fix.

Read the error: it tells you which layer failed#

Every row below was observed from a container on Engine 28.1.1 against a small HTTP server on the host, with the same request made from the default bridge and from a compose network.

What you seeWhich layer failedFix
bad address, Name does not resolve, no such hostThe name. Linux Engine does not define host.docker.internalAdd extra_hosts with host-gateway
Connection refused, immediatelyThe listener. The service is bound to 127.0.0.1 or to some other addressBind to the bridge address or 0.0.0.0
Timeout after several secondsThe path. A wrong hardcoded IP, or the host firewall dropping the packetUse host-gateway; allow the port from Docker subnets
Works from the host with curl localhost, fails from the containerThe listener, almost alwaysAs above

A refusal comes back at once because the kernel answers that nothing is listening on that address and port. A timeout means the packet vanished, and on a server with a firewall that is the firewall.

Layer 1: give the container the name#

In compose:

yaml
services:
  open-webui:
    image: ghcr.io/open-webui/open-webui:main
    extra_hosts:
      - "host.docker.internal:host-gateway"

With docker run, the equivalent is --add-host host.docker.internal:host-gateway. Docker's CLI reference describes host-gateway as a special value that "resolves to the internal IP address of the host", and calls host.docker.internal the conventional name for it. The name itself is just a line in the container's /etc/hosts; on our test it read 10.201.0.1 host.docker.internal.

Which address that is matters for the next two layers. The dockerd reference says host-gateway "resolves to the IPv4 address of the default bridge, and its IPv6 address if it has one". It does not change when the container is on a compose network. Our compose container's own gateway was 10.201.10.1, its /etc/hosts still pointed host.docker.internal at 10.201.0.1, and requests to it worked. You can override the target with host-gateway-ips in daemon.json if you need a different host address.

Find the address on your own host:

bash
ip -4 addr show docker0 | awk '/inet/{print $2}'

Layer 2: the host service has to listen where the container arrives#

A container's request to host.docker.internal arrives at the host on the bridge address, not on loopback. A service bound to 127.0.0.1 never sees it. On our test host a server bound to 127.0.0.1:51235 answered curl on the host and refused every container, through host-gateway, at once. A second detail: a server bound only to 10.201.0.1 refused requests sent to the compose network's own gateway, 10.201.10.1, even though both are addresses of the same machine. Use the host-gateway name and the address it points at, not whichever gateway the container happens to have.

The best-known case is Ollama. Its FAQ is explicit: "Ollama binds 127.0.0.1 port 11434 by default." Behind Open WebUI in a container, that produces the empty model list described on the Open WebUI profile. You have two ways to change the bind address, and the difference between them is who else can reach the port.

Bind to the bridge address. Containers can reach it, the LAN cannot. For Ollama installed with the Linux script, the FAQ says to set environment variables with systemctl edit ollama.service:

ini
[Unit]
After=docker.service

[Service]
Environment="OLLAMA_HOST=172.17.0.1:11434"

Then systemctl daemon-reload and systemctl restart ollama. Use your own docker0 address. The After= line is there because the bridge address only exists once Docker has created docker0. A service that tries to bind an address the host does not have fails with OSError: [Errno 99] Cannot assign requested address, which is the error you would see at boot if the service started first.

Bind to 0.0.0.0. This is what the Ollama FAQ's example does (OLLAMA_HOST "0.0.0.0:11434"), and it is the common advice. It makes the port reachable from every interface, including your LAN and, without a firewall, the internet. For Ollama that is a real exposure: its API documentation states that "the local API at http://localhost:11434 does not require authentication." Anyone who can reach the port can pull, run and delete models. The site's ChatGPT replacement page covers the same risk from the user side.

Layer 3: this traffic goes through the host firewall#

This is the reverse of the problem in Why UFW does not block Docker ports. Docker's documentation explains that traffic to published ports "gets diverted before it goes through the ufw firewall", because it is rewritten in the nat table and forwarded. A container talking to the host is different: the destination is one of the host's own addresses, so the packet is delivered locally, through the INPUT chain, where UFW and most hand-written firewalls keep their rules. Docker does not add anything there for you. On our test host, iptables -S INPUT contained no Docker rule at all.

That host had an INPUT policy of DROP with a list of allowed ports. A server on the bridge address at port 51234, inside an allowed range, answered every container. The same server on port 11434, which the policy did not allow, timed out from both the default bridge and the compose network, while curl on the host itself succeeded. That is the signature: works locally, times out from a container.

With UFW, allow the port from the Docker subnets only. Read the subnet of each network that needs access:

bash
docker network inspect -f '{{range .IPAM.Config}}{{.Subnet}} {{end}}' bridge mystack_default
sudo ufw allow from 172.17.0.0/16 to any port 11434 proto tcp

Repeat the allow for each compose network's subnet. With firewalld, Docker's documentation says it creates a zone called docker with target ACCEPT and puts every bridge interface into it, so traffic arriving from a Docker bridge is accepted and this layer is rarely the problem.

The alternatives, and when to prefer them#

Run the service in a container on the same network. This removes all three layers. Ollama publishes an official image (ollama/ollama, data in /root/.ollama, API on 11434), and on a shared compose network Open WebUI reaches it at http://ollama:11434 by service name. Do not publish 11434 at all unless something outside the stack needs it. This is the right default when the host service exists only to serve containers. The costs are GPU passthrough, which GPUs, transcoding and local AI covers, and one more stack to update. If you are still choosing a model server, Ollama vs LocalAI compares the two common ones.

Use host networking. network_mode: host makes 127.0.0.1 inside the container the host's loopback, and our test container reached a loopback-only service this way. It is the method Open WebUI's troubleshooting page shows, with OLLAMA_BASE_URL=http://127.0.0.1:11434. Docker's documentation lists what you give up: the container's network stack "isn't isolated from the Docker host", and published ports are discarded with a warning. Open WebUI then listens on 8080 instead of the 3000 you mapped, which can collide with something else on the host.

ApproachHost service exposed to LANFirewall rule neededSurvives a changed bridge addressIsolation kept
Same compose network, service nameNoNoYesYes
host-gateway + bind to bridge addressNoYes, if INPUT defaults to denyNo, update the bind addressYes
host-gateway + bind to 0.0.0.0Yes, unless firewalledYes, if INPUT defaults to denyYesYes
network_mode: hostOnly what the service bindsNoYesNo

When this answer stops applying#

  • Docker Desktop on macOS or Windows. host.docker.internal is defined for you, so layer 1 does not apply. Nothing else here was tested there.
  • Rootless Docker and Podman. Everything above was measured on rootful Docker Engine with its default bridge. Nothing here was tested on either; Docker vs Podman covers how they differ.
  • The service is on another machine. Then it is not a host service. Use its LAN address or name, and the firewall in question is on that machine.

What to do next#

If the service only exists for your containers, move it into one and use the service name. If it has to stay on the host, bind it to the bridge address, add extra_hosts to each compose file that needs it, and allow the port from the Docker subnets only. Docker Compose conventions has the per-stack layout this fits into, and A security baseline for a home server covers the firewall defaults to check while you are in there.

Questions#

Why does host.docker.internal work on my Mac but not on my Linux server?

Docker Desktop defines it for you: its documentation says the name resolves to the internal IP address of your host. Docker Engine on Linux does not. A container started without an extra host entry has no such line in /etc/hosts, and lookups fail with bad address or Name does not resolve. Add extra_hosts: ["host.docker.internal:host-gateway"] in compose, or --add-host host.docker.internal:host-gateway with docker run.

Is 172.17.0.1 always the Docker host's address?

No. It is the usual address of the docker0 bridge, but a bip or default-address-pools setting in daemon.json moves it. On one server we tested, docker0 was 10.201.0.1 and a request to 172.17.0.1 simply timed out. Use host-gateway, which follows whatever the default bridge address is, or read it with ip -4 addr show docker0.

Why do I get connection refused from the container when curl works on the host?

The service is listening on 127.0.0.1 only. Curl on the host uses loopback, which works. The container arrives on the bridge address, where nothing listens, so the kernel refuses the connection immediately. Ollama does exactly this by default: its documentation says it binds 127.0.0.1 port 11434. Make the service listen on the bridge address or on 0.0.0.0.

Why does the connection to the host time out instead of being refused?

Something drops the packet. Either the address is wrong, such as a hardcoded 172.17.0.1 on a host whose bridge is elsewhere, or the host firewall drops it. Traffic from a container to the host goes through the INPUT chain, so a default-deny UFW or iptables policy applies to it even though it does not apply to published ports. Allow the port from your Docker subnets.

Should I use network_mode: host for Open WebUI and Ollama?

It works, and it is the method Open WebUI's troubleshooting page shows, with OLLAMA_BASE_URL=http://127.0.0.1:11434. The cost is isolation: the container shares the host's network stack, Docker discards any published ports, and Open WebUI then answers on 8080 instead of 3000. It is a reasonable choice on a single-purpose box and a poor one on a server with many stacks.

Is it safe to set OLLAMA_HOST=0.0.0.0?

Only behind a firewall you have checked. Ollama's API documentation states that the local API does not require authentication, so anything that can reach port 11434 can use it. Binding to the Docker bridge address instead, for example OLLAMA_HOST=172.17.0.1:11434, lets containers in and keeps the port off your LAN. Running Ollama itself in a container on the same compose network avoids the question.

Sources#

Published . Last reviewed . Found something out of date? Tell us and we will fix it and log the change.