Search code examples
node.jsmongodbdocker-composereplicaset

Mongo single node replicaset on Docker, MongooseServerSelectionError: connect ECONNREFUSED


I'm trying to set up a single node MongoDB replicaset on Docker and connect to it within my node app, but the connection is refused. The connection works fine if I use mongo as a standalone instance and no replicaset.

This is how I connect to mongo on my node app :

mongoose.connect("mongodb://admin:secretpass@app_mongodb:27017/dbname?authSource=admin&replicaSet=rs0")

And this is the error I receive :

/var/www/worker/node_modules/mongoose/lib/connection.js:824
const serverSelectionError = new ServerSelectionError();
                               ^
MongooseServerSelectionError: connect ECONNREFUSED 127.0.0.1:27017

Mongo logs show that the app tries to connect :

{"t":{"$date":"2023-01-27T10:22:46.410+00:00"},"s":"I",  "c":"NETWORK",  "id":22943,   "ctx":"listener","msg":"Connection accepted","attr":{"remote":"172.18.0.10:41318","uuid":"2e1ccbc5-3162-4f64-80f3-8be580079ef6","connectionId":68,"connectionCount":11}}
{"t":{"$date":"2023-01-27T10:22:46.417+00:00"},"s":"I",  "c":"NETWORK",  "id":51800,   "ctx":"conn68","msg":"client metadata","attr":{"remote":"172.18.0.10:41318","client":"conn68","doc":{"driver":{"name":"nodejs|Mongoose","version":"4.11.0"},"os":{"type":"Linux","name":"linux","architecture":"x64","version":"5.10.0-12-amd64"},"platform":"Node.js v19.5.0, LE (unified)","version":"4.11.0|6.7.5"}}}
{"t":{"$date":"2023-01-27T10:22:46.425+00:00"},"s":"I",  "c":"NETWORK",  "id":22944,   "ctx":"conn68","msg":"Connection ended","attr":{"remote":"172.18.0.10:41318","uuid":"2e1ccbc5-3162-4f64-80f3-8be580079ef6","connectionId":68,"connectionCount":10}}

docker-compose.yml :

version: '3.8'

services:
    mongodb:
        container_name: app_mongodb
        build:
            dockerfile: ./Dockerfile
            x-bake:
                no-cache: true
        environment:
            - MONGO_INITDB_ROOT_USERNAME=admin
            - MONGO_INITDB_ROOT_PASSWORD=secretpass
        volumes:
            - ./db:/data/db
        networks:
            - proxy
        restart: unless-stopped
    worker:
        container_name: app_worker
        image: "node:latest"
        command: "npm run dev"
        user: "node"
        working_dir: /var/www/worker
        environment:
            WAIT_HOSTS: app_mongodb:27017
        volumes:
            - ./worker:/var/www/worker
        networks:
            - proxy
        links:
            - mongodb
        depends_on:
            - mongodb
        restart: unless-stopped
networks:
    proxy:
        external: true

Dockerfile :

FROM mongo

# Initiate replica set
RUN echo "secretpasswordkey" > "/tmp/replica.key"
RUN chmod 600 /tmp/replica.key
RUN chown 999:999 /tmp/replica.key

CMD ["mongod", "--replSet", "rs0", "--bind_ip_all", "--keyFile", "/tmp/replica.key"]

And I also run this command with mongosh after starting up the container (is there a way to add that to the Dockerfile instead by the way?) :

rs.initiate({_id: 'rs0', members: [{_id:1, 'host':'127.0.0.1:27017'}]})

A quick check shows the replicaset is indeed initialized :

rs0 [direct: primary] admin> rs.status()
{
  set: 'rs0',
  date: ISODate("2023-01-27T10:35:44.062Z"),
  myState: 1,
  term: Long("1"),
  syncSourceHost: '',
  syncSourceId: -1,
  heartbeatIntervalMillis: Long("2000"),
  majorityVoteCount: 1,
  writeMajorityCount: 1,
  votingMembersCount: 1,
  writableVotingMembersCount: 1,
  optimes: {
    lastCommittedOpTime: { ts: Timestamp({ t: 1674815739, i: 1 }), t: Long("1") },
    lastCommittedWallTime: ISODate("2023-01-27T10:35:39.161Z"),
    readConcernMajorityOpTime: { ts: Timestamp({ t: 1674815739, i: 1 }), t: Long("1") },
    appliedOpTime: { ts: Timestamp({ t: 1674815739, i: 1 }), t: Long("1") },
    durableOpTime: { ts: Timestamp({ t: 1674815739, i: 1 }), t: Long("1") },
    lastAppliedWallTime: ISODate("2023-01-27T10:35:39.161Z"),
    lastDurableWallTime: ISODate("2023-01-27T10:35:39.161Z")
  },
  lastStableRecoveryTimestamp: Timestamp({ t: 1674815679, i: 1 }),
  electionCandidateMetrics: {
    lastElectionReason: 'electionTimeout',
    lastElectionDate: ISODate("2023-01-27T10:25:49.015Z"),
    electionTerm: Long("1"),
    lastCommittedOpTimeAtElection: { ts: Timestamp({ t: 1674815148, i: 1 }), t: Long("-1") },
    lastSeenOpTimeAtElection: { ts: Timestamp({ t: 1674815148, i: 1 }), t: Long("-1") },
    numVotesNeeded: 1,
    priorityAtElection: 1,
    electionTimeoutMillis: Long("10000"),
    newTermStartDate: ISODate("2023-01-27T10:25:49.082Z"),
    wMajorityWriteAvailabilityDate: ISODate("2023-01-27T10:25:49.111Z")
  },
  members: [
    {
      _id: 1,
      name: '127.0.0.1:27017',
      health: 1,
      state: 1,
      stateStr: 'PRIMARY',
      uptime: 643,
      optime: { ts: Timestamp({ t: 1674815739, i: 1 }), t: Long("1") },
      optimeDate: ISODate("2023-01-27T10:35:39.000Z"),
      lastAppliedWallTime: ISODate("2023-01-27T10:35:39.161Z"),
      lastDurableWallTime: ISODate("2023-01-27T10:35:39.161Z"),
      syncSourceHost: '',
      syncSourceId: -1,
      infoMessage: '',
      electionTime: Timestamp({ t: 1674815149, i: 1 }),
      electionDate: ISODate("2023-01-27T10:25:49.000Z"),
      configVersion: 1,
      configTerm: 1,
      self: true,
      lastHeartbeatMessage: ''
    }
  ],
  ok: 1,
  '$clusterTime': {
    clusterTime: Timestamp({ t: 1674815739, i: 1 }),
    signature: {
      hash: Binary(Buffer.from("a211cfe3faa237bf8a30ccbc8ca929eea704f467", "hex"), 0),
      keyId: Long("7193276291800367109")
    }
  },
  operationTime: Timestamp({ t: 1674815739, i: 1 })
}

Ideas on what I'm doing wrong ? Thanks !


Solution

  • Thanks to @WernfriedDomscheit's comment pointing me to the right direction, I realized I have to give the mongodb container hostname instead of 127.0.0.1 when initiating the replicaset :

    rs.initiate({_id: 'rs0', members: [{_id: 1, 'host': 'mongodb:27017'}]})
    

    I was able to automate initialization by adding a healthcheck routine to my docker-compose.yml file. The Dockerfile is not necessary anymore.

    The whole mongodb container description is now this :

        mongodb:
            container_name: app_mongodb
            environment:
                - MONGO_INITDB_ROOT_USERNAME=${MONGODB_USER}
                - MONGO_INITDB_ROOT_PASSWORD=${MONGODB_PASS}
            volumes:
                - ./db:/data/db
            networks:
                - proxy
            restart: unless-stopped
            healthcheck:
                test: test $$(echo 'rs.initiate({_id':' "rs0", members':' [{_id':' 1, "host"':' "mongodb':'27017"}]}) || rs.status().ok' | mongosh -u $${MONGO_INITDB_ROOT_USERNAME} -p $${MONGO_INITDB_ROOT_PASSWORD} --quiet) -eq 1
                interval: 10s
                start_period: 30s
    

    It's important to set the host in the initiate() and not leave the arguments empty because otherwise by default it'll setup the replicaset using the container ID as host, and if the container is removed and restarted the replicaset won't work anymore as the new container ID will be different.