Connection refused for NextJS inside Docker swarm only (Works for general run)

I have my private self-hosted docker image with nextjs inside. This image exposes 3000 port.

I tried to run this image via docker and docker swarm.

  1. Docker
docker run -p 3000:3000 path-to-my-image

Result:

docker ps:

0.0.0.0: 3000->3000/tcp, ::: 3000->3000/tcp

ps inside container:

PID   USER     TIME  COMMAND
    1 nextjs    0:00 next-router-wo
   22 nextjs    0:01 next-render-worker-app
   23 nextjs    0:01 next-render-worker-pages
   51 nextjs    0:00 sh
   57 nextjs    0:00 ps

netstat -tulp|grep 3000 inside container:

tcp        0      0 cf748210f6f3:3000       0.0.0.0:*               LISTEN      1/next-router-wo

curl http://127.0.0.1:3000 outputs correct response. So all is fine.

  1. Docker swarm

stack.yaml

version: "3.7"

services:
  app:
    image: path-to-my-image
    ports:
      - 3000:3000
    deploy:
      mode: replicated
      replicas: 1
      placement:
        constraints: [node.role == manager]

docker service ls shows service is online, 1 replica

ps inside container:

PID   USER     TIME  COMMAND
    1 nextjs    0:00 next-router-wo
   22 nextjs    0:01 next-render-worker-app
   23 nextjs    0:01 next-render-worker-pages
   50 nextjs    0:00 sh
   56 nextjs    0:00 ps

netstat -tulp|grep 3000 inside container:

tcp        0      0 3e53ca570e77:3000       0.0.0.0:*               LISTEN      1/next-router-wo

But curl http://127.0.0.1:3000 fails:

curl: (7) Failed to connect to 127.0.0.1 port 3000 after 0 ms: Connection refused

Tried another port, another machines, tried node and pm2. All is same.

I can’t understand the reason and where to dig next.

Thank you in advance.

Just tested this, we usually use Traefik without the Docker ingress network.

version: '3'

services:
  whoami:
    image: traefik/whoami:v1.10
    hostname: '{{.Node.Hostname}}'
    ports:
      - 3000:3000
    deploy:
      mode: global

When checking the ports I see port 3000 used on tcp6, but not on regular tcp4:

# netstat -tulpn | grep 3000
tcp6       0      0 :::3000                 :::*                    LISTEN      730/dockerd

That’s strange.

Just tested this on a brand new Debian 12 VM:

sudo apt update && sudo apt -y upgrade

curl -fsSL https://get.docker.com -o get-docker.sh
sudo sh get-docker.sh

cat <<EOF > whoami.yml
version: '3'
services:
  whoami:
    image: traefik/whoami:v1.10
    hostname: '{{.Node.Hostname}}'
    ports:
      - 3000:80
    deploy:
      mode: global
EOF

docker swarm init
docker stack deploy -c whoami.yml whoami

Leads to same result, no port 3000 on tcp4:

# netstat -tulpn
Active Internet connections (only servers)
Proto Recv-Q Send-Q Local Address           Foreign Address         State       PID/Program name
tcp        0      0 0.0.0.0:22              0.0.0.0:*               LISTEN      862/sshd: /usr/sbin
tcp6       0      0 :::3000                 :::*                    LISTEN      8351/dockerd

UPDATE: Same on new VM with Ubuntu 22.04, no port 3000 on tcp4.

Maybe @meyay can have a look, I heard he is the Swarm expert :slight_smile:

UPDATE2:
Tested another image on Debian 12 VM

sudo apt update && sudo apt -y upgrade
curl -fsSL https://get.docker.com -o get-docker.sh
sudo sh get-docker.sh
reboot

cat <<EOF > python.yml
version: '3.9'
services:
  python:
    image: python:3.9-slim
    entrypoint: ["/bin/sh", "-c"]
    command: 
      - |
        echo Starting
        python -m http.server 80
    ports:
      - 3000:80
    deploy:
      mode: global

EOF

docker swarm init
docker stack deploy -c python.yml python

Same result:

# netstat -tulpn
Active Internet connections (only servers)
Proto Recv-Q Send-Q Local Address           Foreign Address         State       PID/Program name
tcp        0      0 0.0.0.0:22              0.0.0.0:*               LISTEN      807/sshd: /usr/sbin
tcp6       0      0 :::3000                 :::*                    LISTEN      826/dockerd

I’m not him and definitely know less about Swarm, but have you tried curl as well or just checked the output of netstat? Apperantly when something is listening on all IPs including IPv6 IP addresses netstat will show you “:::PORT”. I’m not sure why.

I am not sure either, but I noticed it too! On some OSes applications that bind on ipv4/ipv6 dual stack, only the ipv6 binding is listed in netstat. I second what @rimelek suggested: try accessing the ipv4 address with wget, curl or nc,

Did two repositories and public docker images (see Makefile):

  1. GitHub - indapublic/docker-swarm-expose-test (Pure JS, Works)

  2. GitHub - indapublic/docker-swarm-expose-test-nextjs (NextJS, Issue)

So probably main reason is NextJS or something I didn’t notice. Do you have any idea?

Ok, another try. New Debian 12 VM.

sudo apt update && sudo apt -y upgrade
curl -fsSL https://get.docker.com -o get-docker.sh
sudo sh get-docker.sh
reboot
cat <<EOF > whoami.yml
version: '3'
services:
  whoami:
    image: traefik/whoami:v1.10
    hostname: '{{.Node.Hostname}}'
    ports:
      - 3000:80
    deploy:
      mode: global
EOF
docker swarm init
docker stack deploy -c whoami.yml whoami

Still netstats shows no no tcp (4):

# netstat -tulpn
Active Internet connections (only servers)
Proto Recv-Q Send-Q Local Address           Foreign Address         State       PID/Program name
tcp        0      0 0.0.0.0:22              0.0.0.0:*               LISTEN      826/sshd: /usr/sbin
tcp6       0      0 :::22                   :::*                    LISTEN      826/sshd: /usr/sbin
tcp6       0      0 :::3000                 :::*                    LISTEN      840/dockerd

But a curl works successfully:

# curl http://127.0.0.1:3000
Hostname: debian-4gb-nbg1-1
...

# curl http://<external-IP>:3000
Hostname: debian-4gb-nbg1-1
...

I actually did build the image from docker-swarm-expose-test-nextjs, and even though the nodejs webapp is accessible inside the container on port 3001, it is not accessible from the host on port 3001.

Not sure if it’s a problem with how the webapp is configured, but then again I have no idea of nodejs.

I tried the nextjs project and worked. I tried with the original image and also building a new image from the Dockerfile.

Did you change the port? The app listens on port 3000 which worked for me on Ubuntu 22.04. So the app at least seems working.

This is what I did:

git clone https://github.com/indapublic/docker-swarm-expose-test-nextjs.git
cd docker-swarm-expose-test-nextjs/
docker build -t indapublic/docker-swarm-expose-test-nextjs:test .
docker stack deploy -c stack.yaml test

Then I looked on the host and inside the container with netstat -tlpn, tried curl http://localhost:3000 and curl http://localhost:3001.

When I tested it, the current commit id was 57d2b4d49482bfb4f33e92133c007d5410a2d0a3.

It seems I thought the dummy.js was responsible for running the service which sets port 3001, which is strange because I was sure it set port 3000, but I was wrong., On the other hand, the environment var in the Dockerfile sets port 3000. I indeed tried a different commit, but I don’t see any change in ports. port 3000 works, on which the server is listening and it works from the host and from the container.

By explicitly setting HOSTNAME in my next.js project’s Docker image, I was able to solve this.

I found that when I run:

docker run -p 3000:3000 myimage
  • Docker maps the host machine’s port 3000 directly to the container’s port 3000
  • If Next.js is listening on 0.0.0.0 inside the container, then from your host machine, you can hit http://127.0.0.1:3000 (or http://localhost:3000) successfully

With Docker Swarm when you do:

version: "3.7"
services:
  app:
    image: myimage
    ports:
      - "3000:3000"
    deploy:
      mode: replicated
      replicas: 1
      placement:
        constraints: [node.role == manager]

Then run:

docker stack deploy -c stack.yaml myapp
  • I see the container is running and is listening on port 3000 (inside the container)
  • But accessing http://127.0.0.1:3000 (or curl localhost:3000) from the manager node gives “connection refused” or “connection reset”

This seems to come to the fact that Swarm publishes that port via its ingress network , so you typically have to use the node’s actual IP (e.g., 192.168.x.x:3000) from outside the container or from another machine.

Also, if your Next.js server is not listening on 0.0.0.0, but only on localhost, that can cause issues in Swarm. The container needs to bind to all interfaces so Swarm’s ingress traffic can route through.

I think this comes down to two main things:

  1. Swarm Routing Mesh

    • When you expose a port in Swarm (3000:3000), Swarm sets up routing that listens on the host IP (generally 0.0.0.0 on the node).
    • Traffic arrives at that external IP:port combination, then is forwarded inside to the container’s network interface on port 3000.
    • If you try to curl 127.0.0.1:3000 inside the container, it’s not the same interface that’s receiving the published traffic from outside.
  2. Next.js May Default to localhost

    • Some Node or Next.js setups default to listening on 127.0.0.1 or localhost only.
    • For container-based deployment (especially in Swarm), you typically need to bind to 0.0.0.0. That ensures the service is reachable on any network interface, including the swarm overlay network.
    • If Next.js only listens on 127.0.0.1, connections from the ingress are dropped.

TLDR; Set HOSTNAME=0.0.0.0 (or use -H 0.0.0.0 in your startup command) and then curl your node’s IP, you should see a successful response.