Publishing Ports to the Host

To reach the API from a browser or curl 3,008 on the host, publish its port with -p hostPort:containerPort. Published ports are fixed when a container is created, so the API container is replaced:

Publishing the API on host port 33000 and looking at howShell
docker rm -f l3-api
docker run -d --name l3-api --network l3-booknest -p 33000:3000 -v "$PWD/booknest:/app" \
  -w /app -e PGHOST=postgres node:24-slim node server.js
sleep 2
docker port l3-api
curl -s localhost:33000/health; echo
sudo iptables -t nat -S DOCKER | grep -o 'dport 33000.*'
ps -o args= -C docker-proxy | grep 33000 | cut -c 1-90
Output
l3-api
d6d95cf4462f8c7c7c7b39b764a240b113bb1c03c02e98e737036d2f334bf82d
3000/tcp -> 0.0.0.0:33000
3000/tcp -> [::]:33000
{"status":"ok"}
dport 33000 -j DNAT --to-destination 172.19.0.3:3000
/usr/bin/docker-proxy -proto tcp -host-ip 0.0.0.0 -host-port 33000 -container-ip 172.19.0.
/usr/bin/docker-proxy -proto tcp -host-ip :: -host-port 33000 -container-ip 172.19.0.3 -co

Docker 514 publishes a port twice over. A DNAT rule in the nat table rewrites packets arriving from outside for port 33000 to the container's address, and a docker-proxy process per address family listens on the port for the cases rules cannot handle, such as connections to localhost from the host itself. Engine 29 still writes these rules with iptables 40,292 by default; its nftables 40,292 backend is experimental.

The two paths a request to a published port can take
The two paths a request to a published port can take

A published port listens on every host address, and Docker's rules run ahead of ufw (Official Apt Repository), so bind private services to loopback: -p 127.0.0.1:33000:3000. -P publishes every EXPOSEd port on random high ports.