Compose multi-container stacks break most often on stale upstreams and uneven worker cost, not on the Dockerfile itself. Resolve the Node service by name on the shared network, prefer one upstream entry per replica (or service scale), and let the process manager inside the app image own clustering instead of exposing every worker port to nginx. After a recreate, long-lived nginx workers can keep a dead IP unless you reload, recreate the proxy, or use a variable plus resolver so DNS is re-queried.
For load distribution I use least_conn once replicas are comparable, with modest max_fails and fail_timeout so a crashed container leaves the pool instead of 502-ing half the traffic. Keep app ports internal; only the proxy publishes. When WebSockets exist, give them their own location with Upgrade headers and timeouts above the heartbeat — do not clear Connection on that same block. Post-deploy I compare status and latency hitting each upstream by service name, then through the vhost, and watch error_log for connect() failed versus upstream timed out so I know whether to fix a dead peer or raise read timeouts.