Compare commits

..

11 Commits

Author SHA1 Message Date
5b02a13cb4 ludo web based game config file changes 2026-01-28 14:02:27 +05:30
Wiktor Smaga
4cd0fe970f
Update README.md 2025-04-22 21:09:27 +02:00
Wiktor Smaga
e66965dfc1
Update README.md 2025-04-21 21:52:34 +02:00
Wiktor Smaga
98cf490ad0
Update README.md 2025-03-30 18:24:03 +02:00
Wenszel
4c0286952c Changed AWS Account 2025-03-11 22:53:46 +01:00
Wiktor Smaga
c835846c2d
Merge pull request #10 from Wenszel/feature-deployment
Feature deployment
2024-01-18 15:29:35 +01:00
Wenszel
4366b6aba4 added deploying to ECS 2024-01-18 15:27:48 +01:00
Wenszel
0b21489989 added pushing to AWS ECR 2023-12-28 17:22:34 +01:00
Wenszel
766614f5d2 added deploying a docker image to docker hub 2023-12-20 18:21:45 +01:00
Wenszel
e5a69fa4a9 fixed timeout issue
implemented timeoutManager, a more efficient solution for managing timeouts in RAM memory. this resolves the problem of lingering timeouts in the server, providing faster removal of old timeouts and improving overall performance.
2023-12-20 10:31:09 +01:00
Wiktor Smaga
832102c9f4
Merge pull request #9 from Wenszel/dev
added cypress and handled end of the game
2023-12-19 11:42:53 +01:00
24 changed files with 1241 additions and 77 deletions

View File

@ -1,12 +1,13 @@
version: 2.1
orbs:
aws-cli: circleci/aws-cli@4.1.2
aws-ecr: circleci/aws-ecr@9.4.0
aws-ecs: circleci/aws-ecs@4.0.0
jobs:
build_and_test:
docker:
- image: circleci/node:14
working_directory: ~/app
steps:
- checkout
- run:
@ -26,9 +27,43 @@ jobs:
name: Test Frontend
command: |
npm test
run_task:
docker:
- image: cimg/python:3.10
steps:
- aws-cli/setup:
role_arn: arn:aws:iam::982081055194:role/circleci-ecs-ecr
role_session_name: example-session
- aws-ecs/run_task:
awsvpc: true
assign_public_ip: ENABLED
cluster: arn:aws:ecs:eu-north-1:982081055194:cluster/mern-ludo-cluster
launch_type: FARGATE
task_definition: mern-ludo-fargate
subnet_ids: subnet-06f63f5063b74a7c9,subnet-07cdbe8b01b46e0b7,subnet-06dbac60591c79f5d
security_group_ids: sg-0cfd2369802b8be43
workflows:
version: 2
build:
build_and_test_and_deploy:
jobs:
- build_and_test
- aws-ecr/build_and_push_image:
auth:
- aws-cli/setup:
role_arn: arn:aws:iam::982081055194:role/circleci-ecs-ecr
role_session_name: ecr-session
repo: mern-ludo
public_registry: true
requires:
- build_and_test
filters:
branches:
only:
- main
- run_task:
requires:
- aws-ecr/build_and_push_image
filters:
branches:
only:
- main

35
.dockerignore Normal file
View File

@ -0,0 +1,35 @@
# Include any files or directories that you don't want to be copied to your
# container here (e.g., local build artifacts, temporary files, etc.).
#
# For more help, visit the .dockerignore file reference guide at
# https://docs.docker.com/engine/reference/builder/#dockerignore-file
**/.classpath
**/.dockerignore
**/.env
**/.git
**/.gitignore
**/.project
**/.settings
**/.toolstarget
**/.vs
**/.vscode
**/.next
**/.cache
**/*.*proj.user
**/*.dbmdl
**/*.jfm
**/charts
**/docker-compose*
**/compose*
**/Dockerfile*
**/node_modules
**/npm-debug.log
**/obj
**/secrets.dev.yaml
**/values.dev.yaml
**/build
**/dist
LICENSE
README.md
node_modules

8
.env Normal file
View File

@ -0,0 +1,8 @@
# MongoDB connection for backend
CONNECTION_URI=mongodb://admin:adminpassword@mongo:27017/ludo?authSource=admin&replicaSet=rs0
# Backend port
PORT=18081
# Environment
NODE_ENV=production

3
.gitignore vendored
View File

@ -9,7 +9,7 @@ backend/node_modules
/coverage
# production
/build
build
# misc
.DS_Store
@ -21,3 +21,4 @@ backend/node_modules
npm-debug.log*
yarn-debug.log*
yarn-error.log*
# .env

111
Dockerfile Normal file
View File

@ -0,0 +1,111 @@
# FROM node:14 as frontend
# WORKDIR /app
# COPY . /app
# RUN npm install --production
# RUN npm run build
# FROM node:14 as backend
# WORKDIR /app
# COPY /backend /app
# RUN npm install
# FROM node:14
# WORKDIR /app
# COPY --from=backend /app /app/
# COPY --from=frontend /app/build /app/build
# EXPOSE 8080
# CMD ["npm", "run", "start"]
const express = require('express');
const cors = require('cors');
const path = require('path');
const cookieParser = require('cookie-parser');
const mongoose = require('mongoose');
require('dotenv').config();
const { sessionMiddleware } = require('./config/session');
const PORT = process.env.PORT || 5000;
const app = express();
app.use(cookieParser());
app.use(express.urlencoded({ extended: true }));
app.use(express.json());
app.set('trust proxy', 1);
/* ---------- CORS ---------- */
app.use(
cors({
origin:
process.env.NODE_ENV === 'production'
? true // same origin (Docker / prod)
: 'http://localhost:3000',
credentials: true,
})
);
app.use(sessionMiddleware);
const server = app.listen(PORT, "0.0.0.0", () => {
console.log(`Server listening on port ${PORT}`);
});
/* ---------- Mongo + Socket ---------- */
require('./config/database')(mongoose);
require('./config/socket')(server);
/* ---------- Serve React build ---------- */
if (process.env.NODE_ENV === 'production') {
const buildPath = path.resolve(__dirname, 'build');
app.use(express.static(buildPath));
app.get('*', (req, res) => {
res.sendFile(path.join(buildPath, 'index.html'));
});
}
module.exports = { server };
root@DietPi:~/mern-ludo/backend# ^C
root@DietPi:~/mern-ludo/backend# cd .
root@DietPi:~/mern-ludo/backend# cd ..
root@DietPi:~/mern-ludo# nano Dockerfile
root@DietPi:~/mern-ludo# cat Dockerfile
# Node 20 for Pi 5 (ARM64)
FROM node:20-bullseye-slim
WORKDIR /app
# Copy all files
COPY . /app
# Install frontend (root) dependencies
RUN npm install --legacy-peer-deps
# Build frontend
RUN npm run build
# Install backend dependencies
RUN cd backend && npm install --production --legacy-peer-deps
# Move frontend build into backend/public for Express
# RUN cp -r build backend/public
RUN rm -rf backend/build
RUN cp -r build backend/build
# Copy wait-for-mongo.sh and make it executable
COPY wait-for-mongo.sh ./
RUN chmod +x wait-for-mongo.sh
# Default fallback values (can be overridden by Compose)
ENV NODE_ENV=production
ENV PORT=8080
ENV CONNECTION_URI=mongodb://mongo:27017/ludo?replicaSet=rs0
EXPOSE 18081
# Start backend with wait-for-mongo
CMD ["bash", "./wait-for-mongo.sh", "mongo", "27017", "node", "backend/server.js"]

View File

@ -1,11 +1,10 @@
# <center>Online Multiplayer Ludo Game</center>
## About
<h1 align="center">Online Multiplayer Ludo Game </h1>
Ludo Online is a multiplayer web-based implementation of the classic board game Ludo, built using the MERN stack and integrated with SocketIO for real-time communication.
\>\> Play Online here <<
\>\> [Watch YouTube Video here](https://youtu.be/mGMnH9Nvsyw) <<
<p align="center">
>> <a href="https://youtu.be/mGMnH9Nvsyw">Watch YouTube Video here</a> <<
</p>
## Architecture
@ -14,34 +13,49 @@ Ludo Online is a multiplayer web-based implementation of the classic board game
## Tech Stack
Frontend:
![JavaScript](https://img.shields.io/badge/javascript-%23323330.svg?style=for-the-badge&logo=javascript&logoColor=%23F7DF1E) ![React](https://img.shields.io/badge/react-%2320232a.svg?style=for-the-badge&logo=react&logoColor=%2361DAFB) ![React Router](https://img.shields.io/badge/React_Router-CA4245?style=for-the-badge&logo=react-router&logoColor=white)
![CSS3](https://img.shields.io/badge/css3-%231572B6.svg?style=for-the-badge&logo=css3&logoColor=white) ![HTML5](https://img.shields.io/badge/html5-%23E34F26.svg?style=for-the-badge&logo=html5&logoColor=white) ![MUI](https://img.shields.io/badge/MUI-%230081CB.svg?style=for-the-badge&logo=mui&logoColor=white)
Backend:
![MongoDB](https://img.shields.io/badge/MongoDB-%234ea94b.svg?style=for-the-badge&logo=mongodb&logoColor=white) ![Express.js](https://img.shields.io/badge/express.js-%23404d59.svg?style=for-the-badge&logo=express&logoColor=%2361DAFB) ![Socket.io](https://img.shields.io/badge/Socket.io-black?style=for-the-badge&logo=socket.io&badgeColor=010101) ![NodeJS](https://img.shields.io/badge/node.js-6DA55F?style=for-the-badge&logo=node.js&logoColor=white)
Tests:
![cypress](https://img.shields.io/badge/-cypress-%23E5E5E5?style=for-the-badge&logo=cypress&logoColor=058a5e) ![Mocha](https://img.shields.io/badge/-mocha-%238D6748?style=for-the-badge&logo=mocha&logoColor=white) ![Jest](https://img.shields.io/badge/-jest-%23C21325?style=for-the-badge&logo=jest&logoColor=white)
Tools:
Other:
![Docker](https://img.shields.io/badge/docker-%230db7ed.svg?style=for-the-badge&logo=docker&logoColor=white) ![AWS](https://img.shields.io/badge/AWS-%23FF9900.svg?style=for-the-badge&logo=amazon-aws&logoColor=white) ![CircleCI](https://img.shields.io/badge/circle%20ci-%23161616.svg?style=for-the-badge&logo=circleci&logoColor=white) ![Git](https://img.shields.io/badge/git-%23F05033.svg?style=for-the-badge&logo=git&logoColor=white) ![Jira](https://img.shields.io/badge/jira-%230A0FFF.svg?style=for-the-badge&logo=jira&logoColor=white)
## Key Features and Challenges
- Maintained session consistency with **Express Session** and **MongoDB**.
- Enabled real-time communication via **WebSocket** and **SocketIO**.
- Maintained code reliability by implementing unit and integration tests using **Mocha**, **Chai**, and **Jest**.
- Hosted in a **Docker** container on **AWS EC2**.
- Established CI/CD using **CircleCI**.
- Implemented E2E tests utilizing **Cypress**, addressing challenges related to [testing collaboration](https://docs.cypress.io/guides/references/trade-offs#Multiple-browsers-open-at-the-same-time) and canvas functionality in the application.
- Established a CI/CD pipeline using **CircleCI**, with pushing **Docker** container to **AWS ECR** and deploying to **AWS ECS**
## Installation
1. Download this repository
2. Generate your own [mongoDB atlas](https://www.mongodb.com) credential URL. It should looks like this:
```
mongodb+srv://madmin:<password>@clustername.mongodb.net/<dbname>?retryWrites=true&w=majority
```
3. Add this URL to the /backend/credentials.js file
3. Add this URL to the /backend/.env file (refer to .env.example)
4. Perform these commands in the main directory:
```

3
backend/.env.example Normal file
View File

@ -0,0 +1,3 @@
PORT=8080
CONNECTION_URI=your_mongodb_connection_uri
NODE_ENV="development"

View File

@ -1,15 +1,9 @@
const CONNECTION_URI = require('../credentials.js');
module.exports = function (mongoose) {
mongoose.set('useFindAndModify', false);
mongoose
.connect(CONNECTION_URI, {
useNewUrlParser: true,
useUnifiedTopology: true,
dbName: 'test',
})
.then(() => {
console.log('MongoDB Connected…');
})
.catch(err => console.error(err));
module.exports = async function (mongoose) {
try {
await mongoose.connect(process.env.CONNECTION_URI);
console.log('✅ MongoDB connected');
} catch (err) {
console.error('❌ MongoDB connection error:', err);
process.exit(1);
}
};

View File

@ -1,8 +1,8 @@
const session = require('express-session');
const CONNECTION_URI = require('../credentials.js');
const MongoDBStore = require('connect-mongodb-session')(session);
const store = new MongoDBStore({
uri: CONNECTION_URI,
uri: process.env.CONNECTION_URI,
collection: 'sessions',
});
const sessionMiddleware = session({

View File

@ -1,2 +0,0 @@
// Write your own mongoDBatlas credentials here
module.exports = '';

View File

@ -1,5 +1,5 @@
const { getRoom, updateRoom } = require('../services/roomService');
const { colors } = require('../utils/constants');
const { COLORS } = require('../utils/constants');
module.exports = socket => {
const req = socket.request;
@ -44,7 +44,7 @@ module.exports = socket => {
if (err) return socket.disconnect();
req.session.roomId = room._id.toString();
req.session.playerId = room.players[room.players.length - 1]._id.toString();
req.session.color = colors[room.players.length - 1];
req.session.color = COLORS[room.players.length - 1];
req.session.save();
socket.join(room._id.toString());
socket.emit('player:data', JSON.stringify(req.session));

View File

@ -1,6 +1,7 @@
const mongoose = require('mongoose');
const { colors } = require('../utils/constants');
const { COLORS, MOVE_TIME } = require('../utils/constants');
const { makeRandomMove } = require('../handlers/handlersFunctions');
const timeoutManager = require('./timeoutManager.js');
const PawnSchema = require('./pawn');
const PlayerSchema = require('./player');
@ -12,7 +13,6 @@ const RoomSchema = new mongoose.Schema({
started: { type: Boolean, default: false },
full: { type: Boolean, default: false },
nextMoveTime: Number,
timeoutID: Number,
rolledNumber: Number,
players: [PlayerSchema],
winner: { type: String, default: null },
@ -24,10 +24,10 @@ const RoomSchema = new mongoose.Schema({
let pawn = {};
pawn.basePos = i;
pawn.position = i;
if (i < 4) pawn.color = colors[0];
else if (i < 8) pawn.color = colors[1];
else if (i < 12) pawn.color = colors[2];
else if (i < 16) pawn.color = colors[3];
if (i < 4) pawn.color = COLORS[0];
else if (i < 8) pawn.color = COLORS[1];
else if (i < 12) pawn.color = COLORS[2];
else if (i < 16) pawn.color = COLORS[3];
startPositions.push(pawn);
}
return startPositions;
@ -54,10 +54,10 @@ RoomSchema.methods.changeMovingPlayer = function () {
} else {
this.players[playerIndex + 1].nowMoving = true;
}
this.nextMoveTime = Date.now() + 15000;
this.nextMoveTime = Date.now() + MOVE_TIME;
this.rolledNumber = null;
if (this.timeoutID) clearTimeout(this.timeoutID);
this.timeoutID = setTimeout(makeRandomMove, 15000, this._id.toString());
timeoutManager.clear(this._id.toString());
timeoutManager.set(makeRandomMove, MOVE_TIME, this._id.toString());
};
RoomSchema.methods.movePawn = function (pawn) {
@ -83,14 +83,14 @@ RoomSchema.methods.canStartGame = function () {
RoomSchema.methods.startGame = function () {
this.started = true;
this.nextMoveTime = Date.now() + 15000;
this.nextMoveTime = Date.now() + MOVE_TIME;
this.players.forEach(player => (player.ready = true));
this.players[0].nowMoving = true;
this.timeoutID = setTimeout(makeRandomMove, 15000, this._id.toString());
timeoutManager.set(makeRandomMove, MOVE_TIME, this._id.toString());
};
RoomSchema.methods.endGame = function (winner) {
this.timeoutID = null;
timeoutManager.clear(this._id.toString());
this.rolledNumber = null;
this.nextMoveTime = null;
this.players.map(player => (player.nowMoving = false));
@ -131,7 +131,7 @@ RoomSchema.methods.addPlayer = function (name, id) {
sessionID: id,
name: name,
ready: false,
color: colors[this.players.length],
color: COLORS[this.players.length],
});
};

View File

@ -0,0 +1,19 @@
const timeoutManager = {
timeouts: new Map(),
add: function (roomId, timeoutId) {
this.timeouts.set(roomId, timeoutId);
},
get: function (roomId) {
return this.timeouts.get(roomId);
},
clear: function (roomId) {
clearTimeout(this.timeouts.get(roomId));
this.timeouts.delete(roomId);
},
set: function (timeoutFunction, time, roomId) {
const timeoutId = setTimeout(timeoutFunction, time, roomId);
this.add(roomId, timeoutId);
},
};
module.exports = timeoutManager;

View File

@ -10,9 +10,11 @@
"connect-mongodb-session": "^3.1.1",
"cookie-parser": "^1.4.5",
"cors": "^2.8.5",
"dotenv": "^16.3.1",
"express": "^4.17.1",
"express-session": "^1.17.1",
"mongoose": "^5.12.0",
"sinon": "^17.0.1",
"socket.io": "^4.5.1"
},
"devDependencies": {
@ -21,6 +23,45 @@
"socket.io-client": "^4.7.2"
}
},
"node_modules/@sinonjs/commons": {
"version": "3.0.0",
"resolved": "https://registry.npmjs.org/@sinonjs/commons/-/commons-3.0.0.tgz",
"integrity": "sha512-jXBtWAF4vmdNmZgD5FoKsVLv3rPgDnLgPbU84LIJ3otV44vJlDRokVng5v8NFJdCf/da9legHcKaRuZs4L7faA==",
"dependencies": {
"type-detect": "4.0.8"
}
},
"node_modules/@sinonjs/fake-timers": {
"version": "11.2.2",
"resolved": "https://registry.npmjs.org/@sinonjs/fake-timers/-/fake-timers-11.2.2.tgz",
"integrity": "sha512-G2piCSxQ7oWOxwGSAyFHfPIsyeJGXYtc6mFbnFA+kRXkiEnTl8c/8jul2S329iFBnDI9HGoeWWAZvuvOkZccgw==",
"dependencies": {
"@sinonjs/commons": "^3.0.0"
}
},
"node_modules/@sinonjs/samsam": {
"version": "8.0.0",
"resolved": "https://registry.npmjs.org/@sinonjs/samsam/-/samsam-8.0.0.tgz",
"integrity": "sha512-Bp8KUVlLp8ibJZrnvq2foVhP0IVX2CIprMJPK0vqGqgrDa0OHVKeZyBykqskkrdxV6yKBPmGasO8LVjAKR3Gew==",
"dependencies": {
"@sinonjs/commons": "^2.0.0",
"lodash.get": "^4.4.2",
"type-detect": "^4.0.8"
}
},
"node_modules/@sinonjs/samsam/node_modules/@sinonjs/commons": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/@sinonjs/commons/-/commons-2.0.0.tgz",
"integrity": "sha512-uLa0j859mMrg2slwQYdO/AkrOfmH+X6LTVmNTS9CqexuE2IvVORIkSpJLqePAbEnKJ77aMmCwr1NUZ57120Xcg==",
"dependencies": {
"type-detect": "4.0.8"
}
},
"node_modules/@sinonjs/text-encoding": {
"version": "0.7.2",
"resolved": "https://registry.npmjs.org/@sinonjs/text-encoding/-/text-encoding-0.7.2.tgz",
"integrity": "sha512-sXXKG+uL9IrKqViTtao2Ws6dy0znu9sOaP1di/jKGW1M6VssO8vlpXCQcpZ+jisQ1tTFAC5Jo/EOzFbggBagFQ=="
},
"node_modules/@socket.io/component-emitter": {
"version": "3.1.0",
"resolved": "https://registry.npmjs.org/@socket.io/component-emitter/-/component-emitter-3.1.0.tgz",
@ -668,6 +709,17 @@
"node": ">=0.3.1"
}
},
"node_modules/dotenv": {
"version": "16.3.1",
"resolved": "https://registry.npmjs.org/dotenv/-/dotenv-16.3.1.tgz",
"integrity": "sha512-IPzF4w4/Rd94bA9imS68tZBaYyBWSCE47V1RGuMrB94iyTOIEwRmVL2x/4An+6mETpLrKJ5hQkB8W4kFAadeIQ==",
"engines": {
"node": ">=12"
},
"funding": {
"url": "https://github.com/motdotla/dotenv?sponsor=1"
}
},
"node_modules/ee-first": {
"version": "1.1.1",
"resolved": "https://registry.npmjs.org/ee-first/-/ee-first-1.1.1.tgz",
@ -1081,7 +1133,6 @@
"version": "4.0.0",
"resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz",
"integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==",
"dev": true,
"engines": {
"node": ">=8"
}
@ -1257,6 +1308,11 @@
"js-yaml": "bin/js-yaml.js"
}
},
"node_modules/just-extend": {
"version": "4.2.1",
"resolved": "https://registry.npmjs.org/just-extend/-/just-extend-4.2.1.tgz",
"integrity": "sha512-g3UB796vUFIY90VIv/WX3L2c8CS2MdWUww3CNrYmqza1Fg0DURc2K/O4YrnklBdQarSJ/y8JnJYDGc+1iumQjg=="
},
"node_modules/kareem": {
"version": "2.3.2",
"resolved": "https://registry.npmjs.org/kareem/-/kareem-2.3.2.tgz",
@ -1293,6 +1349,11 @@
"resolved": "https://registry.npmjs.org/lodash.clonedeep/-/lodash.clonedeep-4.5.0.tgz",
"integrity": "sha1-4j8/nE+Pvd6HJSnBBxhXoIblzO8="
},
"node_modules/lodash.get": {
"version": "4.4.2",
"resolved": "https://registry.npmjs.org/lodash.get/-/lodash.get-4.4.2.tgz",
"integrity": "sha512-z+Uw/vLuy6gQe8cfaFWD7p0wVv8fJl3mbzXh33RS+0oW2wvUqiRXiQ69gLWSLpgB5/6sU+r6BlQR0MBILadqTQ=="
},
"node_modules/lodash.set": {
"version": "4.3.2",
"resolved": "https://registry.npmjs.org/lodash.set/-/lodash.set-4.3.2.tgz",
@ -1676,6 +1737,55 @@
"node": ">= 0.6"
}
},
"node_modules/nise": {
"version": "5.1.5",
"resolved": "https://registry.npmjs.org/nise/-/nise-5.1.5.tgz",
"integrity": "sha512-VJuPIfUFaXNRzETTQEEItTOP8Y171ijr+JLq42wHes3DiryR8vT+1TXQW/Rx8JNUhyYYWyIvjXTU6dOhJcs9Nw==",
"dependencies": {
"@sinonjs/commons": "^2.0.0",
"@sinonjs/fake-timers": "^10.0.2",
"@sinonjs/text-encoding": "^0.7.1",
"just-extend": "^4.0.2",
"path-to-regexp": "^1.7.0"
}
},
"node_modules/nise/node_modules/@sinonjs/commons": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/@sinonjs/commons/-/commons-2.0.0.tgz",
"integrity": "sha512-uLa0j859mMrg2slwQYdO/AkrOfmH+X6LTVmNTS9CqexuE2IvVORIkSpJLqePAbEnKJ77aMmCwr1NUZ57120Xcg==",
"dependencies": {
"type-detect": "4.0.8"
}
},
"node_modules/nise/node_modules/@sinonjs/fake-timers": {
"version": "10.3.0",
"resolved": "https://registry.npmjs.org/@sinonjs/fake-timers/-/fake-timers-10.3.0.tgz",
"integrity": "sha512-V4BG07kuYSUkTCSBHG8G8TNhM+F19jXFWnQtzj+we8DrkpSBCee9Z3Ms8yiGer/dlmhe35/Xdgyo3/0rQKg7YA==",
"dependencies": {
"@sinonjs/commons": "^3.0.0"
}
},
"node_modules/nise/node_modules/@sinonjs/fake-timers/node_modules/@sinonjs/commons": {
"version": "3.0.0",
"resolved": "https://registry.npmjs.org/@sinonjs/commons/-/commons-3.0.0.tgz",
"integrity": "sha512-jXBtWAF4vmdNmZgD5FoKsVLv3rPgDnLgPbU84LIJ3otV44vJlDRokVng5v8NFJdCf/da9legHcKaRuZs4L7faA==",
"dependencies": {
"type-detect": "4.0.8"
}
},
"node_modules/nise/node_modules/isarray": {
"version": "0.0.1",
"resolved": "https://registry.npmjs.org/isarray/-/isarray-0.0.1.tgz",
"integrity": "sha512-D2S+3GLxWH+uhrNEcoh/fnmYeP8E8/zHl644d/jdA0g2uyXvy3sb0qxotE+ne0LtccHknQzWwZEzhak7oJ0COQ=="
},
"node_modules/nise/node_modules/path-to-regexp": {
"version": "1.8.0",
"resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-1.8.0.tgz",
"integrity": "sha512-n43JRhlUKUAlibEJhPeir1ncUID16QnEjNpwzNdO3Lm4ywrBpBZ5oLD0I6br9evr1Y9JTqwRtAh7JLoOzAQdVA==",
"dependencies": {
"isarray": "0.0.1"
}
},
"node_modules/normalize-path": {
"version": "3.0.0",
"resolved": "https://registry.npmjs.org/normalize-path/-/normalize-path-3.0.0.tgz",
@ -2039,6 +2149,42 @@
"resolved": "https://registry.npmjs.org/sift/-/sift-13.5.2.tgz",
"integrity": "sha512-+gxdEOMA2J+AI+fVsCqeNn7Tgx3M9ZN9jdi95939l1IJ8cZsqS8sqpJyOkic2SJk+1+98Uwryt/gL6XDaV+UZA=="
},
"node_modules/sinon": {
"version": "17.0.1",
"resolved": "https://registry.npmjs.org/sinon/-/sinon-17.0.1.tgz",
"integrity": "sha512-wmwE19Lie0MLT+ZYNpDymasPHUKTaZHUH/pKEubRXIzySv9Atnlw+BUMGCzWgV7b7wO+Hw6f1TEOr0IUnmU8/g==",
"dependencies": {
"@sinonjs/commons": "^3.0.0",
"@sinonjs/fake-timers": "^11.2.2",
"@sinonjs/samsam": "^8.0.0",
"diff": "^5.1.0",
"nise": "^5.1.5",
"supports-color": "^7.2.0"
},
"funding": {
"type": "opencollective",
"url": "https://opencollective.com/sinon"
}
},
"node_modules/sinon/node_modules/diff": {
"version": "5.1.0",
"resolved": "https://registry.npmjs.org/diff/-/diff-5.1.0.tgz",
"integrity": "sha512-D+mk+qE8VC/PAUrlAU34N+VfXev0ghe5ywmpqrawphmVZc1bEfn56uo9qpyGp1p4xpzOHkSW4ztBd6L7Xx4ACw==",
"engines": {
"node": ">=0.3.1"
}
},
"node_modules/sinon/node_modules/supports-color": {
"version": "7.2.0",
"resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz",
"integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==",
"dependencies": {
"has-flag": "^4.0.0"
},
"engines": {
"node": ">=8"
}
},
"node_modules/sliced": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/sliced/-/sliced-1.0.1.tgz",
@ -2306,7 +2452,6 @@
"version": "4.0.8",
"resolved": "https://registry.npmjs.org/type-detect/-/type-detect-4.0.8.tgz",
"integrity": "sha512-0fr/mIH1dlO+x7TlcMy+bIDqKPsw/70tVyeHW787goQjhmqaZe10uwLujubK9q9Lg6Fiho1KUKDYz0Z7k7g5/g==",
"dev": true,
"engines": {
"node": ">=4"
}
@ -2506,6 +2651,47 @@
}
},
"dependencies": {
"@sinonjs/commons": {
"version": "3.0.0",
"resolved": "https://registry.npmjs.org/@sinonjs/commons/-/commons-3.0.0.tgz",
"integrity": "sha512-jXBtWAF4vmdNmZgD5FoKsVLv3rPgDnLgPbU84LIJ3otV44vJlDRokVng5v8NFJdCf/da9legHcKaRuZs4L7faA==",
"requires": {
"type-detect": "4.0.8"
}
},
"@sinonjs/fake-timers": {
"version": "11.2.2",
"resolved": "https://registry.npmjs.org/@sinonjs/fake-timers/-/fake-timers-11.2.2.tgz",
"integrity": "sha512-G2piCSxQ7oWOxwGSAyFHfPIsyeJGXYtc6mFbnFA+kRXkiEnTl8c/8jul2S329iFBnDI9HGoeWWAZvuvOkZccgw==",
"requires": {
"@sinonjs/commons": "^3.0.0"
}
},
"@sinonjs/samsam": {
"version": "8.0.0",
"resolved": "https://registry.npmjs.org/@sinonjs/samsam/-/samsam-8.0.0.tgz",
"integrity": "sha512-Bp8KUVlLp8ibJZrnvq2foVhP0IVX2CIprMJPK0vqGqgrDa0OHVKeZyBykqskkrdxV6yKBPmGasO8LVjAKR3Gew==",
"requires": {
"@sinonjs/commons": "^2.0.0",
"lodash.get": "^4.4.2",
"type-detect": "^4.0.8"
},
"dependencies": {
"@sinonjs/commons": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/@sinonjs/commons/-/commons-2.0.0.tgz",
"integrity": "sha512-uLa0j859mMrg2slwQYdO/AkrOfmH+X6LTVmNTS9CqexuE2IvVORIkSpJLqePAbEnKJ77aMmCwr1NUZ57120Xcg==",
"requires": {
"type-detect": "4.0.8"
}
}
}
},
"@sinonjs/text-encoding": {
"version": "0.7.2",
"resolved": "https://registry.npmjs.org/@sinonjs/text-encoding/-/text-encoding-0.7.2.tgz",
"integrity": "sha512-sXXKG+uL9IrKqViTtao2Ws6dy0znu9sOaP1di/jKGW1M6VssO8vlpXCQcpZ+jisQ1tTFAC5Jo/EOzFbggBagFQ=="
},
"@socket.io/component-emitter": {
"version": "3.1.0",
"resolved": "https://registry.npmjs.org/@socket.io/component-emitter/-/component-emitter-3.1.0.tgz",
@ -2983,6 +3169,11 @@
"integrity": "sha512-/VTCrvm5Z0JGty/BWHljh+BAiw3IK+2j87NGMu8Nwc/f48WoDAC395uomO9ZD117ZOBaHmkX1oyLvkVM/aIT3w==",
"dev": true
},
"dotenv": {
"version": "16.3.1",
"resolved": "https://registry.npmjs.org/dotenv/-/dotenv-16.3.1.tgz",
"integrity": "sha512-IPzF4w4/Rd94bA9imS68tZBaYyBWSCE47V1RGuMrB94iyTOIEwRmVL2x/4An+6mETpLrKJ5hQkB8W4kFAadeIQ=="
},
"ee-first": {
"version": "1.1.1",
"resolved": "https://registry.npmjs.org/ee-first/-/ee-first-1.1.1.tgz",
@ -3291,8 +3482,7 @@
"has-flag": {
"version": "4.0.0",
"resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz",
"integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==",
"dev": true
"integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ=="
},
"he": {
"version": "1.2.0",
@ -3412,6 +3602,11 @@
"argparse": "^2.0.1"
}
},
"just-extend": {
"version": "4.2.1",
"resolved": "https://registry.npmjs.org/just-extend/-/just-extend-4.2.1.tgz",
"integrity": "sha512-g3UB796vUFIY90VIv/WX3L2c8CS2MdWUww3CNrYmqza1Fg0DURc2K/O4YrnklBdQarSJ/y8JnJYDGc+1iumQjg=="
},
"kareem": {
"version": "2.3.2",
"resolved": "https://registry.npmjs.org/kareem/-/kareem-2.3.2.tgz",
@ -3439,6 +3634,11 @@
"resolved": "https://registry.npmjs.org/lodash.clonedeep/-/lodash.clonedeep-4.5.0.tgz",
"integrity": "sha1-4j8/nE+Pvd6HJSnBBxhXoIblzO8="
},
"lodash.get": {
"version": "4.4.2",
"resolved": "https://registry.npmjs.org/lodash.get/-/lodash.get-4.4.2.tgz",
"integrity": "sha512-z+Uw/vLuy6gQe8cfaFWD7p0wVv8fJl3mbzXh33RS+0oW2wvUqiRXiQ69gLWSLpgB5/6sU+r6BlQR0MBILadqTQ=="
},
"lodash.set": {
"version": "4.3.2",
"resolved": "https://registry.npmjs.org/lodash.set/-/lodash.set-4.3.2.tgz",
@ -3711,6 +3911,59 @@
"resolved": "https://registry.npmjs.org/negotiator/-/negotiator-0.6.2.tgz",
"integrity": "sha512-hZXc7K2e+PgeI1eDBe/10Ard4ekbfrrqG8Ep+8Jmf4JID2bNg7NvCPOZN+kfF574pFQI7mum2AUqDidoKqcTOw=="
},
"nise": {
"version": "5.1.5",
"resolved": "https://registry.npmjs.org/nise/-/nise-5.1.5.tgz",
"integrity": "sha512-VJuPIfUFaXNRzETTQEEItTOP8Y171ijr+JLq42wHes3DiryR8vT+1TXQW/Rx8JNUhyYYWyIvjXTU6dOhJcs9Nw==",
"requires": {
"@sinonjs/commons": "^2.0.0",
"@sinonjs/fake-timers": "^10.0.2",
"@sinonjs/text-encoding": "^0.7.1",
"just-extend": "^4.0.2",
"path-to-regexp": "^1.7.0"
},
"dependencies": {
"@sinonjs/commons": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/@sinonjs/commons/-/commons-2.0.0.tgz",
"integrity": "sha512-uLa0j859mMrg2slwQYdO/AkrOfmH+X6LTVmNTS9CqexuE2IvVORIkSpJLqePAbEnKJ77aMmCwr1NUZ57120Xcg==",
"requires": {
"type-detect": "4.0.8"
}
},
"@sinonjs/fake-timers": {
"version": "10.3.0",
"resolved": "https://registry.npmjs.org/@sinonjs/fake-timers/-/fake-timers-10.3.0.tgz",
"integrity": "sha512-V4BG07kuYSUkTCSBHG8G8TNhM+F19jXFWnQtzj+we8DrkpSBCee9Z3Ms8yiGer/dlmhe35/Xdgyo3/0rQKg7YA==",
"requires": {
"@sinonjs/commons": "^3.0.0"
},
"dependencies": {
"@sinonjs/commons": {
"version": "3.0.0",
"resolved": "https://registry.npmjs.org/@sinonjs/commons/-/commons-3.0.0.tgz",
"integrity": "sha512-jXBtWAF4vmdNmZgD5FoKsVLv3rPgDnLgPbU84LIJ3otV44vJlDRokVng5v8NFJdCf/da9legHcKaRuZs4L7faA==",
"requires": {
"type-detect": "4.0.8"
}
}
}
},
"isarray": {
"version": "0.0.1",
"resolved": "https://registry.npmjs.org/isarray/-/isarray-0.0.1.tgz",
"integrity": "sha512-D2S+3GLxWH+uhrNEcoh/fnmYeP8E8/zHl644d/jdA0g2uyXvy3sb0qxotE+ne0LtccHknQzWwZEzhak7oJ0COQ=="
},
"path-to-regexp": {
"version": "1.8.0",
"resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-1.8.0.tgz",
"integrity": "sha512-n43JRhlUKUAlibEJhPeir1ncUID16QnEjNpwzNdO3Lm4ywrBpBZ5oLD0I6br9evr1Y9JTqwRtAh7JLoOzAQdVA==",
"requires": {
"isarray": "0.0.1"
}
}
}
},
"normalize-path": {
"version": "3.0.0",
"resolved": "https://registry.npmjs.org/normalize-path/-/normalize-path-3.0.0.tgz",
@ -3989,6 +4242,34 @@
"resolved": "https://registry.npmjs.org/sift/-/sift-13.5.2.tgz",
"integrity": "sha512-+gxdEOMA2J+AI+fVsCqeNn7Tgx3M9ZN9jdi95939l1IJ8cZsqS8sqpJyOkic2SJk+1+98Uwryt/gL6XDaV+UZA=="
},
"sinon": {
"version": "17.0.1",
"resolved": "https://registry.npmjs.org/sinon/-/sinon-17.0.1.tgz",
"integrity": "sha512-wmwE19Lie0MLT+ZYNpDymasPHUKTaZHUH/pKEubRXIzySv9Atnlw+BUMGCzWgV7b7wO+Hw6f1TEOr0IUnmU8/g==",
"requires": {
"@sinonjs/commons": "^3.0.0",
"@sinonjs/fake-timers": "^11.2.2",
"@sinonjs/samsam": "^8.0.0",
"diff": "^5.1.0",
"nise": "^5.1.5",
"supports-color": "^7.2.0"
},
"dependencies": {
"diff": {
"version": "5.1.0",
"resolved": "https://registry.npmjs.org/diff/-/diff-5.1.0.tgz",
"integrity": "sha512-D+mk+qE8VC/PAUrlAU34N+VfXev0ghe5ywmpqrawphmVZc1bEfn56uo9qpyGp1p4xpzOHkSW4ztBd6L7Xx4ACw=="
},
"supports-color": {
"version": "7.2.0",
"resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz",
"integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==",
"requires": {
"has-flag": "^4.0.0"
}
}
}
},
"sliced": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/sliced/-/sliced-1.0.1.tgz",
@ -4187,8 +4468,7 @@
"type-detect": {
"version": "4.0.8",
"resolved": "https://registry.npmjs.org/type-detect/-/type-detect-4.0.8.tgz",
"integrity": "sha512-0fr/mIH1dlO+x7TlcMy+bIDqKPsw/70tVyeHW787goQjhmqaZe10uwLujubK9q9Lg6Fiho1KUKDYz0Z7k7g5/g==",
"dev": true
"integrity": "sha512-0fr/mIH1dlO+x7TlcMy+bIDqKPsw/70tVyeHW787goQjhmqaZe10uwLujubK9q9Lg6Fiho1KUKDYz0Z7k7g5/g=="
},
"type-is": {
"version": "1.6.18",

View File

@ -5,9 +5,11 @@
"connect-mongodb-session": "^3.1.1",
"cookie-parser": "^1.4.5",
"cors": "^2.8.5",
"dotenv": "^16.3.1",
"express": "^4.17.1",
"express-session": "^1.17.1",
"mongoose": "^5.12.0",
"sinon": "^17.0.1",
"socket.io": "^4.5.1"
},
"devDependencies": {

View File

@ -1,38 +1,49 @@
const express = require('express');
const cors = require('cors');
const path = require('path');
const cookieParser = require('cookie-parser');
const mongoose = require('mongoose');
require('dotenv').config();
const { sessionMiddleware } = require('./config/session');
const PORT = 8080;
const PORT = process.env.PORT || 5000;
const app = express();
app.use(cookieParser());
app.use(
express.urlencoded({
extended: true,
})
);
app.use(express.urlencoded({ extended: true }));
app.use(express.json());
app.set('trust proxy', 1);
/* ---------- CORS ---------- */
app.use(
cors({
origin: 'http://localhost:3000',
origin:
process.env.NODE_ENV === 'production'
? true // same origin (Docker / prod)
: 'http://localhost:3000',
credentials: true,
})
);
app.use(sessionMiddleware);
const server = app.listen(PORT);
const server = app.listen(PORT, "0.0.0.0", () => {
console.log(`Server listening on port ${PORT}`);
});
/* ---------- Mongo + Socket ---------- */
require('./config/database')(mongoose);
require('./config/socket')(server);
/* ---------- Serve React build ---------- */
if (process.env.NODE_ENV === 'production') {
app.use(express.static('/app/build'));
app.get('/', (req, res) => {
res.sendFile('/app/build/index.html');
const buildPath = path.resolve(__dirname, 'build');
app.use(express.static(buildPath));
app.get('*', (req, res) => {
res.sendFile(path.join(buildPath, 'index.html'));
});
}

View File

@ -0,0 +1,50 @@
const chai = require('chai');
const sinon = require('sinon');
const timeoutManager = require('../../models/timeoutManager');
const { expect } = chai;
describe('timeoutManager', () => {
beforeEach(() => {
timeoutManager.timeouts.clear();
sinon.useFakeTimers({ shouldClearNativeTimers: true });
});
afterEach(() => {
sinon.restore();
});
it('should add a timeout to the map', () => {
timeoutManager.add('room1', 1);
expect(timeoutManager.timeouts.size).to.equal(1);
});
it('should get a timeout from the map', () => {
timeoutManager.add('room1', 1);
const timeoutId = timeoutManager.get('room1');
expect(timeoutId).to.equal(1);
});
it('should clear a timeout from the map', () => {
timeoutManager.add('room1', 1);
timeoutManager.clear('room1');
expect(timeoutManager.timeouts.size).to.equal(0);
});
it('should set a new timeout', () => {
const timeoutFunction = sinon.spy();
timeoutManager.set(timeoutFunction, 100, 'room1');
expect(timeoutManager.timeouts.size).to.equal(1);
sinon.clock.tick(101);
sinon.assert.calledOnce(timeoutFunction);
});
it('should not call the timeout function if cleared', () => {
const timeoutFunction = sinon.spy();
timeoutManager.set(timeoutFunction, 100, 'room1');
timeoutManager.clear('room1');
sinon.clock.tick(101);
sinon.assert.notCalled(timeoutFunction);
expect(timeoutManager.timeouts.size).to.equal(0);
});
});

View File

@ -1,2 +1,3 @@
const colors = ["red", "blue", "green", "yellow"];
module.exports = { colors };
const COLORS = ['red', 'blue', 'green', 'yellow'];
const MOVE_TIME = 15000;
module.exports = { COLORS, MOVE_TIME };

42
docker-compose.yml Normal file
View File

@ -0,0 +1,42 @@
version: "3.9"
services:
mongo:
image: mongo:7.0
container_name: ludo-mongo
restart: unless-stopped
networks:
- ludo-net
volumes:
- ludo-mongo-data:/data/db
- ./mongo-keyfile:/etc/mongo-keyfile:ro
# - ./mongo-init.js:/docker-entrypoint-initdb.d/mongo-init.js:ro
command:
- bash
- -c
- |
cp /etc/mongo-keyfile /data/db/keyfile &&
chown mongodb:mongodb /data/db/keyfile &&
chmod 400 /data/db/keyfile &&
exec mongod --replSet rs0 --keyFile /data/db/keyfile --bind_ip_all
ports:
- "27017:27017"
app:
build: .
container_name: mern-ludo-app
restart: unless-stopped
networks:
- ludo-net
env_file:
- .env
ports:
- "18081:${PORT}" # Host:Container mapping
depends_on:
- mongo
networks:
ludo-net:
volumes:
ludo-mongo-data:

View File

@ -23,9 +23,7 @@
"scripts": {
"start": "react-scripts start",
"build": "react-scripts build",
"heroku-postbuild": "cd backend && npm install && cd .. && npm install && npm run build",
"test": "react-scripts test",
"eject": "react-scripts eject"
"test": "react-scripts test"
},
"eslintConfig": {
"extends": [
@ -48,11 +46,10 @@
"last 1 safari version"
]
},
"proxy": "http://localhost:5000",
"devDependencies": {
"@testing-library/jest-dom": "^6.1.5",
"@testing-library/react": "^14.1.2",
"cypress": "^13.6.1",
"@babel/plugin-proposal-private-property-in-object": "^7.16.7"
"@babel/plugin-transform-private-property-in-object": "^7.16.7"
}
}

View File

@ -13,7 +13,7 @@ function App() {
const [playerSocket, setPlayerSocket] = useState();
const [redirect, setRedirect] = useState();
useEffect(() => {
const socket = io('http://localhost:8080', { withCredentials: true });
const socket = io(`${window.location.protocol}//${window.location.host}`, { withCredentials: true });
socket.on('player:data', data => {
data = JSON.parse(data);
setPlayerData(data);
@ -72,5 +72,3 @@ function App() {
</SocketContext.Provider>
);
}
export default App;

View File

@ -1,7 +1,7 @@
.serversTableContainer {
display: flex;
height: 500px;
overflow: scroll;
height: 500px;
width: 100%;
}
.refresh {

View File

@ -1,6 +1,8 @@
.roomName {
max-width: 150px;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
text-align: left !important;
}
.rooms > thead > tr :nth-child(2) {
@ -30,6 +32,7 @@
.rooms {
border-collapse: collapse;
width: 100%;
height: fit-content;
}
.lastColumn {

562
troubleshooting.md Normal file
View File

@ -0,0 +1,562 @@
Youve identified the Mongo volume thats causing the auth issue.
What this means
local mern-ludo_mongo_data
This volume still exists, so Mongo is:
reusing old, broken initialization
not recreating the admin user
continuing to fail authentication
We now need to delete this volume so Mongo can initialize cleanly.
✅ Do this EXACTLY (safe + correct)
🔴 Step 1: Stop containers
docker compose down
🔴 Step 2: Remove the Mongo volume
docker volume rm mern-ludo_mongo_data
If Docker complains its in use, run:
docker compose down -v
(that removes volumes automatically)
✅ Step 3: Confirm its gone
docker volume ls | grep ludo
👉 This should return nothing.
✅ Step 4: Start ONLY Mongo first (important)
docker compose up -d mongo
Wait 1015 seconds.
Check logs:
docker logs ludo-mongo --tail=30
You should NOT see:
bad file
Unable to acquire security key
crash/restart loops
✅ Step 5: Test authentication (this MUST work)
docker exec -it ludo-mongo mongosh -u admin -p adminpassword --authenticationDatabase admin
If you get a > prompt → 🎉 auth is fixed
✅ Step 6: Initialize replica set (ONCE)
Inside mongosh:
rs.initiate({
_id: "rs0",
members: [{ _id: 0, host: "mongo:27017" }]
})
Then:
rs.status().members[0].stateStr
Must be:
"PRIMARY"
✅ Step 7: Start the app
docker compose up -d app
(or just)
docker compose up -d
Then:
docker restart mern-ludo-app
🎯 Expected final result
Mongo
Auth works
Replica set PRIMARY
No errors in logs
Backend
MongoDB connected
Server listening on port 18081
Browser
App loads
No crashes
No noscript / auth errors
🧾 One-line status
The Mongo volume still existed; deleting it allows Mongo to create the admin user correctly.
Once Step 5 succeeds, you are fully unblocked.
Run Step 2 now and tell me when mongosh login works 👍
Youre very close, and the remaining issue is not random.
What the two messages really mean (important)
1⃣ These messages are NOT the main problem ❌
Collection [local.oplog.rs] not found
ReadConcernMajorityNotAvailableYet
These appear when:
replica set is not initiated yet
PRIMARY is not elected yet
They are expected noise before rs.initiate() succeeds.
👉 Well fix them automatically once the replica set is initialized.
2⃣ THIS is the real blocker 🔴
MongoServerError: Authentication failed
This means 100%:
🔐 MongoDB is running with authentication enabled,
❌ but the admin user does not exist.
This is why:
backend cant connect
mongosh -u admin fails
oplog cant be created
replica set cant complete
Why the admin user does NOT exist (root cause)
Mongo creates the root user only once, on first successful startup, when:
/data/db is empty
Mongo reaches a stable running state
Earlier, Mongo:
crashed due to keyFile issues
restarted several times
skipped user creation
but auth stayed enabled
Result:
Mongo requires auth, but no users exist → permanent auth failure
This is a classic Mongo + replica set + keyFile edge case.
✅ The ONLY correct fix (clean & guaranteed)
We must let Mongo start once without auth, create the user manually, then re-enable auth.
This is the official MongoDB recovery method.
🔥 DO THIS STEP-BY-STEP (no skipping)
STEP 1⃣ Stop everything
docker compose down
STEP 2⃣ Temporarily DISABLE auth (Mongo only)
Edit docker-compose.yml mongo service:
🔴 TEMPORARY CHANGE
Comment out these lines for now:
# environment:
# MONGO_INITDB_ROOT_USERNAME: admin
# MONGO_INITDB_ROOT_PASSWORD: adminpassword
And change the command to NO keyFile, NO auth:
command:
- mongod
- --replSet
- rs0
- --bind_ip_all
(Leave volumes as-is.)
STEP 3⃣ Start Mongo only
docker compose up -d mongo
Wait ~10 seconds.
STEP 4⃣ Connect WITHOUT auth (this must work)
docker exec -it ludo-mongo mongosh
If you get a prompt → good.
STEP 5⃣ Create admin user MANUALLY
Inside mongosh:
use admin
db.createUser({
user: "admin",
pwd: "adminpassword",
roles: [ { role: "root", db: "admin" } ]
})
You must see:
{ ok: 1 }
STEP 6⃣ Initialize replica set (NOW it will work)
rs.initiate({
_id: "rs0",
members: [{ _id: 0, host: "mongo:27017" }]
})
Wait ~510 seconds, then:
rs.status().members[0].stateStr
Must be:
"PRIMARY"
Exit mongosh:
exit
STEP 7⃣ Re-enable auth + keyFile (FINAL config)
Restore your correct mongo config:
environment:
MONGO_INITDB_ROOT_USERNAME: admin
MONGO_INITDB_ROOT_PASSWORD: adminpassword
command:
- bash
- -c
- |
cp /tmp/mongo-keyfile /data/db/keyfile &&
chown mongodb:mongodb /data/db/keyfile &&
chmod 400 /data/db/keyfile &&
exec mongod --replSet rs0 --keyFile /data/db/keyfile --bind_ip_all
STEP 8⃣ Restart clean
docker compose down
docker compose up -d
STEP 9⃣ Test auth (this WILL work now)
docker exec -it ludo-mongo mongosh -u admin -p adminpassword --authenticationDatabase admin
STEP 🔟 Start / restart backend
docker restart mern-ludo-app
✅ What you should see now
Mongo
Auth works
PRIMARY elected
local.oplog.rs exists
No auth errors
Backend
MongoDB connected
Server listening on port 18081
App
Loads normally
No crashes
Change Streams work
🧠 One-line final diagnosis
MongoDB authentication was enabled before any user existed; manual user creation was required to recover.
This is the last MongoDB trap — once done, you will not hit this again.
MongoServerError[NotWritablePrimary]: not primary
Perfect — that error is expected at this exact moment, and it tells us precisely what to do next 👍
You didnt break anything.
What this error means (very clear)
MongoServerError[NotWritablePrimary]: not primary
This means:
MongoDB is running as a replica set member,
but no PRIMARY has been elected yet.
Until a node becomes PRIMARY:
❌ you cannot create users
❌ you cannot write data
❌ db.createUser() will fail
So this is not an auth issue anymore.
✅ Correct order (important)
When running Mongo as a replica set, the order must be:
Start Mongo
Initiate replica set
Wait until PRIMARY
Create users
Enable auth (already planned)
Right now, youre at step 2½.
✅ DO THIS NOW (inside mongosh)
You are already connected with:
docker exec -it ludo-mongo mongosh
At the > prompt, run THIS FIRST 👇
🔥 Step 1: Initiate replica set
rs.initiate({
_id: "rs0",
members: [
{ _id: 0, host: "mongo:27017" }
]
})
Expected output
{ ok: 1 }
🔥 Step 2: Wait a few seconds, then verify PRIMARY
rs.status().members[0].stateStr
You must see:
"PRIMARY"
If it still says STARTUP or SECONDARY, wait 5 seconds and run again.
✅ ONLY AFTER PRIMARY → create admin user
Now run:
use admin
db.createUser({
user: "admin",
pwd: "adminpassword",
roles: [ { role: "root", db: "admin" } ]
})
Expected:
{ ok: 1 }
🧠 Why this happened (important lesson)
You started Mongo with:
mongod --replSet rs0
That automatically puts Mongo into replica set mode, but:
replica set is inactive
no PRIMARY exists yet
Mongo rejects all writes until a PRIMARY is elected.
This is correct MongoDB behavior.
✅ After user creation (next steps preview)
Once { ok: 1 } appears:
Exit mongosh:
exit
Re-enable auth + keyFile in docker-compose (as discussed)
Restart containers
Verify:
docker exec -it ludo-mongo mongosh -u admin -p adminpassword --authenticationDatabase admin
🎯 One-line status update
MongoDB is in replica-set mode but not PRIMARY yet — initialize the replica set first.
👉 Action for you now
Run rs.initiate(...), wait for PRIMARY, then retry db.createUser.
Tell me what rs.status().members[0].stateStr prints, and well finish this cleanly 💪
```yaml
version: "3.9"
services:
mongo:
image: mongo:7.0
container_name: ludo-mongo
restart: unless-stopped
networks:
- ludo-net
environment:
MONGO_INITDB_ROOT_USERNAME: admin
MONGO_INITDB_ROOT_PASSWORD: adminpassword
volumes:
- ludo-mongo-data:/data/db
- ./mongo-keyfile:/tmp/mongo-keyfile:ro
command:
- bash
- -c
- |
cp /tmp/mongo-keyfile /data/db/keyfile &&
chown mongodb:mongodb /data/db/keyfile &&
chmod 400 /data/db/keyfile &&
exec mongod --replSet rs0 --keyFile /data/db/keyfile --bind_ip_all
ports:
- "27017:27017"
app:
build: .
container_name: mern-ludo-app
restart: unless-stopped
networks:
- ludo-net
env_file:
- .env
ports:
- "18081:8080"
depends_on:
- mongo
networks:
ludo-net:
volumes:
ludo-mongo-data:
```
Step 2⃣ Recreate keyfile cleanly (one last time)
cd ~/mern-ludo
rm -f mongo-keyfile
openssl rand -base64 756 > mongo-keyfile
chmod 400 mongo-keyfile
Verify:
wc -c mongo-keyfile
Step 3⃣ Full reset (MANDATORY)
docker compose down -v
docker compose up -d --build
Wait ~10 seconds.
Step 4⃣ Confirm Mongo is RUNNING (not restarting)
docker ps
You should NOT see Restarting.
Step 5⃣ Now exec WILL work
docker exec -it ludo-mongo mongosh -u admin -p adminpassword --authenticationDatabase admin
If you get a shell → 🎉 Mongo is fixed
Step 6⃣ Initialize replica set (ONCE)
rs.initiate({
_id: "rs0",
members: [{ _id: 0, host: "mongo:27017" }]
})
Check:
rs.status().members[0].stateStr
// PRIMARY
Step 7⃣ Restart app
docker restart mern-ludo-app
Your Change Stream error will now be gone permanently.
🧠 Why ALL previous attempts failed
Attempt Why it failed
Bind mount keyfile Ownership mismatch
chmod 400 only Not enough
DietPi ARM Stricter FS behavior
Mongo 7 Enforces keyfile rules hard
This copy + chown pattern solves all of them.
✅ One-line final diagnosis
MongoDB rejected the keyfile because bind-mounted files keep host ownership, and Mongo runs as a non-root user.