> Source: https://docs.nometa.az/v4.1.0/database-redundancy

# Database Redundancy

High availability for SAMURAI Networks' database with a clustered MongoDB **replica set**.

SAMURAI Networks stores all of its data in MongoDB. The bundled image runs a single embedded
`mongod`, which is fine for evaluation, but production deployments should point SAMURAI Networks at
an external **replica set** for high availability and durability. SAMURAI Networks connects to it
through the `MONGODB_URL` you set at deploy time (see [Deployment](/v4.1.0/deployment)).

## What a replica set is

A replica set is a group of `mongod` instances that hold the same data set:

- One **primary** receives all writes.
- Two or more **secondaries** replicate the primary's oplog and stay in sync.
- If the primary fails, the remaining members **elect** a new primary automatically,
  usually within seconds, and the application reconnects without manual intervention.

Run an **odd** number of voting members (three is the common minimum) so a majority can
always be reached to elect a primary. Three full data-bearing members is the recommended
baseline; a 2 + 1 layout (two data members plus a lightweight **arbiter**) also works but
keeps only one extra copy of the data.

A replica set provides **high availability** (failover and redundancy). It is not the same
as a **sharded cluster**, which spreads one data set across many shards for horizontal
scale. SAMURAI Networks needs a replica set, not sharding.

## Set up a three-node replica set

The example brings up a three-member set (`rs0`) with authentication enabled using Docker
Compose on one host. For a native deployment with each member on its own machine, see
[On independent VMs](#on-independent-vms) below.

### Create an internal-auth keyfile

Members authenticate to each other with a shared keyfile. Generate one and restrict its
permissions:

```bash
openssl rand -base64 756 > mongo-keyfile
chmod 400 mongo-keyfile
```

### Start the members

```yaml
# docker-compose.yml
services:
  mongo1:
    image: mongo:8
    command: ["--replSet", "rs0", "--keyFile", "/etc/mongo/keyfile", "--bind_ip_all"]
    volumes:
      - ./mongo-keyfile:/etc/mongo/keyfile:ro
      - mongo1-data:/data/db
    networks: [samurai-db]
  mongo2:
    image: mongo:8
    command: ["--replSet", "rs0", "--keyFile", "/etc/mongo/keyfile", "--bind_ip_all"]
    volumes:
      - ./mongo-keyfile:/etc/mongo/keyfile:ro
      - mongo2-data:/data/db
    networks: [samurai-db]
  mongo3:
    image: mongo:8
    command: ["--replSet", "rs0", "--keyFile", "/etc/mongo/keyfile", "--bind_ip_all"]
    volumes:
      - ./mongo-keyfile:/etc/mongo/keyfile:ro
      - mongo3-data:/data/db
    networks: [samurai-db]
networks:
  samurai-db:
    name: samurai-db
volumes:
  mongo1-data:
  mongo2-data:
  mongo3-data:
```

```bash
docker compose up -d
```

**Why no published ports?** Each container has its own network namespace and IP, so all
three `mongod` processes listen on 27017 at the same time without colliding. A port clash
only happens when you map the same **host** port (for example `-p 27017:27017`) to more
than one container. Here the members reach each other over the `samurai-db` Docker network
by service name (`mongo1:27017`, `mongo2:27017`, `mongo3:27017`), and SAMURAI Networks joins the
same network, so no host ports are published. Run admin commands with `docker compose exec`.

Enabling a keyfile also turns on access control. Initialize the set and create the first
user in the same session through MongoDB's **localhost exception**, before any other user
exists.

### Initialize the set

```bash
docker compose exec mongo1 mongosh --eval '
  rs.initiate({
    _id: "rs0",
    members: [
      { _id: 0, host: "mongo1:27017" },
      { _id: 1, host: "mongo2:27017" },
      { _id: 2, host: "mongo3:27017" }
    ]
  })'
```

Use real hostnames or IPs (reachable from SAMURAI Networks and between members) instead of the
Compose service names in a multi-host deployment.

### Create the users

```bash
docker compose exec mongo1 mongosh --eval '
  admin = db.getSiblingDB("admin");
  admin.createUser({ user: "admin", pwd: "CHANGE_ME", roles: ["root"] });
  admin.auth("admin", "CHANGE_ME");
  admin.createUser({
    user: "samurai",
    pwd: "CHANGE_ME_TOO",
    roles: [{ role: "readWrite", db: "samurai" }]
  })'
```

## On independent VMs

The Compose setup runs all three members on one host, which is convenient but shares a
single failure domain. In production, run each member on its own VM with MongoDB installed
natively. The replica-set logic is identical; only how you provision and start the members
changes.

### Install MongoDB on each VM (Ubuntu)

Install MongoDB 8.0 from the official APT repository on all three VMs:

```bash
sudo apt-get install -y gnupg curl
curl -fsSL https://www.mongodb.org/static/pgp/server-8.0.asc \
  | sudo gpg -o /usr/share/keyrings/mongodb-server-8.0.gpg --dearmor
echo "deb [ arch=amd64,arm64 signed-by=/usr/share/keyrings/mongodb-server-8.0.gpg ] https://repo.mongodb.org/apt/ubuntu jammy/mongodb-org/8.0 multiverse" \
  | sudo tee /etc/apt/sources.list.d/mongodb-org-8.0.list
sudo apt-get update
sudo apt-get install -y mongodb-org
```

The repository codename above is for **Ubuntu 22.04 (jammy)**; use `noble` for 24.04. See
[Install on Ubuntu](https://www.mongodb.com/docs/manual/tutorial/install-mongodb-on-ubuntu/)
for other releases.

### Distribute the keyfile

Create the keyfile once (as above) and copy it to the same path on every VM, owned by the
MongoDB service user `mongodb` with `400` permissions:

```bash
sudo install -o mongodb -g mongodb -m 400 mongo-keyfile /etc/mongod.keyfile
```

### Configure each member

Edit `/etc/mongod.conf` on every VM so it joins `rs0`, enables internal auth, and listens
on an address the other members and SAMURAI Networks can reach:

```yaml
net:
  port: 27017
  bindIp: 0.0.0.0          # or the VM's private IP / a comma-separated list
security:
  keyFile: /etc/mongod.keyfile
replication:
  replSetName: rs0
```

Allow TCP 27017 between the three VMs and inbound from the SAMURAI Networks host, then enable on
boot and (re)start MongoDB on each VM so it picks up the config:

```bash
sudo systemctl enable mongod
sudo systemctl restart mongod
```

### Initialize and create users

From the first VM, run `mongosh` and issue the same `rs.initiate(...)` and user creation
shown above, but use each VM's hostname or IP for the members:

```js
rs.initiate({
  _id: "rs0",
  members: [
    { _id: 0, host: "db1.example.com:27017" },
    { _id: 1, host: "db2.example.com:27017" },
    { _id: 2, host: "db3.example.com:27017" }
  ]
})
```

## Point SAMURAI Networks at the replica set

Set `MONGODB_URL` to a connection string that lists every member and the replica-set name,
plus `MONGODB_DATABASE`:

```bash
docker run -d --name samurai --network samurai-db -p 80:80 -p 443:443 \
  -v samurai-data:/app/mongo-data \
  -e MONGODB_URL="mongodb://samurai:CHANGE_ME_TOO@mongo1:27017,mongo2:27017,mongo3:27017/samurai?replicaSet=rs0&authSource=admin" \
  -e MONGODB_DATABASE="samurai" \
  beyrak44/samurai:latest
```

- `--network samurai-db` joins SAMURAI Networks to the same Docker network as the members, so the
  `mongoN` service names in the URL resolve. On independent VMs, omit `--network` and use
  the VM hostnames or IPs in the URL instead.
- List **all** members in the URL so the driver can find the primary after a failover.
- `replicaSet=rs0` tells the driver to discover the set and follow primary elections.
- `authSource=admin` matches where the `samurai` user was created.
- The embedded `mongod` volume (`-v samurai-data:/app/mongo-data`) is not used for data once
  an external URL is reachable, but leaving it mounted is harmless.

SAMURAI Networks is **external-first**: when `MONGODB_URL` is set and reachable it uses the replica
set; if it is set but unreachable at startup it falls back to the embedded `mongod` (see
[Deployment](/v4.1.0/deployment)). Confirm the application logs show it connected to your
set.

Use TLS and a private network between SAMURAI Networks and the database in production. Keep the
keyfile and user passwords in a secret store, not in the Compose file or shell history.

## Verify failover

Stop the current primary and confirm a secondary is elected and SAMURAI Networks keeps working:

```bash
docker compose exec mongo1 mongosh --eval 'rs.status().members.map(m => ({ name: m.name, state: m.stateStr }))'
docker compose stop mongo1   # the current primary
# re-run rs.status() against mongo2; a new PRIMARY should appear within seconds
```

## Resources

- [Replication (concepts)](https://www.mongodb.com/docs/manual/replication/)
- [Deploy a replica set](https://www.mongodb.com/docs/manual/tutorial/deploy-replica-set/)
- [Replica set with keyfile access control](https://www.mongodb.com/docs/manual/tutorial/deploy-replica-set-with-keyfile-access-control/)
- [Connection string URI format](https://www.mongodb.com/docs/manual/reference/connection-string/)
- [Configure TLS/SSL](https://www.mongodb.com/docs/manual/tutorial/configure-ssl/)
- [Security checklist](https://www.mongodb.com/docs/manual/administration/security-checklist/)
