Skip to Content
Docker Intermediate: Images and Compose That Survive Real Projects
Intermediate
0 likes
Courses
Docker Intermediate: Images and Compose That Survive Real Projects

Publish Ports Without Exposing Everything

See who can reach a published port, bind it to 127.0.0.1, and keep databases off your network by not publishing them. After this, you can publish only what has to be reachable.

Course content

Sign in to save your progress.

Course Home

Skip ahead: If you already bind published ports to 127.0.0.1, skip to "Do not publish what only containers need", then go on to the next lesson.

Publishing a port is how a service in a container becomes reachable from outside it. The command is short, -p 8080:80, and what it opens is wider than most people think. This lesson shows who can reach a published port, how to narrow that, and how to avoid publishing things at all.

The short form opens to everyone

-p 8091:80 publishes container port 80 on port 8091 of your computer, on every network interface it has. That includes your Wi-Fi or office network. Add an address in front of the host port and it binds only there. Start two containers and look at both.

docker run -d --name pub-open -p 8091:80 nginx:1.30-alpine | Out-Null
docker run -d --name pub-local -p 127.0.0.1:8092:80 nginx:1.30-alpine | Out-Null
docker ps --filter name=pub- --format "table {{.Names}}\t{{.Ports}}"
NAMES       PORTS
pub-local   127.0.0.1:8092->80/tcp
pub-open    0.0.0.0:8091->80/tcp, [::]:8091->80/tcp

0.0.0.0 means all addresses, and [::] is the same for IPv6. 127.0.0.1 is the loopback address, and only programs on this computer can use it. Docker Desktop also opens these ports on the Windows side, so you can check what Windows is listening on.

netstat -an | Select-String ":809[12] " | ForEach-Object { $_.ToString().Trim() } | Sort-Object
TCP    [::]:8091              [::]:0                 LISTENING
TCP    [::1]:8091             [::]:0                 LISTENING
TCP    0.0.0.0:8091           0.0.0.0:0              LISTENING
TCP    127.0.0.1:8092         0.0.0.0:0              LISTENING

The first container listens on all addresses. The second listens on loopback only. Both pages answer on your computer.

curl.exe -s -o NUL -w "open: %{http_code}`n" http://localhost:8091
curl.exe -s -o NUL -w "local: %{http_code}`n" http://127.0.0.1:8092
open: 200
local: 200

The difference is invisible from your own computer. It shows up when another computer on your network tries the same address, and then only the first container answers. I did not test from a second computer, so the listening addresses above are my evidence, not a request from outside. Windows Firewall may also decide what gets through for the first one.

⚠ Warning: Publish to 127.0.0.1 unless another computer really has to reach the service. A development database on 0.0.0.0:5432 with a default password is open to your whole network.

EXPOSE does not publish anything

EXPOSE 80 in a Dockerfile is documentation. It tells the reader which port the program uses, and it opens nothing. The official nginx image has that line. Start it without -p and look.

docker run -d --name pub-none nginx:1.30-alpine | Out-Null
docker ps --filter name=pub-none --format "table {{.Names}}\t{{.Ports}}"
docker port pub-none
NAMES      PORTS
pub-none   80/tcp

80/tcp without an arrow means the port is declared and not published. docker port prints nothing, because there is nothing to report. If you want Docker to choose a free host port, use -P (capital). Then docker port NAME tells you which one it picked.

Do not publish what only containers need

A database or a cache usually has no reason to be reachable from your computer at all. Containers on the same network reach it by name, with no -p. Here an nginx container stands in for a database.

docker network create pub-net | Out-Null
docker run -d --name pub-db --network pub-net nginx:1.30-alpine | Out-Null
docker run --rm --network pub-net alpine:3.24 wget -qO /dev/null http://pub-db
"from the network: exit code $LASTEXITCODE"
curl.exe -s -m 3 -o NUL http://localhost:18094
"from your computer: exit code $LASTEXITCODE"
from the network: exit code 0
from your computer: exit code 7

Exit code 0 is success, and 7 from curl is "failed to connect". The other containers reach it. Your computer cannot, and neither can anyone else. Publish only the service that people or browsers talk to, such as the web front end.

The same thing in Compose

In compose.yaml, the address goes in front of the host port, in quotes.

services:
  web:
    image: nginx:1.30-alpine
    ports:
      - "127.0.0.1:8095:80"
Set-Location pub-stack
docker compose config
name: pub-stack
services:
  web:
    image: nginx:1.30-alpine
    networks:
      default: null
    ports:
      - mode: ingress
        host_ip: 127.0.0.1
        target: 80
        published: "8095"
        protocol: tcp
networks:
  default:
    name: pub-stack_default

Compose reads the short string and shows it as separate fields, with the host address, the published port and the container port. Quote the whole mapping. Docker's documentation recommends strings because an unquoted xx:yy can be parsed as a number in some YAML readers.

Clean up

docker rm -f pub-open pub-local pub-none pub-db
docker network rm pub-net

If you ran docker ps right now, which of your containers listen on 0.0.0.0, and does each one need to?

About

See who can reach a published port, bind it to 127.0.0.1, and keep databases off your network by not publishing them. After this, you can publish only what has to be reachable.

Duration
5 min
Last update
08/07/2026
Was this helpful?
​ 0 ​ 0

Comments (0)

Sign in to comment

No comments yet.