The app binds to the container's loopback interface, so traffic from the published port never reaches it; bind to
0.0.0.0The right answer is that the app binds to the container's loopback interface, so it should bind to 0.0.0.0. The loopback address 127.0.0.1 only accepts traffic from inside the same machine. Inside a container, 127.0.0.1 is the container's own loopback. Port-forwarded traffic arrives on the container's network interface, not on loopback. So the app never sees it. Listening on 0.0.0.0 means "accept traffic on every interface".
"The Dockerfile is missing EXPOSE 3000" is wrong. EXPOSE is only documentation and would not change anything.
Read more