What is the difference between localhost, 127.0.0.1 and 0.0.0.0?
This article offers an explanation of the differences between 127.0.0.1, localhost and 0.0.0.0 with practical examples.
Introduction
127.0.0.1 is the loopback address. This refers to the local machine. Any request sent to 127.0.0.1 is handled by the local machine itself and doesn’t propagate across the network. It is a part of the reserved IP block 127.0.0.1/8, which is entirely dedicated to loopback addresses.
localhost is just a hostname that typically resolves to 127.0.0.1 via the system’s DNS. The entry can be found in /etc/hosts file on Unix-based systems.
0.0.0.0 is a special IP address that means “all IPv4 addresses on the local machine.” A computer can have multiple network interfaces and each network can have its own IP address. When we bind a server to 0.0.0.0 it means that it will accept incoming requests from any network interface.
Practical Examples
Note: In the examples below, I have used macOS interface names (
en0andlo0). The linux equivalents of these are typicallyeth0andlo.
For all the examples explained below, we will assume that your host machine is connected to a WiFi network on the en0 interface with IP address 192.168.169.214.
Example 1: Binding to 127.0.0.1
When you run:
python3 -m http.server 8000 --bind 127.0.0.1The server is bound to the loopback interface (lo0). It will only accept incoming requests that reach the lo0 interface and will reject all requests arriving on other network interfaces.
If you try to access 192.168.169.214:8000 from another device on the same WiFi network, the request will timeout. This happens because you are trying to reach port 8000 via the en0 interface, but your server is only listening on the loopback (lo0) interface.
In this case, the only way to access your server is by visiting 127.0.0.1:8000 or localhost:8000 from the same machine on which the server is running.
Example 2: Binding to your Network Interface IP
When you run:
python3 -m http.server 8000 —-bind 192.168.169.214The server is bound to the en0 interface. It will only accept incoming requests that reach the en0 interface and will reject requests arriving on other network interfaces.
If you try to access 192.168.169.214:8000 from another device on the same WiFi network, the request will succeed. You are reaching port 8000 via the en0 interface, and the machine is listening for incoming requests on that interface.
However, if you try to access 127.0.0.1:8000 from the same machine on which the server is running, the request will be refused. This is because the server is only listening for connections on the en0 interface, but when you hit 127.0.0.1:8000, the request goes through the loopback interface (lo0).
To access the server from your host machine in this scenario, you need to use 192.168.169.214:8000 so that it reaches the en0 interface.
Example 3: Binding to 0.0.0.0
When you run:
python3 -m http.server 8000 --bind 0.0.0.0The server is bound to all interfaces. It will accept incoming requests arriving on any of the network interfaces.
If you try to access 192.168.169.214:8000 from another device on the same WiFi network, the request will succeed. You are reaching port 8000 via the en0 interface, and the machine is listening for incoming requests on all interfaces.
On the host machine where the server is running, you can access the server using either 127.0.0.1:8000 or 192.168.169.214:8000. The first URL will hit the loopback (lo0) interface, and the second URL will hit the en0 interface. Since the server is listening on all interfaces, it will accept both connection requests.
Docker and Port Mapping: Practical Examples with Express Server
We will create a basic Express server (Node + TypeScript), run it locally with different hostname bindings, then containerize it using Docker to observe the networking behavior in both environments.
Setup Instructions
-
Initialize the project
mkdir express-ts-docker && cd express-ts-docker yarn init -y -
Install dependencies
yarn add express yarn add -D typescript ts-node @types/node @types/express nodemon -
Initialize TypeScript config
npx tsc --init -
Modify
tsconfig.json// Ensure the following settings are enabled: { "compilerOptions": { "outDir": "./dist", "rootDir": "./src", "strict": true } } -
Create a basic Express server
import express, {Request, Response} from "express" const app = express() const port = 3000 const hostname = '127.0.0.1' app.get('/', (_: Request, res: Response) => { res.status(200).json({message: "Hello World!"}) }) app.listen(port, hostname, () => { console.log(`Server is listening on host: ${hostname} and port: ${port}`) }) -
Setup scripts in
package.json"scripts": { "dev": "nodemon src/index.ts", "build": "tsc", "start": "node dist/index.js" } -
Create a
.dockerignorefilenode_modules dist -
Create a
Dockerfile# Builder Stage FROM node:22.14-alpine AS builder WORKDIR /app COPY package.json yarn.lock ./ # Install all dependencies, including devDependencies (needed for TypeScript) RUN yarn install COPY . . RUN yarn build # Production Stage FROM node:22.14-alpine # Install curl RUN apk add --no-cache curl WORKDIR /app COPY package.json yarn.lock ./ # Install only production dependencies RUN yarn install --production # Copy built files from the builder stage COPY --from=builder /app/dist ./dist # Expose the application port # this does not actually publish the port, but serves as documentation and allows the container to be run with -P EXPOSE 3000 # Start the application CMD ["node", "dist/index.js"]
Now that we have completed the setup, let’s explore different hosting scenarios.
Example 1: Server Binding to 127.0.0.1
(a) Running Locally
In the index.ts file, ensure the hostname is set as:
const hostname = '127.0.0.1'Run the server:
yarn devThis starts your server on port 3000, with hostname binding at 127.0.0.1 (the loopback interface lo0).
In macOS you can run the below command to check which ports are listening for incoming connections and on which interface
$ sudo lsof -i -n -P | grep LISTENYour output should also be something similar:

It shows that, port 3000 is listening for TCP connections on host binding 127.0.0.1.
The server is bound to the loopback interface (lo0). It will only accept incoming requests that reach the lo0 interface and will reject all requests arriving on other network interfaces.
If you try to access 192.168.169.214:3000 from another device on the same WiFi network, the request will timeout. This happens because you are trying to reach port 3000 via the en0 interface, but your server is only listening on the loopback (lo0) interface.
In this case, the only way to access your server is by visiting 127.0.0.1:3000 or localhost:3000 from the same machine on which the server is running.
(b) Containerized with Docker
Let’s containerize the app and observe the behavior with port mapping:
-
Build the image
docker build --no-cache -t express-ts --file Dockerfile . -
Start a container with port mapping
docker run -d -p 3000:3000 --name express-ts express-ts:latest
This starts the Express server inside a Docker container with port 3000 of your host machine mapped to port 3000 of the container.
But is it accessbile? Not really.
When you run curl localhost:3000 from the host machine, you will receive a message saying curl: (52) Empty reply from server . This means no HTTP headers or content, simply a closed TCP connection with no HTTP payload is transmitted. This is a server-side issue and not a client-side issue.
Let’s exec into the container and check what is going wrong.
-
Exec inside the container:
docker exec -it express-ts sh -
Since the docker container is running inside a linux environment, we can use the command
netstat -tlnpto check all active TCP ports that are currently listening for incoming connections./app # netstat -tlnp -
The output will show
Active Internet connections (only servers) Proto Recv-Q Send-Q Local Address Foreign Address State PID/Program name tcp 0 0 127.0.0.1:3000 0.0.0.0:* LISTEN 1/node /app #
The “Local Address” shows 127.0.0.1:3000, meaning the server is only listening for requests arriving at the container’s loopback interface.
When you curl localhost:3000 from the host machine, it attempts to connect to the container’s eth0 interface, but the server is not listening for incoming connections at eht0 interface and hence the connection is dropped.
Why
eth0? Because the container is running in a linux environment andeth0is the default interface that gets hit when establishing a connection with a container running in the default Docker bridge network.
But wait? The “Foregin Address” still says 0.0.0.0:*. Doesn’t it mean it is listening on all interfaces?
Foreign Address: 0.0.0.0:*, just means the service is theoretically open to connections from any remote IP as long as they target 127.0.0.1, which is impossible from outside the container, hence we are unable to connect to the server.
In this case, the only way to access your server is by doing curl 127.0.0.1:3000 from inside the container.
Cleanup the container by running
docker stop express-ts && docker rm express-tsExample 2: Server Binding to 0.0.0.0
(a) Running Locally
Update the hostname in index.ts:
const hostname = '0.0.0.0'Run the server:
yarn devThis starts your server on port 3000, with hostname binding at 0.0.0.0 (all interfaces).
Check listening ports:
$ sudo lsof -i -n -P | grep LISTENYour output should be something similar:

It shows that, port 3000 is listening for TCP connections on host binding * which means 0.0.0.0
The server is now bound to all interfaces and will accept incoming requests from any network interface.
If you try to access 192.168.169.214:3000 from another device on the same WiFi network, the request will succeed. You are reaching port 3000 via the en0 interface, and the machine is listening for incoming requests on all interfaces.
On the host machine where the server is running, you can access the server using either 127.0.0.1:3000 or 192.168.169.214:3000. The first URL will hit the loopback (lo0) interface, and the second URL will hit the en0 interface. Since the server is listening on all interfaces, it will accept both connection requests.
(b) Containerized with Docker
Let’s containerize this version:
-
Build the image
docker build --no-cache -t express-ts --file Dockerfile . -
Start a container with port mapping
docker run -d -p 3000:3000 --name express-ts express-ts:latest
Is it accessible now? Yes!
When you run curl localhost:3000 from the host machine, you will receive:
{"message":"Hello World!"}Let’s check what is happening inside the container:
-
Exec inside the container
docker exec -it express-ts sh -
Check all active TCP ports that are currently listening for incoming connections
/app # netstat -tlnp -
The output will show
Active Internet connections (only servers) Proto Recv-Q Send-Q Local Address Foreign Address State PID/Program name tcp 0 0 0.0.0.0:3000 0.0.0.0:* LISTEN 1/node
The “Local Address” now shows 0.0.0.0:3000, meaning the server is listening on all interfaces.
When you will perform curl 127.0.0.1:3000 from the host machine, it connects to the container’s eth0 interface, and the connection succeeds.
We saw that the server is accessible from our host machine. But is it also accessbile from any other device which is connected to the same network as my host machine? The answer is yes.
If you try to access 192.168.169.214:3000 from another device on the same WiFi network, the request will succeed. You are reaching port 3000 via the en0 interface, and the host machine is listening for incoming requests on all interfaces.
You might be confused - why is my host machine listening for incoming requests on all interfaces?
The reason is that the port mapping -p 3000:3000 is equivalent to -p 0.0.0.0:3000:3000, binding the host machine to all interfaces on port 3000.
This might not always be what you want. There should be a more restrictive way of doing port mapping.
Restricting Container Access
For security, you may want to restrict access to your containerized app.
Here’s how:
-
Clean up the previous container
docker stop express-ts && docker rm express-ts -
Start the container with restrictive port mapping
docker run -d -p 127.0.0.1:3000:3000 --name express-ts express-ts:latestNote that earlier we were doing
-p 3000:3000but now we are doing-p 127.0.0.1:3000:3000 -
Checking the listening ports:
$ sudo lsof -i -n -P | grep LISTEN
Your output should be something similar:

You will see that, port 3000 is listening for TCP connections on host binding 127.0.0.1 instead of 0.0.0.0.
With this configuration, the server is bound to the loopback interface (lo0) on your host machine. External devices cannot connect to it, even if they are on the same network.
The only way to access the server is via 127.0.0.1:3000 or localhost:3000 from the host machine itself.
Conclusion
Understanding the nuances of network interfaces and port binding is crucial for both local development and containerized applications. Let’s recap what we have learned:
Key Takeaways
-
IP Address Binding:
127.0.0.1: Binds only to the loopback interface, accessible only from the local machine0.0.0.0: Binds to all available interfaces, making your service accessible from anywhere that can reach your machine- Specific IP (e.g.,
192.168.169.214): Binds to a particular network interface
-
Docker Port Mapping:
-p 3000:3000: By default, maps to all interfaces (0.0.0.0:3000:3000)-p 127.0.0.1:3000:3000: Restricts access to only the host machine- Even if your application binds to
0.0.0.0inside the container, the host binding controls external accessibility
-
Security Considerations:
- For development environments, binding to
127.0.0.1or using restrictive port mapping adds a layer of security - Production services that need external access should use
0.0.0.0(or a specific interface IP) with appropriate firewall rules and authentication mechanisms
- For development environments, binding to
Best Practices
When developing applications, consider your networking needs carefully:
- For services that should only be accessible locally, use
127.0.0.1binding - For services that need to be accessible across your network, use
0.0.0.0binding - With Docker, always use restrictive port mapping:
-p 127.0.0.1:<host_port>:<container_port>unless you explicitly need external access