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.
-
Dockerfiles That Don't Rot
-
.dockerignore and What Belongs in the Build Context
-
ENTRYPOINT, CMD and When to Override Them
-
-
-
Data That Outlives Containers
-
Bind Mounts vs Named Volumes: Which One When
-
Permissions, Ownership and Why the Container Can't Write
-
-
-
Networks Beyond Defaults
-
Bridge Networks, Aliases and DNS
-
Publish Ports Without Exposing Everything
-
-
-
Compose for Real Apps
-
Services, depends_on and Healthchecks
-
Environment Files, Secrets and What Not to Commit
-
-
-
Images Across Machines
-
Tag Images So You Can Find Them Later
-
Push and Pull From a Registry Without Surprises
-
-
- Final Check: Docker Intermediate
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
Comments (0)
Sign in to commentNo comments yet.