Chapter 19Lesson 02~145 minutes

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.

Hands-onLoopbackAll interfacesdocker portEXPOSE

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.
Lab boundary. This lab uses BusyBox 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.

Next lesson

Next: Configuration, Design Choices, and Tradeoffs

Turn the lab observations into design choices: local-only versus externally reachable binds, fixed versus ephemeral ports, reverse-proxy boundaries, direct routing, and TLS termination.

Knowledge check

Why prove the service with an in-container request before adding -p?

What evidence distinguishes the loopback and all-interface cases?

Does an EXPOSE line make docker port show a mapping?

Why is a real second-host test optional?

A host firewall snapshot differs from the Docker mapping. Which is primary for Docker intent?

Official references and version notes

Version baseline, verified 2026-09-21.

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.

Ethereum / ERC-20
0x716c4Ab160C4B66F31a28AE2448BfF68fc3a2ef0Send only Ethereum/ERC-20 compatible assets to this address.