DevOps

Fix RabbitMQ ECONNREFUSED 5672 between Docker containers

Your consumer starts, RabbitMQ starts, and the first connect is refused. Fix the amqp URL, pick a healthcheck that waits for port 5672, and avoid the guest trap.

Mahdi Ben RhoumaMahdi Ben Rhouma
8 min read

TL;DR#

connect ECONNREFUSED 127.0.0.1:5672 from a Node consumer running in Docker Compose has two RabbitMQ-flavoured causes. First, the amqp URL points at localhost, which inside a container is the consumer itself: use the broker's service name, as in amqp://guest:guest@rabbitmq:5672. Second, once the host is right, RabbitMQ takes several seconds to open port 5672 after its container starts, so a consumer that connects once at boot is refused again, this time at the broker's private IP. Gate the consumer on a healthcheck that tests the AMQP port (rabbitmq-diagnostics -q check_port_connectivity), not ping, which reports healthy too early. If you mount your own /etc/rabbitmq/, expect ACCESS_REFUSED next.

The error#

The case comes from a Stack Overflow question: a RabbitMQ container and a Seneca consumer started by the same docker-compose.yml, with the consumer reading its broker address from AMQP_RECEIVE_URL. Seneca wraps the socket error in its own message:

text
seneca: Action hook:listen,role:transport,type:amqp failed: connect ECONNREFUSED 127.0.0.1:5672.

The question uses Seneca; the same failure appears with amqplib, the plain AMQP 0-9-1 client for Node. Reproduced on 10 October 2026 with amqplib 2.2.0, Node 22.23.3 (node:22-alpine) and RabbitMQ 3.13.7 (rabbitmq:3-management), Docker Engine 29.7.2 and Compose 5.5.0, an unhandled amqp.connect('amqp://guest:guest@localhost:5672') prints (abridged: the stack frames and errno fields of the two inner errors are cut):

text
AggregateError [ECONNREFUSED]: 
    at internalConnectMultiple (node:net:1135:18)
    at afterConnectMultiple (node:net:1716:7) {
  code: 'ECONNREFUSED',
  [errors]: [
    Error: connect ECONNREFUSED ::1:5672
    Error: connect ECONNREFUSED 127.0.0.1:5672

Note the empty message on the first line. On this Node version localhost resolved to both ::1 and 127.0.0.1, both attempts were refused, and the error that reaches your code is an AggregateError whose message is an empty string. A console.error(err.message) in a catch block prints a blank line; log err.code and err.errors instead, or you will be debugging a silent crash.

Why it happens#

The wrong host. The accepted answer on the question names the first cause: "the listener is trying to connect to localhost(127.0.0.1) which is not the rabbitmq service container IP". Each container has its own loopback interface, and Compose registers every service name in its internal DNS. That part is not specific to RabbitMQ: the ECONNREFUSED 127.0.0.1:5432 fix for Postgres walks through it, including why the host side of a ports mapping is irrelevant between two services. The question's ports: - 5672:5672 line only matters for clients on your machine.

A broker that is running but not listening. The second answer suspects the other cause: "your RabbitMQ service is not fully up, at the time the connection is attempted". This is where RabbitMQ differs from a lighter service. The Compose startup-order guide says "Compose does not wait until a container is 'ready', only until it's running", and a RabbitMQ container spends a long time between the two: the Erlang runtime boots, the broker waits for its metadata store, and only then does the AMQP listener open. In the reproduction, the start banner and the listener were about 12 seconds apart:

text
rabbitmq-1  | 2026-10-10 22:24:37.639582+00:00 [info] <0.254.0>  Starting RabbitMQ 3.13.7 on Erlang 26.2.5.16 [jit]
rabbitmq-1  | 2026-10-10 22:24:49.292169+00:00 [info] <0.846.0> started TCP listener on [::]:5672
rabbitmq-1  | 2026-10-10 22:24:49.404605+00:00 [info] <0.684.0> Server startup complete; 5 plugins started.

With the host fixed to rabbitmq and a short-form depends_on: - rabbitmq, the consumer connected inside that window and was refused at the broker's private address:

text
consumer-1  | [consumer] connecting to amqp://guest:guest@rabbitmq:5672
consumer-1  | [consumer] code=ECONNREFUSED message=connect ECONNREFUSED 172.20.0.2:5672

The address in the error tells you which cause you have. 127.0.0.1 or ::1 means the URL still says localhost. A private address such as 172.20.0.2 means DNS worked and the broker had not opened the port yet.

The image ships no healthcheck. The official image's Docker Hub page says it "does not come with a default HEALTHCHECK defined", and points users to write their own. Without one, condition: service_healthy has nothing to wait for.

Fix#

  1. Point the URL at the service name and pass it to the consumer. The accepted answer's un.env holds exactly this (under the name the question's Seneca code reads, AMQP_RECEIVE_URL; the examples below call it AMQP_URL), and the consumer must read it rather than fall back to a default — amqplib connects to amqp://localhost when it is given no URL:

    bash
    AMQP_URL=amqp://guest:guest@rabbitmq:5672
  2. Add a healthcheck that tests the AMQP port, and make the consumer wait for it. RabbitMQ's monitoring guide orders its health checks in stages. Stage 1, rabbitmq-diagnostics -q ping, only checks that the Erlang runtime is running (and, indirectly, that CLI tools can authenticate with it); it says nothing about client listeners. Stage 4 adds check_port_connectivity, which "will try to open a TCP connection to the discovered listener ports". The second one is what a client needs:

    yaml
    services:
      rabbitmq:
        image: rabbitmq:3-management
        hostname: rabbitmq
        healthcheck:
          test: ["CMD", "rabbitmq-diagnostics", "-q", "check_port_connectivity"]
          interval: 5s
          timeout: 10s
          retries: 10
          start_period: 30s
      consumer:
        build: .
        restart: on-failure
        environment:
          AMQP_URL: amqp://guest:guest@rabbitmq:5672
        depends_on:
          rabbitmq:
            condition: service_healthy

    The image tag is the one the reproduction ran; check_port_connectivity is documented in the current RabbitMQ monitoring guide, so the same healthcheck applies to the 4.x images. The hostname line is not decoration. The Docker Hub page explains that RabbitMQ stores its data under a node name that "defaults to the hostname", and asks for an explicit hostname "so that we don't get a random hostname and can keep track of our data". That matters as soon as /var/lib/rabbitmq sits on a volume, as it does in the question.

    The monitoring guide also notes that most CLI commands, rabbitmq-diagnostics included, join the Erlang distribution, which has a cost, and that the RabbitMQ Kubernetes Operator uses a plain TCP check on the AMQP port as its readiness probe. That is the same idea as the accepted answer's wait-for-it.sh rabbitmq:5672 wrapper: wait for the port, not for the process. Either works; the healthcheck keeps the waiting in the Compose file instead of the consumer's image.

  3. Retry the first connection in the consumer anyway. A healthcheck only helps where Compose honours depends_on, and it does nothing if the broker restarts while the consumer runs. A bounded retry loop covers start-up on any platform, and exiting on close lets the restart: on-failure policy from step 2 bring the consumer back after a broker restart:

    js
    // consumer.js (amqplib)
    const amqp = require('amqplib');
     
    const url = process.env.AMQP_URL;
    if (!url) throw new Error('AMQP_URL is not set');
     
    async function connectWithRetry(attempts = 15, delayMs = 2000) {
      for (let i = 1; i <= attempts; i += 1) {
        try {
          return await amqp.connect(url);
        } catch (err) {
          const reason = err.code || err.message;
          console.warn(`RabbitMQ not ready (attempt ${i}/${attempts}): ${reason}`);
          if (i === attempts) throw err;
          await new Promise((resolve) => setTimeout(resolve, delayMs));
        }
      }
    }
     
    (async () => {
      const conn = await connectWithRetry();
      conn.on('error', (err) => console.error('AMQP connection error:', err.message));
      conn.on('close', () => {
        console.error('AMQP connection closed, exiting so Docker restarts the consumer');
        process.exit(1);
      });
      const ch = await conn.createChannel();
      await ch.assertQueue('new_user_notification');
      console.log('Connected, waiting for messages on new_user_notification');
      await ch.consume('new_user_notification', (msg) => {
        if (msg === null) return;
        console.log('Received:', msg.content.toString());
        ch.ack(msg);
      });
    })().catch((err) => {
      console.error('Giving up on RabbitMQ:', err);
      process.exit(1);
    });

    Note err.code || err.message: it keeps the log readable when the error is the AggregateError with an empty message shown above.

  4. Recreate the stack, so the consumer gets the new environment and the broker gets its healthcheck:

    bash
    docker compose down
    docker compose up --build

Verify the fix#

With the healthcheck from step 2, Compose holds the consumer until the broker is healthy, and the first connection succeeds. From the reproduction:

text
rabbitmq-1  | 2026-10-10 22:24:49.292169+00:00 [info] <0.846.0> started TCP listener on [::]:5672
 Container aotd-docker-and-rabbitmq-econnrefused-between-containers-rabbitmq-1 Healthy 
consumer-1  | [consumer] connected after 243 ms, queue asserted

Without any healthcheck, the retry loop from step 3 shows the boot window from the client's side: eight refusals, two seconds apart, then a connection once the listener is up:

text
consumer-1  | RabbitMQ not ready (attempt 7/15): ECONNREFUSED
consumer-1  | RabbitMQ not ready (attempt 8/15): ECONNREFUSED
rabbitmq-1  | 2026-10-10 22:26:39.567401+00:00 [info] <0.838.0> started TCP listener on [::]:5672
consumer-1  | Connected, waiting for messages on new_user_notification

The close handler earns its place when the broker restarts under a running consumer. After docker compose restart rabbitmq, amqplib emitted close, the consumer exited, Docker restarted it once (the container's restart count went to 1), and the retry loop waited out the new boot:

text
consumer-1  | AMQP connection closed, exiting so Docker restarts the consumer
consumer-1  | RabbitMQ not ready (attempt 1/15): ECONNREFUSED
consumer-1  | RabbitMQ not ready (attempt 7/15): ECONNREFUSED
consumer-1  | Connected, waiting for messages on new_user_notification

On your own stack, docker compose ps should list the broker as healthy, and docker compose exec rabbitmq rabbitmq-diagnostics -q listeners lists the AMQP listener on 5672 once it is open.

One email a month — the latest explained errors

What the site published that month — mostly Next.js, Supabase and Node errors, explained. You confirm by email; unsubscribe any time in one click.

Why a ping healthcheck still lets ECONNREFUSED through#

rabbitmq-diagnostics -q ping looks like the natural healthcheck, and it is the one that fooled the reproduction. With only the test line changed to ping, Compose marked the broker healthy, started the consumer, and the consumer was refused, twice out of two runs:

text
 Container aotd-docker-and-rabbitmq-econnrefused-between-containers-rabbitmq-1 Healthy 
consumer-1  | [consumer] connecting to amqp://guest:guest@rabbitmq:5672
consumer-1  | [consumer] code=ECONNREFUSED message=connect ECONNREFUSED 172.20.0.2:5672

This is consistent with how the monitoring guide defines ping: the runtime answers it before the RabbitMQ application has opened its client listeners. If your healthcheck says healthy and the consumer still gets ECONNREFUSED at the broker's IP, check which command the healthcheck runs before anything else.

The next error: ACCESS_REFUSED after mounting /etc/rabbitmq#

The question's Compose file also mounts a host directory over the whole config directory, /rabbitmq/conf:/etc/rabbitmq/. That hides the files the official image puts there. One of them, conf.d/10-defaults.conf, contains loopback_users.guest = false, with the comment "allow access to the guest user from anywhere on the network". RabbitMQ's own default, per its access control guide, is that "the guest user is prohibited from connecting from remote hosts".

So once the host and the timing are fixed, a consumer connecting as guest from another container fails in a new way. Reproduced with the healthcheck from step 2 and a mounted directory that contains only a one-line rabbitmq.conf (log.console = true):

text
rabbitmq-1  | 2026-10-10 22:25:48.756869+00:00 [info] <0.660.0> Server startup complete; 0 plugins started.
rabbitmq-1  | 2026-10-10 22:25:52.796628+00:00 [error] <0.697.0> PLAIN login refused: user 'guest' can only connect via localhost
consumer-1  | [consumer] code=undefined message=Handshake terminated by server: 403 (ACCESS-REFUSED) with message "ACCESS_REFUSED - Login was refused using authentication mechanism PLAIN. For details see the broker logfile."

The same mount also hid /etc/rabbitmq/enabled_plugins, which in this image lists the management and Prometheus plugins: the broker logged "0 plugins started", against "5 plugins started" without the mount, so the management plugin was not running either. The fix RabbitMQ recommends is a user of your own, not re-opening guest. The image reads RABBITMQ_DEFAULT_USER and RABBITMQ_DEFAULT_PASS; with the same mount kept and only those two variables and the URL changed, the consumer connected:

yaml
services:
  rabbitmq:
    image: rabbitmq:3-management
    hostname: rabbitmq
    volumes:
      - ./conf-own:/etc/rabbitmq/
    environment:
      RABBITMQ_DEFAULT_USER: app
      RABBITMQ_DEFAULT_PASS: change-me
  consumer:
    environment:
      AMQP_URL: amqp://app:change-me@rabbitmq:5672

The default user is created only when a node starts with an uninitialised database (a "blank node"), so on a volume that already holds data, recreate the volume or add the user with rabbitmqctl. Better still, mount single files into /etc/rabbitmq/conf.d/ rather than replacing the directory. If the variables never reach the container at all, check the YAML shape of the environment block: a - KEY: value item fails with the Compose "unexpected type map" error.

The reproduction used RabbitMQ 3.13.7 because that image was already on the test machine; the Docker Hub tags are now on the 4.x series, whose image ships the same 10-defaults.conf line and no healthcheck. When the same kind of failure shows up in CI rather than on your machine, with a job running inside a container, the GitHub Actions ECONNREFUSED fix explains which host a job container must use. With the service name in the URL, a healthcheck that waits for the AMQP port, and a user that is allowed to log in remotely, the consumer's first connection is no longer a race against the broker's boot.

Further reading: Docker: Run a Shell Command Before ENTRYPOINT at Startup.

Frequently Asked Questions

|

Have more questions? Contact us

Still stuck? Paste your error.

Paste the error message or stack trace — we check it against every fix and tool on the site.

Matching happens in your browser. When you search, a cleaned error signature is sent so we can spot errors we do not cover yet — never the raw text, your IP address or your e-mail. Do not paste passwords or keys. Details

Written by

Mahdi Ben Rhouma
Mahdi Ben Rhouma

Full-Stack Dev — Next.js & Supabase

Solo developer building SaaS products with Next.js and Supabase. Writing about production patterns the official docs skip.

Remote