Inside a container, localhost is the container itself, not your machine. To reach a service on the host, use the special hostname host.docker.internal.
The short version:
shell
1# Docker Desktop (Mac, Windows, Linux): works out of the box2docker run --rm curlimages/curl http://host.docker.internal:80003
4# Docker Engine on Linux: map the name to the host first5docker run --rm --add-host=host.docker.internal:host-gateway curlimages/curl http://host.docker.internal:8000In Docker Compose, the Linux equivalent is extra_hosts:
yaml
1services:2 app:3 image: curlimages/curl4 command: ["http://host.docker.internal:8000"]5 extra_hosts:6 - "host.docker.internal:host-gateway"If the name resolves but the connection is refused or times out, the problem is on the host side: the service listens only on 127.0.0.1, or a firewall drops the traffic. Both are covered below. All commands were tested on Docker Desktop for Mac and on Docker Engine 29 on Linux.
How host.docker.internal works
host.docker.internal is a hostname that points to your host machine from inside a container. Use it anywhere you'd normally write localhost: in a database URL, an API endpoint, or an app config.
On Docker Desktop (Mac, Windows and Linux), Docker resolves the name for you. No flags needed. In our test on Docker Desktop for Mac, even a service that listened only on 127.0.0.1 was reachable this way.
Docker Desktop also has gateway.docker.internal. It points to the gateway of Docker Desktop's internal VM, not to your machine, so don't use it to reach host services.
Docker Engine on Linux has no VM in between and doesn't create host.docker.internal by default. Without extra setup, the name simply doesn't resolve.
Docker Engine on Linux: add host-gateway
Docker Engine 20.10 and later supports a special value, host-gateway. It resolves to the host's address on the default bridge network (usually the docker0 interface). You map host.docker.internal to it, and Docker writes the line into the container's /etc/hosts.
With docker run
shell
1docker run -it --rm --add-host=host.docker.internal:host-gateway ubuntu:24.04 bashInside the container, check the mapping:
shell
1grep host.docker.internal /etc/hostsYou'll see something like:
shell
1172.17.0.1 host.docker.internalThe address depends on how Docker set up the bridge on your machine, and that's the point: you don't need to know it. The flag also works for containers on user-defined networks.
With Docker Compose
Add extra_hosts to every service that needs the host:
yaml
1services:2 app:3 image: my-app:latest4 environment:5 DATABASE_URL: postgres://app:secret@host.docker.internal:5432/app6 extra_hosts:7 - "host.docker.internal:host-gateway"Then start it as usual with docker compose up. The same file works on Docker Desktop too, so you can commit it and share it with a team on mixed operating systems.
Change the gateway address
If host-gateway should point somewhere other than the default bridge, set host-gateway-ip in /etc/docker/daemon.json (or pass --host-gateway-ip to dockerd) and restart Docker. You rarely need this.
Test the connection
A quick way to check the whole chain is a tiny HTTP server on the host and curl from a container.
Step 1: Start a test server on the host
shell
1python3 -m http.server 8000By default, the server listens on all interfaces, which is what you want for this test.
Step 2: Start a container
On Docker Desktop:
shell
1docker run -it --rm ubuntu:24.04 bashOn Docker Engine on Linux:
shell
1docker run -it --rm --add-host=host.docker.internal:host-gateway ubuntu:24.04 bashStep 3: Install curl and call the host
shell
1apt-get update && apt-get install -y curl2curl -I http://host.docker.internal:8000You should get HTTP/1.0 200 OK and a log line in the terminal where the Python server runs. Type exit to leave the container. It's removed automatically because of --rm.
Why the connection fails on Linux
Each failure has its own error, so read it first.
Could not resolve host
shell
1curl: (6) Could not resolve host: host.docker.internalThe container doesn't know the name. Add --add-host=host.docker.internal:host-gateway or extra_hosts as shown above. Python apps report the same problem as Name or service not known. That's the classic error when Open WebUI or another app in Docker tries to reach Ollama on port 11434 of the host on Linux.
Connection refused
shell
1curl: (7) Failed to connect to host.docker.internal port 8000 after 0 ms: Could not connect to serverThe name resolves, but nothing accepts the connection on that address. Most often the service on the host listens only on 127.0.0.1. Traffic from a container arrives on the bridge interface, not on loopback, so the service never sees it.
Make the service listen on all interfaces (0.0.0.0) or on the bridge address. Some common examples:
- Ollama binds
127.0.0.1:11434by default. SetOLLAMA_HOST=0.0.0.0:11434for the service:
shell
1sudo systemctl edit ollama.service2# add under [Service]:3# Environment="OLLAMA_HOST=0.0.0.0:11434"4sudo systemctl daemon-reload5sudo systemctl restart ollama- PostgreSQL: set
listen_addressesinpostgresql.confand allow the Docker subnet inpg_hba.conf. - MySQL and MariaDB: set
bind-addressin the server config. - Dev servers (Vite, Next.js, Django and others): start them with
--host 0.0.0.0or the framework's equivalent.
Listening on 0.0.0.0 also exposes the service to your network. On a laptop or a server with a public IP, protect it with a firewall rule that allows only the Docker subnets.
Connection timed out
shell
1curl: (28) Connection timed out after 5001 millisecondsA firewall on the host drops the packets. With ufw and its default deny incoming policy, this happens out of the box. Allow the port from Docker networks:
shell
1sudo ufw allow from 172.16.0.0/12 to any port 8000 proto tcpA rule like ufw allow in on docker0 covers only the default bridge. Docker Compose creates its own network with a separate br-... interface, so that rule doesn't match Compose containers. A subnet rule covers both. 172.16.0.0/12 matches Docker's default address pools; if you changed them in daemon.json, use your ranges.
On a cloud server, the provider's firewall or security group doesn't matter here: the traffic never leaves the machine.
Other ways to reach the host
Host network mode
shell
1docker run --rm --network host curlimages/curl http://127.0.0.1:8000With --network host, the container shares the host's network stack. localhost inside the container is the host, so even services bound to 127.0.0.1 are reachable. In Compose, it's network_mode: host.
The trade-offs:
- The container loses network isolation and sees every port on the host.
- Port publishing (
-p) is ignored. Docker printsWARNING: Published ports are discarded when using host network mode. - Two containers can't listen on the same port.
On Docker Engine for Linux, it works out of the box. Docker Desktop supports host networking since version 4.34 as an opt-in setting (Settings > Resources > Network > Enable host networking). It works for Linux containers and TCP/UDP only. For most setups, host.docker.internal is the simpler choice.
The bridge IP (172.17.0.1)
Many older answers hardcode 172.17.0.1, the usual address of docker0. It works until it doesn't. Docker picks a free subnet at startup, admins change it with bip, and Docker Desktop doesn't use it at all. In our Linux test, docker0 came up as 172.18.0.1 because 172.17.0.0/16 was already taken, and requests to 172.17.0.1 were refused.
host-gateway resolves the right address for you. Use it instead of a hardcoded IP.
Podman
Podman adds both host.containers.internal and host.docker.internal to /etc/hosts automatically, so most Docker examples work without flags. If it can't detect the host address, it skips the entries silently. In that case, set host_containers_internal_ip in containers.conf.
Controlling other containers: docker.sock, not host.docker.internal
host.docker.internal gives a container network access to services on the host. It doesn't let a container start, stop or inspect other containers.
For that, the container needs the Docker API. The usual way is to mount the Docker socket:
shell
1docker run --rm -v /var/run/docker.sock:/var/run/docker.sock docker:cli docker psThis is how backup tools stop containers before a snapshot, and how web UIs like Portainer manage them.
Be careful with it. Access to the Docker socket is root access to the host. Anyone who controls that container can start a new container that mounts / from the host and read or change any file. Mount the socket only into images you trust. If a tool needs just a few API calls, put a filtering proxy like docker-socket-proxy in front of the socket, or run Docker in rootless mode.
Using host.docker.internal in production
host.docker.internal is great for local development: a database on your laptop, Ollama on a GPU box, a dev server in your IDE.
In production, it usually means a dependency lives outside the container setup and is reached through a side door. Run the dependency as a container on the same Docker network and address it by service name, or give it a proper DNS name. That way you don't depend on bridge addresses and host firewall rules.
Wrapping up
From a container, localhost is the container. To reach the host, use host.docker.internal. Docker Desktop resolves it for you. On Docker Engine for Linux, add --add-host=host.docker.internal:host-gateway or extra_hosts in Compose. If it still fails, check the error: no name means a missing mapping, refused means the service listens on 127.0.0.1, and a timeout means a firewall.
New to Docker? Start with how to run a Docker container on your local machine. Need the reverse direction, from the host into a container? See how to expose a port in a Docker container.
When containers outgrow a single machine and need scaling, rolling updates and monitoring, the next step is usually Kubernetes. If you'd like help with that, take a look at our Kubernetes consulting services.
