Port Publishing, NAT, Firewall Interaction, Direct Routing, Host Binding, and Exposure Security: Guided Hands-On Workflow and Core Operations
Build a disposable HTTP lab that proves internal listening first, compares loopback and all-interface publication, inspects mappings/sockets, and separates EXPOSE metadata from exposure.
Learning objectives
- Create a disposable HTTP service and prove its in-container listener before publishing any host port.
- Compare an unpublished service, a loopback-published service, and an all-interface-published service using exact Docker mappings and client tests.
-
Inspect
docker port, container inspect output, and optional host socket/firewall evidence without editing host networking. - Demonstrate that EXPOSE metadata and actual publication are independent states.
- Use exact names/labels and guarded cleanup so only Chapter 19 resources are removed.
httpd, no credentials, no production endpoints, no daemon
changes, and no firewall changes. The all-interface step is
short-lived and must be skipped on an untrusted network unless your
host firewall/environment makes the exposure acceptable.
1. Preflight and ownership
Record the daemon and image identity before creating anything. Use
the fixed lab label for inventory and cleanup. Port
18080 is the preferred fixed host port; if it is
already occupied, choose another high unprivileged port and record
the substitution.
docker version
docker info
docker context show
docker compose version || true
docker network ls
docker ps -a --no-trunc
docker pull busybox:1.36.1
docker image inspect busybox:1.36.1 --format 'ID={{.Id}} RepoDigests={{json .RepoDigests}}'
docker ps -a --filter label=devops-academy.lab=ch19
# Linux/macOS/WSL read-only port check when available:
ss -lnt 2>/dev/null | grep ':18080 ' || true
# If 18080 is occupied, choose a different high port and document it.
2. Start the service with no host publication
The server listens on container port 8080. There is deliberately no
-p. First prove the application works inside its own
network namespace.
docker run -d \
--name da19-web-internal \
--label devops-academy.lab=ch19 busybox:1.36.1 sh -c 'mkdir -p /www; echo ch19-internal-ok >/www/index.html; exec httpd -f -p 8080 -h /www'
docker exec da19-web-internal wget -qO- http://127.0.0.1:8080/
docker port da19-web-internal
docker inspect da19-web-internal --format 'Mode={{.HostConfig.NetworkMode}} Ports={{json .NetworkSettings.Ports}} Exposed={{json .Config.ExposedPorts}}'
Expected: the in-container request returns
ch19-internal-ok; docker port prints
nothing because no host mapping exists. Internal application health
and host exposure are now proven separately.
3. Publish the same service to host loopback
Create a second container instead of mutating the first. That preserves before/after evidence. The host address is explicit, so the declared exposure intent is local-host only in the normal NAT publication model.
docker run -d \
--name da19-web-loopback \
--label devops-academy.lab=ch19 -p 127.0.0.1:18080:8080 busybox:1.36.1 sh -c 'mkdir -p /www; echo ch19-loopback-ok >/www/index.html; exec httpd -f -p 8080 -h /www'
docker exec da19-web-loopback wget -qO- http://127.0.0.1:8080/
docker port da19-web-loopback
docker inspect da19-web-loopback --format '{{json .NetworkSettings.Ports}}'
# Host test:
curl -fsS http://127.0.0.1:18080/
Expected host response: ch19-loopback-ok. Record both
container-port and host-port evidence. If host curl fails but the
in-container wget succeeds, keep the failure and investigate the
publication/host path rather than rebuilding the image.
4. Inspect the host-side socket or forwarding endpoint
On a native Linux host, socket/rule evidence can clarify the host path. On Docker Desktop, the forwarding endpoint may be implemented by Docker Desktop's backend rather than a conventional native Linux listener visible in your host OS. Treat these commands as optional evidence, not a prerequisite.
# Native Linux read-only socket view:
ss -lntp 2>/dev/null | grep ':18080 ' || true
# Docker's own mapping remains the portable source of truth:
docker port da19-web-loopback 8080/tcp
docker inspect da19-web-loopback --format '{{json .NetworkSettings.Ports}}'
5. Compare an all-interface publication safely
Remove only the loopback lab container, then recreate the same workload with an omitted host IP. Docker's default publish behavior makes the mapping available on all host addresses. Perform this step only on a trusted/disposable host and keep it brief.
docker rm -f da19-web-loopback
docker run -d \
--name da19-web-all \
--label devops-academy.lab=ch19 -p 18080:8080 busybox:1.36.1 sh -c 'mkdir -p /www; echo ch19-all-ok >/www/index.html; exec httpd -f -p 8080 -h /www'
docker port da19-web-all
docker inspect da19-web-all --format '{{json .NetworkSettings.Ports}}'
curl -fsS http://127.0.0.1:18080/
The mapping should show an unspecified/all-interface host address
rather than 127.0.0.1. A localhost request alone does
not prove remote reachability, but the broader bind is already
security-relevant evidence.
6. Optional peer-path test
If you have an authorized second machine on the same lab LAN, test
http://HOST_LAN_IP:18080/ from that machine and capture
the result. If not, do not manufacture a public exposure. On Docker
Desktop, host.docker.internal can demonstrate
container-to-host forwarding, but it is not equivalent to a real LAN
trust-boundary test.
# Optional helper-container check of the host forwarding path.
# On native Linux, host-gateway supplies the host-side gateway mapping.
docker run --rm --add-host host.docker.internal:host-gateway busybox:1.36.1 wget -T 3 -qO- http://host.docker.internal:18080/ || true
Interpret the result in context. A host firewall, Desktop backend, VPN, or platform policy can legitimately change reachability. Record the environment instead of forcing the network open.
7. Observe firewall/NAT state without editing it
Docker on native Linux normally creates packet-filter/NAT rules for bridge networking. If you are authorized and the tooling is present, capture a read-only snapshot. Do not flush, disable, or rewrite host rules as part of this course lab.
# Native Linux, optional/read-only and often requires administrative access:
sudo iptables -t nat -S 2>/dev/null | grep -E 'DOCKER|18080' || true
sudo iptables -S 2>/dev/null | grep -E 'DOCKER|18080' || true
sudo nft list ruleset 2>/dev/null | grep -E 'docker|18080' || true
Use these only to corroborate Docker-visible evidence. Engine 29's nftables backend is experimental; a host may legitimately use iptables, nftables, firewalld integration, or Desktop-specific forwarding.
8. Prove EXPOSE metadata does not publish
BusyBox does not declare EXPOSE for this lab, so create a tiny local
image that does. Build it, start it without -p, and
compare image metadata with runtime publication.
mkdir -p da19-expose && cd da19-expose
cat > Dockerfile <<'EOF'
FROM busybox:1.36.1
EXPOSE 8080
CMD ["sh","-c","mkdir -p /www; echo expose-only-ok >/www/index.html; exec httpd -f -p 8080 -h /www"]
EOF
docker buildx build --load -t da19-expose:lab .
docker run -d --name da19-expose-only --label devops-academy.lab=ch19 da19-expose:lab
docker image inspect da19-expose:lab --format '{{json .Config.ExposedPorts}}'
docker port da19-expose-only
docker exec da19-expose-only wget -qO- http://127.0.0.1:8080/
Expected: the image reports 8080/tcp in metadata, the
application responds internally, and docker port still
shows no host mapping.
9. Challenge: identify the broken layer
Scenario A: docker port shows
127.0.0.1:18080, but the app listens on container port
9090. Scenario B: the app listens on 8080 and Docker maps
0.0.0.0:18080, localhost works, but a LAN peer cannot
connect. For each scenario, state what evidence you would gather
next and which layer you would not change yet.
Correct direction: A is an application/container-port mismatch; firewall/DNS changes are premature. B has already proven application + Docker local publication, so investigate host firewall/interface/routing and the peer path without rebuilding the image.
10. Exact cleanup
docker rm -f da19-web-internal da19-web-all da19-expose-only 2>/dev/null || true
docker image rm da19-expose:lab 2>/dev/null || true
cd .. 2>/dev/null || true
rm -rf da19-expose
docker ps -a --filter label=devops-academy.lab=ch19
Do not use broad prune operations. The purpose of the lab is to practice exact ownership and rollback.
Knowledge check
Why prove the service with an in-container request before
adding -p?
It isolates application/listener state from host publication. If the internal test fails, host NAT/firewall work is the wrong layer.
What evidence distinguishes the loopback and all-interface cases?
docker port and inspect show different HostIp
values; host/client tests then prove actual path behavior.
Does an EXPOSE line make docker port show a
mapping?
No. EXPOSE is image metadata. A runtime publish operation is still required.
Why is a real second-host test optional?
Not every learner has an authorized lab LAN. The course must not require opening services to an uncontrolled network merely to prove a concept.
A host firewall snapshot differs from the Docker mapping. Which is primary for Docker intent?
The Docker container configuration/mapping proves declared Docker intent; host firewall evidence explains how the host enforces or modifies the resulting path.
Official references and version notes
- Docker Docs — Port publishing and mapping — bind addresses, NAT/PAT, direct routing, gateway modes, and default binding behavior.
- Docker Docs — Packet filtering and firewalls — Docker-created firewall rules, iptables/nftables backends, forwarding, firewalld, and UFW interaction.
- Docker Docs — Docker with nftables — experimental Engine 29 nftables backend and migration cautions.
-
Docker CLI — docker container run
—
--publish,--publish-all, host-IP binding, and runtime networking flags. - Dockerfile reference — EXPOSE — image metadata versus actual host publication.
- Docker Desktop networking — Desktop forwarding architecture, host binding, and firewall integration differences.
- Docker Docs — Host network driver — host namespace semantics and why publish flags do not apply in host mode.
- Docker Engine 29 release notes — Engine 29 networking changes and experimental nftables support.
Docker Engine 29.8.1 is the current Engine 29 release. On Linux, Docker uses iptables by default and the Engine 29 nftables backend remains experimental. Docker Desktop implements host exposure through its managed VM/backend and can differ from native Linux host rule inspection. The labs therefore make Docker API/container-visible evidence mandatory and host firewall inspection optional/read-only.
Keep the academy open
Support free, practical DevOps education.
Every lesson is designed to remain readable in a browser, downloadable from GitHub, and usable without a paid learning platform. Contributions help expand and maintain the curriculum.
0x716c4Ab160C4B66F31a28AE2448BfF68fc3a2ef0Send only Ethereum/ERC-20 compatible assets to this
address.