Docker · advanced
Docker port-binding checklist
Docker port-binding checklist that turns an unreachable container or host-listener conflict into a documented, evidence-driven decision. The piece argues that most "port not working" symptoms are not network problems at all but binding-scope, port-already-in-use, or firewall ordering mistakes, and walks through the exact boundaries an engineer must inspect before changing configuration.
The symptoms
- •Client connection to the published <port> times out or returns connection refused even though the container process is reported as running.
- •docker run exits with bind: address already in use or cannot assign requested address on a specific host port.
- •Port reachable from the Docker host but unreachable from another machine on the LAN, or vice versa.
- •Port worked previously but regressed after a restart, an image upgrade, or a host service change.
- •Two containers or a container and a host service compete for the same published port, with only one starting.
Likely causes
- •Binding scope mismatch: the container listens on 127.0.0.1 inside the container network namespace while the user expects a host-reachable port.
- •Daemon-level address already in use: another process, a previous container, or a host service owns the host port because the host network namespace is the actual binding scope.
- •Published port never opened: the container process is bound but the user-publish flag format is wrong, so Docker does not forward traffic.
- •External firewall or cloud security group silently drops the packet between the host and the client, so the symptom looks like a Docker problem.
- •DNS or proxy indirection sends the client to a different host than the one running the container.
First ten minutes
- 01Confirm the container is actually running and not restarting: capture <container-id> and read its state, last exit code, and restart count before touching ports.
- 02Compare the container's listening sockets to the host's listening sockets and label every row by IP family, address, and process to identify scope mismatches.
- 03Identify the exact bind address inside the container: a process bound to 127.0.0.1 is not reachable from outside the container's network namespace.
- 04Identify the binding owner on the host: if the host port is owned by a non-Docker process, Docker cannot claim it and the publish silently fails.
- 05Walk the traffic path in order: host socket, kernel forwarding, host firewall, upstream router or cloud security group, then client DNS resolution.
Evidence to collect
- •Container inspection output showing PortBindings, HostConfig.PortBindings, and NetworkSettings.Ports with explicit empty-value entries.
- •Process-list snapshot filtered to the container PID, showing the listening socket, protocol, and bind address inside the container's network namespace.
- •Host socket snapshot filtered to the published <port>, showing owner PID, process name, and bind address on the host network namespace.
- •Host firewall rule dump for the published <port> and protocol, plus any cloud or upstream security group that covers the path.
- •Daemon log lines around the container start, including any translation warnings and any EADDRINUSE or EACCES-class messages.
Where to look
- •Boundary between the container network namespace and the host network namespace, where Docker translates a host:port publish into a container_ip:container_port DNAT rule.
- •Boundary between the host kernel and the host firewall, where published ports can be open on the socket but dropped by local rules.
- •Boundary between the host and the client network, where cloud security groups, upstream routers, and DNS records decide whether the client ever reaches the host.
- •Boundary between the prior container state and the current container state, where a previous container can leave a publisher bound on the host port.
Diagnostic steps
- 01Capture <container-id> and read its state, last exit code, restart count, and ports section; an empty published-ports section is a hard stop before any further work.
- 02Read the container's listening sockets from inside its network namespace and label each entry by bind address, protocol, and owning PID to confirm the application is listening on the expected interface.
- 03Read the host's listening sockets for the published <port> and confirm the owner is the Docker proxy or the daemon; if the owner is unrelated, the publish cannot be created.
- 04Compare the container's bind address to the published target: a 127.0.0.1 bind inside the container will never be reachable through -p regardless of host port choice.
- 05Walk the host firewall and upstream security group for the published <port> and protocol, and record whether each hop accepts or drops the packet.
- 06Resolve the client target to an IP and confirm that IP is the host running the container, eliminating DNS and proxy indirection as the cause.
- 07Re-run the start with explicit host IP, host port, container port, and protocol, and re-inspect the published-ports section to confirm the publish now exists.
Common mistakes
- •Assuming the published port is open on every host interface when it was published to a specific host IP; the symptom is reachable on one IP and unreachable on another.
- •Assuming the container is reachable just because the published port is open, without verifying the application inside the container is bound to a non-loopback address.
- •Assuming a previous container has released the host port; a crashed container can leave the host port bound in the host's network namespace until the proxy is cleaned up.
- •Assuming the host firewall is permissive by default; many distributions and cloud images ship with a default-deny or restricted policy that drops published ports.
- •Assuming a successful Docker-level publish means the client can reach the service; the actual gatekeeper is often the upstream router or security group, not Docker.
Safe fixes
- •Re-publish the port with explicit host IP, host port, container port, and protocol, then re-inspect the container's PortBindings to confirm the slot is no longer empty before any further action.
- •If the application inside the container is bound to 127.0.0.1, change the application bind address to 0.0.0.0 inside the container or to the container's IP on the Docker network, and document the change before redeploying.
- •If the host port is owned by a non-Docker process, stop that process or change the published host port, and verify the new owner is the Docker proxy before declaring the conflict resolved.
- •If the host firewall drops the published port, add a scoped rule that accepts the published <port> and protocol from the documented client CIDR only, and re-check the rule order before re-testing.
- •If the upstream security group drops the packet, add a scoped ingress rule for the published <port> and protocol from the documented client CIDR only, and confirm with the provider's rule viewer before re-testing.
Prove the fix
- 01Container inspection shows a non-empty PortBindings entry for the published <port> and protocol, and the host socket owner for that port is the Docker proxy or the daemon.
- 02A second client on the documented host IP and host port can complete a TCP handshake or receive a documented application-level response, and the same test fails from outside the documented CIDR.
- 03A regression run that restarts the container and re-publishes the port produces the same non-empty PortBindings entry and the same reachable-result, with no EADDRINUSE or EACCES lines in the daemon log.
- 04A change record lists the host IP, host port, container port, protocol, application bind address, and the firewall or security group rule that was added, so the next engineer can reproduce the boundary.
Prevention and next steps
- •Adopt a project-wide port-allocation table that records host IP, host port, container port, protocol, owning service, and ticket, so conflicts are visible before a deploy.
- •Standardize on explicit host IP and host port in every run command and compose file, and lint compose files for empty host-port fields in CI.
- •Require applications to bind to 0.0.0.0 inside the container, and document that requirement in the image README so the contract is visible to operators.
- •Capture a publish-port snapshot in every deployment record so the next on-call engineer can compare the current published-ports section to the documented baseline.
Safe commands and checks
docker ps --no-trunc --filter id=<container-id>
docker inspect --format '{{json .State}}' <container-id>
docker inspect --format '{{json .HostConfig.PortBindings}}' <container-id>
docker inspect --format '{{json .NetworkSettings.Ports}}' <container-id>
nsenter -t <pid> -n ss -ltnp
ss -ltnp 'sport = :<port>'
docker run --rm -p <host-ip>:<host-port>:<container-port>/<proto> <image> -- <print-bind-address>
docker events --since <timestamp> --filter container=<container-id>