Docker Port Mapping Explained: -p host:container With Examples

Docker port mapping forwards a port on your computer to a port inside a container: docker run -p 8080:80 nginx sends traffic for localhost:8080 to port 80 in the container. The format is always HOST:CONTAINER, host port on the left, container port on the right.

Without -p (or --publish), a container's ports are not reachable from the host at all. Docker's documentation puts it directly: the daemon blocks access to ports that have not been published.

Docker port mapping syntax

All of these come from the -p forms in Docker's port publishing reference:

FlagWhat it does
-p 8080:80Host port 8080 on every host address to TCP port 80 in the container
-p 127.0.0.1:8080:80Same, but only reachable from the Docker host itself
-p 192.168.1.100:8080:80Only on that one host IP address
-p 8080:80/udpUDP instead of TCP
-p 80Container port 80 to a random free host port
-PEvery port the image EXPOSEs to a random high host port

TCP is the default, so -p 8080:80 and -p 8080:80/tcp are the same. To publish both protocols on one port, give two flags: -p 53:53/tcp -p 53:53/udp. If you are unsure which protocol a service needs, TCP vs UDP covers the difference.

The host and container ports don't have to match. Running two copies of the same web server is a common reason to make them differ:

docker run -d --name site-a -p 8080:80 nginx
docker run -d --name site-b -p 8081:80 nginx

Both containers listen on port 80 internally, and each has its own network namespace, so there is no clash. On the host they are localhost:8080 and localhost:8081.

Check which ports a container has mapped

docker ps shows mappings in the PORTS column. Example output fragment from Docker's docs, for a container started with -p 80:

0.0.0.0:54772->80/tcp

Read it left to right: host address and port, then the container port. Here Docker picked host port 54772. For one container, docker port lists every mapping. Example output from Docker's CLI reference:

$ docker port test
7890/tcp -> 0.0.0.0:4321
9876/tcp -> 0.0.0.0:1234

$ docker port test 7890/tcp
0.0.0.0:4321

Note the direction flips: docker port prints the container port first and the host side after the arrow.

Bind to 127.0.0.1 for anything private

A port published without an address listens on all host interfaces, 0.0.0.0 and [::]. Docker's documentation calls this "insecure by default": a database started with -p 5432:5432 on a laptop is reachable by anyone on the same network who can reach the laptop. Add the loopback address to keep it local:

docker run -d -p 127.0.0.1:5432:5432 -e POSTGRES_PASSWORD=example postgres

On Linux there is a second reason. Docker's firewall page says Docker and ufw are "incompatible": traffic to a published port is routed before ufw's rules are applied, so a ufw rule that denies port 5432 does not protect a container published on 5432. Binding to 127.0.0.1, or not publishing the port at all, is what actually keeps it closed.

EXPOSE does not publish anything

A Dockerfile line such as EXPOSE 80 is documentation. The Dockerfile reference says "The EXPOSE instruction doesn't actually publish the port." It tells whoever runs the image which port the app uses. Two things read it:

  • docker run -P publishes every exposed port to a random high host port. Run docker ps or docker port afterwards to see which ones it chose.
  • Tools and people reading the image metadata.

An image with no EXPOSE line can still be published with -p; the mapping works on any port the process inside actually listens on.

Port mapping in Docker Compose

If Compose is new to you, Docker Compose explained covers the file format. The Compose ports list uses the same [HOST:]CONTAINER[/PROTOCOL] format:

services:
  web:
    image: nginx
    ports:
      - "8080:80"
      - "127.0.0.1:8443:443"
  db:
    image: postgres
    environment:
      POSTGRES_PASSWORD: example
    expose:
      - "5432"

Quote the values. The Compose reference says HOST:CONTAINER should always be a quoted string, because YAML can read an unquoted value like 22:22 as a base-60 number. The db service has no ports entry on purpose: web reaches it at db:5432 over the Compose network, and nothing outside Docker can reach it. Mapping is only for traffic that starts outside Docker.

Compose also accepts ranges, such as "9090-9091:8080-8081", where both sides must cover the same number of ports.

Why a mapped port still doesn't work

The app inside listens on 127.0.0.1

This is the most common cause when docker ps shows the mapping but the browser gets a connection reset or an empty reply. Forwarded traffic arrives on the container's network interface, not on the container's own loopback, so a server bound to 127.0.0.1 inside the container never sees it. Many development servers bind to localhost by default. Flask's quickstart, for example, shows Running on http://127.0.0.1:5000 and says to add --host=0.0.0.0 to listen on all addresses. Inside a container, start the server with its "listen on all interfaces" option:

flask run --host=0.0.0.0

Binding to all interfaces inside the container is safe in this sense, because the container's interfaces are only reachable through the ports you publish. Control exposure with the host side of -p, as shown above.

The container port is wrong

The right-hand number must be the port the process actually listens on. -p 8080:8080 on an nginx image that listens on 80 maps to a port where nothing is listening. Check the image's documentation or its EXPOSE line, or the app's startup log.

The host port is taken

If another program or container already holds the host port, docker run fails with an error such as Bind for 0.0.0.0:8080 failed: port is already allocated. A container you forgot is still running in the background is a frequent culprit, so check docker ps first. If a non-Docker program holds it, find and stop the process using the port, or pick another host port.

You can't edit mappings on an existing container

Ports are fixed when the container is created. To change one, remove the container and create it again:

docker stop site-a
docker rm site-a
docker run -d --name site-a -p 9090:80 nginx

Any data you need to keep across that recreate should live in a volume or bind mount, not in the container's own filesystem; see Docker volume vs bind mount.

Frequently asked questions

Which side of -p 8080:80 is the host port?
The left side. In -p 8080:80, 8080 is the port on your computer (the Docker host) and 80 is the port inside the container. You open http://localhost:8080 in a browser, and Docker forwards the traffic to port 80 in the container.
Does EXPOSE in a Dockerfile publish a port?
No. Docker's Dockerfile reference says EXPOSE does not actually publish the port. It documents which ports the image expects to use. The port only becomes reachable from the host when you run the container with -p, or with -P, which publishes every exposed port to a random high host port.
Do containers need port mapping to talk to each other?
Not when they are on the same user-defined Docker network. Containers on the same network reach each other directly on the container port, for example a web container connecting to db:5432. Port mapping is only needed for traffic coming from outside Docker, such as your browser or another machine.
How do I change the port mapping of a running container?
You can't change it in place. Port mappings are set when the container is created, so stop and remove the container and run it again with the new -p value. With Docker Compose, edit the ports entry and run docker compose up -d, which recreates the service.

Sources