Skip to content

Lesson 4.4: Exposing Ports

Welcome to Lesson 4.4! By now, you've run containers on various networks and seen that containers on a bridge network can communicate internally without port mapping. But how do you make a container's service accessible from outside the Docker host? The answer lies in port publishing. In this lesson, we'll explore the -p and -P flags, understand how they work with bridge networks, and learn best practices for exposing ports securely.


Learning Objectives

TIP

By the end of this lesson, you will be able to:

  • Explain the difference between exposing and publishing ports.
  • Publish container ports using -p (or --publish) with various syntaxes.
  • Use -P to publish all exposed ports automatically.
  • Understand how port mapping works with NAT (Network Address Translation) on bridge networks.
  • Map ports to specific host interfaces (e.g., localhost).
  • Distinguish between EXPOSE in Dockerfile and runtime port publishing.
  • Debug common port‑related issues.

1. Exposing vs. Publishing Ports

It's important to understand the distinction between exposing a port and publishing a port:

  • Expose (EXPOSE in Dockerfile or --expose at runtime) is documentation. It tells users which ports the container listens on, but does not make them accessible from the host. Exposed ports are visible in docker inspect and are used by -P (see below). They are optional.

  • Publish (-p or --publish at runtime) actually maps a container port to a port on the Docker host, making it accessible from outside the container. This is required for external access when using bridge networks.

INFO

On a user‑defined bridge or the default bridge, publishing is necessary for traffic from the host to reach the container. On the host network, publishing is not needed (the container directly uses the host's ports), but you can still use -p to limit exposure.


2. Publishing Ports with -p (or --publish)

The -p flag follows this syntax:

-p [host_ip:]host_port:container_port[/protocol]
  • host_ip (optional): The IP address on the host to bind to. If omitted, the container listens on all host interfaces (0.0.0.0).
  • host_port: The port number (or range) on the host.
  • container_port: The port number (or range) inside the container.
  • /protocol: Optional; can be tcp (default) or udp.

2.1. Basic Port Mapping

Map host port 8080 to container port 80 (HTTP):

bash
docker run -d --name web -p 8080:80 nginx

Now visiting http://localhost:8080 will reach the Nginx container.

2.2. Map to a Specific Host IP

Bind only to the loopback interface (localhost) to prevent external access:

bash
docker run -d --name web -p 127.0.0.1:8080:80 nginx

Now the container is only reachable from the host itself, not from other machines on the network.

2.3. Map UDP Ports

By default, ports are TCP. For UDP, specify the protocol:

bash
docker run -d --name dns -p 53:53/udp -p 53:53/tcp mydns

2.4. Map a Range of Ports

You can map a contiguous range:

bash
docker run -d --name app -p 9000-9005:9000-9005 myapp

This maps ports 9000-9005 on the host to the same ports inside the container.

2.5. Random Host Port Allocation

If you specify only the container port, Docker assigns a random high port on the host:

bash
docker run -d --name web -p 80 nginx

Use docker port web to see which host port was assigned.


3. Publishing All Exposed Ports with -P

If a Dockerfile contains EXPOSE instructions (e.g., EXPOSE 80 443), you can publish all of them to random host ports using -P (uppercase):

bash
docker run -d --name web -P nginx

Docker assigns random host ports for each exposed port. Use docker port web to see the mappings.

This is useful for quick tests where you don't care which host ports are used.


4. How Port Publishing Works Under the Hood

When you publish a port on a bridge network, Docker creates a NAT (Network Address Translation) rule in the host's firewall (iptables on Linux). Traffic arriving at the host on the specified port is forwarded to the container's IP and port.

INFO

On Linux, you can see these rules with sudo iptables -t nat -L (look for DOCKER chain).

This is why containers on a bridge network cannot be accessed directly by their IP from the host unless you publish ports; the host's network stack is separate from the bridge network.


Visual: NAT Port Forwarding Flow

NAT dorwarding

5. Publishing Ports in Docker Compose

In Docker Compose, you define port mappings under the ports key:

yaml
services:
  web:
    image: nginx
    ports:
      - "8080:80"            # host:container
      - "127.0.0.1:8081:80" # bind to localhost
      - "80"                 # random host port

6. Port Conflicts

If the host port you specify is already in use by another process (including another container), Docker will return an error. Either stop the conflicting container, change the host port, or use a random port.


7. Debugging Port Mappings

Use these commands to inspect port mappings:

  • docker port <container>: Shows all published ports for a container.
  • docker inspect <container>: Look for NetworkSettings.Ports.
  • docker ps: Shows published ports in the PORTS column.

8. Best Practices for Port Publishing

TIP

  • Avoid using -p 80:80 in production unless you control the host's firewall. Use a reverse proxy (like Traefik or Nginx) that listens on port 80 and routes to containers on internal ports.
  • Bind to 127.0.0.1 when the service should only be accessible locally (e.g., admin interfaces).
  • Use random host ports (-p 80) for temporary containers or when the exact port doesn't matter.
  • Prefer specific, well‑known ports for production (e.g., 8080 for app, 3306 for MySQL) but consider using a reverse proxy.
  • Expose ports in Dockerfiles (EXPOSE) to document intent, even if not required.

Hands-On Tasks

Task 1: Basic Port Mapping

  1. Run an Nginx container mapping host port 8080 to container port 80:
    bash
    docker run -d --name nginx1 -p 8080:80 nginx
  2. Open a browser and visit http://localhost:8080. You should see the Nginx welcome page.
  3. Check the mapping with docker port nginx1.
  4. Stop and remove the container.

Task 2: Multiple Ports

  1. Run a container with multiple port mappings (e.g., a web app on 8080 and a debug port 5000):
    bash
    docker run -d --name multi -p 8080:80 -p 5000:5000 nginx
  2. Use docker ps to verify both mappings.
  3. Inspect with docker port multi.

Task 3: Bind to Localhost Only

  1. Run an Nginx container mapped only to localhost:
    bash
    docker run -d --name local-only -p 127.0.0.1:8080:80 nginx
  2. Access http://localhost:8080 – it works.
  3. Find your host's IP (e.g., ifconfig or ip addr), and try http://<host-ip>:8080 from another machine on the same network. It should fail because the port is bound only to loopback.

Task 4: Random Host Port

  1. Run a container with -p 80:
    bash
    docker run -d --name random -p 80 nginx
  2. Check the assigned host port with docker port random. It will show something like 80/tcp -> 0.0.0.0:32768.
  3. Visit http://localhost:32768 (replace with the actual port). You should see Nginx.
  4. Remove the container.

Task 5: Using -P with EXPOSE

  1. Create a Dockerfile (or use an image that already has EXPOSE). For example, the official Nginx image has EXPOSE 80. Run:
    bash
    docker run -d --name expose-demo -P nginx
  2. Check the published ports with docker port expose-demo.
  3. Access the service using the host port.

Task 6: Port Conflicts

  1. Run a container using port 8080:80.
  2. Attempt to run another container also mapping to 8080:80. Observe the error.
  3. Resolve by using a different host port or stopping the first container.

Task 7: Inspect Ports with docker inspect

  1. Run a container with multiple port mappings.
  2. Use docker inspect <container> and look for NetworkSettings.Ports. See the detailed mapping.

Summary

Key Takeaways

  • Publishing ports (-p, -P) makes container services accessible from the host.
  • Basic syntax: -p host_port:container_port.
  • You can bind to a specific host IP, map UDP, use ranges, or let Docker assign a random host port.
  • -P publishes all ports listed in EXPOSE instructions.
  • Port publishing works through NAT on bridge networks.
  • Use docker port and docker inspect to see mappings.
  • Best practice: Bind to localhost when possible, use reverse proxies for production.

Check Your Understanding

  1. What is the difference between EXPOSE in a Dockerfile and -p at runtime?
  2. Write a docker run command to map host port 8080 to container port 80, and also map host port 3306 to container port 3306 for MySQL.
  3. How do you map a container's UDP port 53 to host port 53?
  4. What does the -P flag do?
  5. How can you make a container only accessible from localhost on the host?
  6. What command shows you the port mappings for a running container?
Click to see answers
  1. EXPOSE is documentation—it tells which ports the container uses but doesn't actually publish them. -p at runtime creates the actual port mapping that makes the port accessible from the host.
  2. docker run -d --name mysql -p 8080:80 -p 3306:3306 mysql-image
  3. docker run -d --name dns -p 53:53/udp mydns-image
  4. -P publishes all ports listed in the Dockerfile's EXPOSE instructions to random host ports.
  5. Use docker run -p 127.0.0.1:8080:80 imagename to bind only to the loopback interface.
  6. docker port containername or docker inspect containername (look for NetworkSettings.Ports).

Additional Resources


Next Up

In the next lesson, we'll explore the Container Network Model (CNM) and how Docker's networking architecture fits together at a deeper level. See you there!