How to Fix: ECONNREFUSED when call backend API with docker name from /pages/api

6 min read

Your Next.js API route can reach localhost, but it fails with ECONNREFUSED when you switch to a Docker service name because the code inside /pages/api is not always running in the same network context you think it is. In practice, the request is often executed by the Next.js server process on the host or in a different container, while the backend service name only resolves correctly inside the Docker network.

Understanding the Root Cause

This issue happens because Docker service discovery only works for containers attached to the same Docker network. If your backend is named backend in docker-compose, then http://backend:5000 is resolvable only from another container on that same network.

With Next.js 14, code under /pages/api runs server-side. That does not automatically mean it runs inside Docker. If your frontend is started with next dev on your machine, then the API route executes on the host OS, not in the Docker network. In that case:

  • backend is not a valid DNS name on the host.
  • localhost refers to the machine running Next.js, not the backend container.
  • The request fails with ECONNREFUSED or name resolution errors depending on the exact setup.

Another common source of confusion is that browser-side code and server-side code resolve addresses differently:

  • In the browser, localhost means the user’s machine.
  • In a container, localhost means that container itself.
  • In a Next.js API route, the target hostname depends on where the Next.js server process is actually running.

So the real root cause is usually one of these:

  • The Next.js app is not running in Docker, but tries to call the backend using a Docker service name.
  • The frontend and backend containers are not on the same network.
  • The backend is listening on a different port internally than the one being requested.
  • The backend binds only to 127.0.0.1 instead of 0.0.0.0, making it unreachable from other containers.

Step-by-Step Solution

The fix is to make your runtime network context match your hostname choice.

1. Run both services in the same Docker network

If you want to call the backend by Docker service name such as backend, the Next.js server must also run in Docker on the same network.

version: '3.9'
services:
  frontend:
    build: ./frontend
    ports:
      - "3000:3000"
    environment:
      - BACKEND_URL=http://backend:8080
    depends_on:
      - backend

  backend:
    build: ./backend
    ports:
      - "8080:8080"
    environment:
      - ASPNETCORE_URLS=http://0.0.0.0:8080

Key detail: use the container-internal port when one container calls another. If the backend container listens on 8080, then the frontend should call http://backend:8080, not the published host port unless that is also the internal port.

2. Use an environment variable instead of hardcoding the hostname

Inside your Next.js API route, do not hardcode localhost or a Docker name directly. Read the backend URL from an environment variable.

const BACKEND_URL = process.env.BACKEND_URL;

export default async function handler(req, res) {
  try {
    const response = await fetch(`${BACKEND_URL}/api/values`);
    const data = await response.json();
    res.status(200).json(data);
  } catch (error) {
    console.error('API proxy error:', error);
    res.status(500).json({ message: 'Failed to reach backend API' });
  }
}

This gives you a clean split:

  • Use BACKEND_URL=http://backend:8080 when Next.js runs in Docker.
  • Use BACKEND_URL=http://localhost:8080 when Next.js runs on your host machine and the backend port is published.

3. Make sure the .NET backend listens on all interfaces

A frequent cause of ECONNREFUSED is that the .NET app is only listening on loopback. In Docker, that prevents other containers from connecting.

ASPNETCORE_URLS=http://0.0.0.0:8080

You can set it in docker-compose.yml, your Dockerfile, or container environment config.

For example:

services:
  backend:
    build: ./backend
    environment:
      - ASPNETCORE_URLS=http://0.0.0.0:8080

4. Verify the backend port mapping versus listening port

Developers often confuse these two values:

  • Published host port: the port exposed to your machine, like 8080:8080
  • Container listening port: the actual port the app listens on inside the container

If your backend listens internally on 80, then from the frontend container you must call http://backend:80, even if Docker publishes it as 8080:80.

services:
  backend:
    ports:
      - "8080:80"

In this case, container-to-container traffic should use:

http://backend:80

5. Confirm the frontend is really running where you expect

If you start Next.js with a local command like this:

npm run dev

then /pages/api runs on your host machine, not in the Docker network. In that scenario, http://backend:8080 will usually fail because backend is only known to Docker DNS.

Use one of these approaches:

  • Run the Next.js app in Docker too, so it can resolve backend.
  • Keep Next.js on the host, but point it to the published backend URL like http://localhost:8080.

6. Add a minimal Dockerfile for Next.js if needed

FROM node:20-alpine
WORKDIR /app
COPY package*.json ./
RUN npm install
COPY . .
EXPOSE 3000
CMD ["npm", "run", "dev"]

Then run both services together with Docker Compose so service-name resolution works consistently.

7. Test connectivity from inside the frontend container

If the issue persists, test the request from the frontend container itself.

docker compose exec frontend sh
wget -qO- http://backend:8080/api/values

If this fails, the problem is not in Next.js code. It is a networking, port, or backend binding issue.

A stable setup usually looks like this:

  • Next.js in Docker calls http://backend:PORT
  • Next.js on host calls http://localhost:PUBLISHED_PORT
  • Use environment-specific variables so the code does not change between environments
# .env.local for host development
BACKEND_URL=http://localhost:8080
# docker-compose environment
BACKEND_URL=http://backend:8080

Common Edge Cases

  • Using the wrong port: The backend may expose 8080 publicly but listen on 80 internally.
  • Backend startup delay: depends_on starts containers in order, but does not guarantee the backend is ready to accept traffic.
  • .NET binding issue: If Kestrel binds only to localhost, other containers cannot connect.
  • Mixed runtime context: Browser code, Next.js API routes, and containerized services may all resolve hostnames differently.
  • Using host.docker.internal incorrectly: This is useful when a container needs to reach the host, not when containers should talk to each other through Docker DNS.
  • Custom Docker networks missing: If services are not attached to the same network, service-name resolution will fail.
  • Proxy or rewrite confusion: If Next.js rewrites are involved, you may be debugging the wrong hop.

FAQ

Why does localhost work in one place but not in another?

Because localhost is relative to the current runtime. In your browser it means your machine. In a container it means that specific container. In a Next.js API route it means the environment where the Next.js server process is running.

Can /pages/api access Docker service names directly?

Yes, but only if the Next.js server itself runs inside Docker on the same network as the backend. If Next.js runs on the host, Docker service names like backend are usually not resolvable.

Why do I still get ECONNREFUSED even though the hostname resolves?

If DNS resolution works but the connection is refused, the backend is likely not listening on that port, not ready yet, or bound only to 127.0.0.1 instead of 0.0.0.0.

The practical fix is simple: match the hostname to where your Next.js API route is running. Use backend only for container-to-container traffic, use localhost or another host-reachable address for host-to-container traffic, and keep the target URL in an environment variable so your setup stays predictable across development and Docker environments.

Leave a Reply

Your email address will not be published. Required fields are marked *