What is the difference between localhost, 127.0.0.1 and 0.0.0.0?

What is the difference between localhost, 127.0.0.1 and 0.0.0.0?

April 1, 2025·Syed Kamran Ahmed
Syed Kamran Ahmed

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 (en0 and lo0). The linux equivalents of these are typically eth0 and lo.

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.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: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.214

The 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.0

The 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

  1. Initialize the project

    mkdir express-ts-docker && cd express-ts-docker
    yarn init -y
  2. Install dependencies

    yarn add express
    yarn add -D typescript ts-node @types/node @types/express nodemon
  3. Initialize TypeScript config

    npx tsc --init
  4. Modify tsconfig.json

    // Ensure the following settings are enabled:
    {
      "compilerOptions": {
        "outDir": "./dist",
        "rootDir": "./src",
        "strict": true
      }
    }
  5. 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}`)
    })
  6. Setup scripts in package.json

    "scripts": {
      "dev": "nodemon src/index.ts",
      "build": "tsc",
      "start": "node dist/index.js"
    }
  7. Create a .dockerignore file

    node_modules
    dist
  8. 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 dev

This 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 LISTEN

Your output should also be something similar: Listening ports

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:

  1. Build the image

    docker build --no-cache -t express-ts --file Dockerfile .
  2. 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.

  1. Exec inside the container:

    docker exec -it express-ts sh
  2. Since the docker container is running inside a linux environment, we can use the command netstat -tlnp to check all active TCP ports that are currently listening for incoming connections.

    /app # netstat -tlnp
  3. 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 and eth0 is 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-ts

Example 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 dev

This 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 LISTEN

Your output should be something similar: Listening ports

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:

  1. Build the image

    docker build --no-cache -t express-ts --file Dockerfile .
  2. 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:

  1. Exec inside the container

    docker exec -it express-ts sh
  2. Check all active TCP ports that are currently listening for incoming connections

    /app # netstat -tlnp
  3. 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:

  1. Clean up the previous container

    docker stop express-ts && docker rm express-ts
  2. Start the container with restrictive port mapping

    docker run -d -p 127.0.0.1:3000:3000 --name express-ts express-ts:latest

    Note that earlier we were doing -p 3000:3000 but now we are doing -p 127.0.0.1:3000:3000

  3. Checking the listening ports:

    $ sudo lsof -i -n -P | grep LISTEN

Your output should be something similar: Listening ports

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

  1. IP Address Binding:

    • 127.0.0.1: Binds only to the loopback interface, accessible only from the local machine
    • 0.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
  2. 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.0 inside the container, the host binding controls external accessibility
  3. Security Considerations:

    • For development environments, binding to 127.0.0.1 or 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

Best Practices

When developing applications, consider your networking needs carefully:

  • For services that should only be accessible locally, use 127.0.0.1 binding
  • For services that need to be accessible across your network, use 0.0.0.0 binding
  • With Docker, always use restrictive port mapping:
    • -p 127.0.0.1:<host_port>:<container_port> unless you explicitly need external access
Last updated on