Nmap can’t directly scan ports on Docker containers from the host machine because Docker’s networking isolates container ports.
Let’s see Nmap in action, scanning a simple Nginx container.
First, we need a container running. Here’s a minimal Nginx container:
docker run -d --name nginx-test -p 8080:80 nginx
This command does a few things:
-d: Runs the container in detached mode (in the background).--name nginx-test: Assigns a recognizable name to the container.-p 8080:80: This is crucial. It maps port 80 inside the container (where Nginx listens by default) to port 8080 on our host machine.nginx: Specifies the Docker image to use.
Now, from your host machine, you can scan the host’s port 8080, and Nmap will effectively be scanning the container’s port 80:
nmap -p 8080 localhost
The output will look something like this, showing port 8080 is open and Nginx is running:
Starting Nmap 7.94 ( https://nmap.org ) at 2023-10-27 10:00 PDT
Nmap scan report for localhost (127.0.0.1)
Host is up (0.000020s latency).
PORT STATE SERVICE
8080/tcp open http
Nmap done: 1 IP address (1 host up) scanned in 0.10 seconds
This works because Docker’s port mapping (-p 8080:80) creates a rule on the host’s network stack. When traffic hits localhost:8080, the host’s kernel forwards it to the container’s network namespace on port 80. Nmap, scanning localhost:8080, is just interacting with that host-level forwarding rule.
To scan ports inside a container without relying on host port mapping, you need to run Nmap from within the container’s network namespace. Docker provides a special --network container:<container_name_or_id> option for this.
Let’s try scanning another service, say a Redis container, from within its own network namespace.
First, start a Redis container:
docker run -d --name redis-test redis
Now, run Nmap from within the redis-test container’s network:
docker run --rm --network container:redis-test nicolaka/netshoot nmap -p 6379 localhost
Let’s break this down:
docker run --rm: Runs a new container and removes it when it exits.--network container:redis-test: This is the key. It tells the new container (nicolaka/netshoot) to share the network stack of theredis-testcontainer. Any network activity fromnicolaka/netـshootwill appear to originate from theredis-testcontainer.nicolaka/netshoot: This is a popular Docker image that bundles networking tools like Nmap, ping, curl, etc., making it ideal for network diagnostics.nmap -p 6379 localhost: We’re running Nmap from inside the shared network namespace.localhosthere refers to theredis-testcontainer itself, and we’re scanning its default Redis port (6379).
The output will confirm Redis is listening on port 6379:
Starting Nmap 7.94 ( https://nmap.org ) at 2023-10-27 10:05 PDT
Nmap scan report for localhost (127.0.0.1)
Host is up (0.000000s latency).
PORT STATE SERVICE
6379/tcp open redis
Nmap done: 1 IP address (1 host up) scanned in 0.07 seconds
This approach is powerful because it lets you see exactly what ports are open from the perspective of the container itself, irrespective of any port mappings configured on the host. It’s as if you’ve exec-ed into the container and run Nmap there, but without the need to install Nmap in your application container.
The most surprising thing about Docker networking is how seamlessly it allows containers to share network namespaces, making it feel like they are directly connected or even the same machine from a network perspective.
The primary problem Docker networking solves is providing isolated yet interconnected network environments for containers. Each container can have its own IP address and network stack, preventing conflicts and allowing for fine-grained control over network traffic. Docker’s default bridge network, host network, and overlay networks offer different levels of isolation and connectivity for various use cases.
Internally, Docker uses Linux kernel features like network namespaces and virtual Ethernet pairs (veth pairs). When you create a container, a new network namespace is created for it. A veth pair is then created, with one end in the host’s root namespace (often attached to a bridge like docker0) and the other end inside the container’s network namespace, becoming its network interface (e.g., eth0). Port mapping is implemented using NAT rules (iptables) on the host.
When you use --network container:<container>, you’re not creating a new network namespace. Instead, you’re instructing the Docker daemon to place the new container’s network interfaces into the existing network namespace of the target container. This means both containers share the same IP address, routing table, and network interfaces. From a network perspective, they are indistinguishable.
The one thing most people don’t realize is that when you use --network container:<container_name>, the localhost inside the new container refers to the target container’s network stack, not the new container’s own (which is, in essence, the same as the target’s). So, scanning localhost from within the nicolaka/netshoot container when using --network container:redis-test is actually scanning port 6379 on the redis-test container itself.
The next step is understanding how to manage complex multi-container network communication with Docker Compose.