I’m working on a MERN monorepo using pnpm workspaces and trying to dockerize the entire setup using Docker Compose. The application runs perfectly without Docker but it breaks when I start docker compose up
. My project structure looks like this:
<code>mern-monorepo/
api/
client/
docker-compose.yml
package.json
pnpm-lock.yaml
pnpm-workspace.yaml
tsconfig.json
</code>
<code>mern-monorepo/
api/
client/
docker-compose.yml
package.json
pnpm-lock.yaml
pnpm-workspace.yaml
tsconfig.json
</code>
mern-monorepo/
api/
client/
docker-compose.yml
package.json
pnpm-lock.yaml
pnpm-workspace.yaml
tsconfig.json
This is my root tsconfig.json:
<code>{
"compilerOptions": {
/* Project Setup */
"target": "es2022",
"lib": ["dom", "esnext", "es2022"],
"allowJs": true,
"outDir": "./dist",
"sourceMap": true,
"removeComments": true,
"isolatedModules": true,
"downlevelIteration": true,
"skipLibCheck": true,
/* Module Resolution */
"module": "NodeNext",
"esModuleInterop": true,
"moduleResolution": "NodeNext",
"allowSyntheticDefaultImports": true,
"baseUrl": ".",
"paths": {
"@/*": ["api/src/*", "client/src/*"]
},
/* Linter Checks */
"strict": true,
"noImplicitThis": true,
"noUnusedLocals": true,
"noUnusedParameters": true,
"noImplicitReturns": true,
"noFallthroughCasesInSwitch": true,
/* Experimental Options */
"emitDecoratorMetadata": true,
"experimentalDecorators": true,
/* Advanced Options */
"forceConsistentCasingInFileNames": true
},
"types": ["vitest/globals"],
"include": [
"**/*.ts",
"**/*.tsx",
"api/vite.config.ts",
"client/vitest.config.ts",
"vitest.config.mts",
"vitest.workspace.mts"
],
"exclude": [
"node_modules",
"**/node_modules/*",
"**/dist/*",
"./client/cypress.config.ts"
]
}
</code>
<code>{
"compilerOptions": {
/* Project Setup */
"target": "es2022",
"lib": ["dom", "esnext", "es2022"],
"allowJs": true,
"outDir": "./dist",
"sourceMap": true,
"removeComments": true,
"isolatedModules": true,
"downlevelIteration": true,
"skipLibCheck": true,
/* Module Resolution */
"module": "NodeNext",
"esModuleInterop": true,
"moduleResolution": "NodeNext",
"allowSyntheticDefaultImports": true,
"baseUrl": ".",
"paths": {
"@/*": ["api/src/*", "client/src/*"]
},
/* Linter Checks */
"strict": true,
"noImplicitThis": true,
"noUnusedLocals": true,
"noUnusedParameters": true,
"noImplicitReturns": true,
"noFallthroughCasesInSwitch": true,
/* Experimental Options */
"emitDecoratorMetadata": true,
"experimentalDecorators": true,
/* Advanced Options */
"forceConsistentCasingInFileNames": true
},
"types": ["vitest/globals"],
"include": [
"**/*.ts",
"**/*.tsx",
"api/vite.config.ts",
"client/vitest.config.ts",
"vitest.config.mts",
"vitest.workspace.mts"
],
"exclude": [
"node_modules",
"**/node_modules/*",
"**/dist/*",
"./client/cypress.config.ts"
]
}
</code>
{
"compilerOptions": {
/* Project Setup */
"target": "es2022",
"lib": ["dom", "esnext", "es2022"],
"allowJs": true,
"outDir": "./dist",
"sourceMap": true,
"removeComments": true,
"isolatedModules": true,
"downlevelIteration": true,
"skipLibCheck": true,
/* Module Resolution */
"module": "NodeNext",
"esModuleInterop": true,
"moduleResolution": "NodeNext",
"allowSyntheticDefaultImports": true,
"baseUrl": ".",
"paths": {
"@/*": ["api/src/*", "client/src/*"]
},
/* Linter Checks */
"strict": true,
"noImplicitThis": true,
"noUnusedLocals": true,
"noUnusedParameters": true,
"noImplicitReturns": true,
"noFallthroughCasesInSwitch": true,
/* Experimental Options */
"emitDecoratorMetadata": true,
"experimentalDecorators": true,
/* Advanced Options */
"forceConsistentCasingInFileNames": true
},
"types": ["vitest/globals"],
"include": [
"**/*.ts",
"**/*.tsx",
"api/vite.config.ts",
"client/vitest.config.ts",
"vitest.config.mts",
"vitest.workspace.mts"
],
"exclude": [
"node_modules",
"**/node_modules/*",
"**/dist/*",
"./client/cypress.config.ts"
]
}
API package.json:
<code>{
"name": "@mern-monorepo/api",
"version": "1.0.0",
"description": "Node and Express server API",
"main": "index.js",
"license": "MIT",
"scripts": {
"start:dev": "pnpm tsx watch src/server.ts",
"start": "pnpm build && node dist/server.js",
"build": "tsc && tsc-alias",
"deploy:clean": "pm2 stop all && pm2 delete all",
"deploy:dev": "pm2 start ecosystem.config.js --only dev && pm2 logs all",
"deploy:prod": "pnpm build && pm2 start ecosystem.config.js --only prod",
"test": "dotenv -e .env.development.test -- npx vitest"
},
"dependencies": {
"bcrypt": "^5.1.0",
"class-transformer": "^0.5.1",
"class-validator": "^0.14.0",
"compression": "^1.7.4",
"cookie-parser": "^1.4.6",
"cookie-session": "^2.0.0",
"cors": "^2.8.5",
"dotenv": "^16.1.4",
"express": "^4.18.2",
"helmet": "^7.0.0",
"hpp": "^0.2.3",
"http-status-codes": "^2.2.0",
"ioredis": "^5.3.2",
"jsonwebtoken": "^9.0.0",
"mongoose": "^8.4.4",
"winston": "^3.9.0"
},
"devDependencies": {
"@types/bcrypt": "^5.0.0",
"@types/compression": "^1.7.2",
"@types/cookie-parser": "^1.4.3",
"@types/cookie-session": "^2.0.44",
"@types/cors": "^2.8.13",
"@types/express": "^4.17.17",
"@types/hpp": "^0.2.2",
"@types/jsonwebtoken": "^9.0.2",
"@types/node": "^20.2.5",
"@types/supertest": "^6.0.2",
"@types/swagger-jsdoc": "^6.0.1",
"@types/swagger-ui-express": "^4.1.3",
"dotenv-cli": "^7.2.1",
"nodemon": "^3.1.4",
"pm2": "^5.3.0",
"supertest": "^7.0.0",
"swagger-jsdoc": "^6.2.8",
"swagger-ui-express": "^5.0.1",
"tinyspy": "^3.0.0",
"ts-node": "^10.9.1",
"tsc-alias": "^1.8.6",
"tsconfig-paths": "^4.2.0",
"tsx": "^4.16.0",
"typescript-transform-paths": "^3.4.7"
},
"config": {
"commitizen": {
"path": "cz-conventional-changelog"
}
},
"engines": {
"node": ">=22.0.0",
"pnpm": ">=9.4.0"
},
"packageManager": "[email protected]+sha512.f549b8a52c9d2b8536762f99c0722205efc5af913e77835dbccc3b0b0b2ca9e7dc8022b78062c17291c48e88749c70ce88eb5a74f1fa8c4bf5e18bb46c8bd83a"
}
</code>
<code>{
"name": "@mern-monorepo/api",
"version": "1.0.0",
"description": "Node and Express server API",
"main": "index.js",
"license": "MIT",
"scripts": {
"start:dev": "pnpm tsx watch src/server.ts",
"start": "pnpm build && node dist/server.js",
"build": "tsc && tsc-alias",
"deploy:clean": "pm2 stop all && pm2 delete all",
"deploy:dev": "pm2 start ecosystem.config.js --only dev && pm2 logs all",
"deploy:prod": "pnpm build && pm2 start ecosystem.config.js --only prod",
"test": "dotenv -e .env.development.test -- npx vitest"
},
"dependencies": {
"bcrypt": "^5.1.0",
"class-transformer": "^0.5.1",
"class-validator": "^0.14.0",
"compression": "^1.7.4",
"cookie-parser": "^1.4.6",
"cookie-session": "^2.0.0",
"cors": "^2.8.5",
"dotenv": "^16.1.4",
"express": "^4.18.2",
"helmet": "^7.0.0",
"hpp": "^0.2.3",
"http-status-codes": "^2.2.0",
"ioredis": "^5.3.2",
"jsonwebtoken": "^9.0.0",
"mongoose": "^8.4.4",
"winston": "^3.9.0"
},
"devDependencies": {
"@types/bcrypt": "^5.0.0",
"@types/compression": "^1.7.2",
"@types/cookie-parser": "^1.4.3",
"@types/cookie-session": "^2.0.44",
"@types/cors": "^2.8.13",
"@types/express": "^4.17.17",
"@types/hpp": "^0.2.2",
"@types/jsonwebtoken": "^9.0.2",
"@types/node": "^20.2.5",
"@types/supertest": "^6.0.2",
"@types/swagger-jsdoc": "^6.0.1",
"@types/swagger-ui-express": "^4.1.3",
"dotenv-cli": "^7.2.1",
"nodemon": "^3.1.4",
"pm2": "^5.3.0",
"supertest": "^7.0.0",
"swagger-jsdoc": "^6.2.8",
"swagger-ui-express": "^5.0.1",
"tinyspy": "^3.0.0",
"ts-node": "^10.9.1",
"tsc-alias": "^1.8.6",
"tsconfig-paths": "^4.2.0",
"tsx": "^4.16.0",
"typescript-transform-paths": "^3.4.7"
},
"config": {
"commitizen": {
"path": "cz-conventional-changelog"
}
},
"engines": {
"node": ">=22.0.0",
"pnpm": ">=9.4.0"
},
"packageManager": "[email protected]+sha512.f549b8a52c9d2b8536762f99c0722205efc5af913e77835dbccc3b0b0b2ca9e7dc8022b78062c17291c48e88749c70ce88eb5a74f1fa8c4bf5e18bb46c8bd83a"
}
</code>
{
"name": "@mern-monorepo/api",
"version": "1.0.0",
"description": "Node and Express server API",
"main": "index.js",
"license": "MIT",
"scripts": {
"start:dev": "pnpm tsx watch src/server.ts",
"start": "pnpm build && node dist/server.js",
"build": "tsc && tsc-alias",
"deploy:clean": "pm2 stop all && pm2 delete all",
"deploy:dev": "pm2 start ecosystem.config.js --only dev && pm2 logs all",
"deploy:prod": "pnpm build && pm2 start ecosystem.config.js --only prod",
"test": "dotenv -e .env.development.test -- npx vitest"
},
"dependencies": {
"bcrypt": "^5.1.0",
"class-transformer": "^0.5.1",
"class-validator": "^0.14.0",
"compression": "^1.7.4",
"cookie-parser": "^1.4.6",
"cookie-session": "^2.0.0",
"cors": "^2.8.5",
"dotenv": "^16.1.4",
"express": "^4.18.2",
"helmet": "^7.0.0",
"hpp": "^0.2.3",
"http-status-codes": "^2.2.0",
"ioredis": "^5.3.2",
"jsonwebtoken": "^9.0.0",
"mongoose": "^8.4.4",
"winston": "^3.9.0"
},
"devDependencies": {
"@types/bcrypt": "^5.0.0",
"@types/compression": "^1.7.2",
"@types/cookie-parser": "^1.4.3",
"@types/cookie-session": "^2.0.44",
"@types/cors": "^2.8.13",
"@types/express": "^4.17.17",
"@types/hpp": "^0.2.2",
"@types/jsonwebtoken": "^9.0.2",
"@types/node": "^20.2.5",
"@types/supertest": "^6.0.2",
"@types/swagger-jsdoc": "^6.0.1",
"@types/swagger-ui-express": "^4.1.3",
"dotenv-cli": "^7.2.1",
"nodemon": "^3.1.4",
"pm2": "^5.3.0",
"supertest": "^7.0.0",
"swagger-jsdoc": "^6.2.8",
"swagger-ui-express": "^5.0.1",
"tinyspy": "^3.0.0",
"ts-node": "^10.9.1",
"tsc-alias": "^1.8.6",
"tsconfig-paths": "^4.2.0",
"tsx": "^4.16.0",
"typescript-transform-paths": "^3.4.7"
},
"config": {
"commitizen": {
"path": "cz-conventional-changelog"
}
},
"engines": {
"node": ">=22.0.0",
"pnpm": ">=9.4.0"
},
"packageManager": "[email protected]+sha512.f549b8a52c9d2b8536762f99c0722205efc5af913e77835dbccc3b0b0b2ca9e7dc8022b78062c17291c48e88749c70ce88eb5a74f1fa8c4bf5e18bb46c8bd83a"
}
My api Dockerfile:
<code>FROM node:alpine
RUN apk update && apk add --no-cache nodejs && corepack enable
WORKDIR /api
COPY package.json tsconfig.json ./
COPY src ./src
RUN ls -a
RUN pnpm install
EXPOSE 4000
CMD [ "pnpm", "start:dev" ]
</code>
<code>FROM node:alpine
RUN apk update && apk add --no-cache nodejs && corepack enable
WORKDIR /api
COPY package.json tsconfig.json ./
COPY src ./src
RUN ls -a
RUN pnpm install
EXPOSE 4000
CMD [ "pnpm", "start:dev" ]
</code>
FROM node:alpine
RUN apk update && apk add --no-cache nodejs && corepack enable
WORKDIR /api
COPY package.json tsconfig.json ./
COPY src ./src
RUN ls -a
RUN pnpm install
EXPOSE 4000
CMD [ "pnpm", "start:dev" ]
API Docker Compose File:
<code>services:
api:
container_name: api
image: api
build:
context: .
dockerfile: Dockerfile.dev
restart: always
ports:
- 4000:4000
- 5173:5173
environment:
- NODE_ENV=development
- BASE_URL=http://localhost:4000
volumes:
- .:/api
- node_modules:/api/node_modules
</code>
<code>services:
api:
container_name: api
image: api
build:
context: .
dockerfile: Dockerfile.dev
restart: always
ports:
- 4000:4000
- 5173:5173
environment:
- NODE_ENV=development
- BASE_URL=http://localhost:4000
volumes:
- .:/api
- node_modules:/api/node_modules
</code>
services:
api:
container_name: api
image: api
build:
context: .
dockerfile: Dockerfile.dev
restart: always
ports:
- 4000:4000
- 5173:5173
environment:
- NODE_ENV=development
- BASE_URL=http://localhost:4000
volumes:
- .:/api
- node_modules:/api/node_modules
Root Docker Compose File:
<code>services:
api:
extends:
service: api
file: api/docker-compose.yaml
volumes:
node_modules:
networks:
mern:
name: mern
</code>
<code>services:
api:
extends:
service: api
file: api/docker-compose.yaml
volumes:
node_modules:
networks:
mern:
name: mern
</code>
services:
api:
extends:
service: api
file: api/docker-compose.yaml
volumes:
node_modules:
networks:
mern:
name: mern
The error I get in console when I run docker compose up:
<code>api | > @mern-monorepo/[email protected] start:dev /api
api | > pnpm tsx watch src/server.ts
api |
api | node:internal/modules/cjs/loader:1219
api | const err = new Error(message);
api | ^
api |
api | Error: Cannot find module '@/libs/shared/middlewares/error.middleware'
api | Require stack:
api | - /api/src/app/index.ts
api | - /api/src/server.ts
api | at Module._resolveFilename (node:internal/modules/cjs/loader:1219:15)
api | at resolve (/api/node_modules/.pnpm/[email protected]/node_modules/tsx/dist/register-CCR7NebB.cjs:2:554)
api | at resolveRequest (/api/node_modules/.pnpm/[email protected]/node_modules/tsx/dist/register-CCR7NebB.cjs:1:2758)
api | at /api/node_modules/.pnpm/[email protected]/node_modules/tsx/dist/register-CCR7NebB.cjs:2:583
api | at m._resolveFilename (file:///api/node_modules/.pnpm/[email protected]/node_modules/tsx/dist/register-Cehy_bZS.mjs:1:789)
api | at Module._load (node:internal/modules/cjs/loader:1045:27)
api | at TracingChannel.traceSync (node:diagnostics_channel:315:14)
api | at wrapModuleLoad (node:internal/modules/cjs/loader:215:24)
api | at Module.require (node:internal/modules/cjs/loader:1304:12)
api | at require (node:internal/modules/helpers:123:16) {
api | code: 'MODULE_NOT_FOUND',
api | requireStack: [ '/api/src/app/index.ts', '/api/src/server.ts' ]
api | }
api |
api | Node.js v22.4.0
</code>
<code>api | > @mern-monorepo/[email protected] start:dev /api
api | > pnpm tsx watch src/server.ts
api |
api | node:internal/modules/cjs/loader:1219
api | const err = new Error(message);
api | ^
api |
api | Error: Cannot find module '@/libs/shared/middlewares/error.middleware'
api | Require stack:
api | - /api/src/app/index.ts
api | - /api/src/server.ts
api | at Module._resolveFilename (node:internal/modules/cjs/loader:1219:15)
api | at resolve (/api/node_modules/.pnpm/[email protected]/node_modules/tsx/dist/register-CCR7NebB.cjs:2:554)
api | at resolveRequest (/api/node_modules/.pnpm/[email protected]/node_modules/tsx/dist/register-CCR7NebB.cjs:1:2758)
api | at /api/node_modules/.pnpm/[email protected]/node_modules/tsx/dist/register-CCR7NebB.cjs:2:583
api | at m._resolveFilename (file:///api/node_modules/.pnpm/[email protected]/node_modules/tsx/dist/register-Cehy_bZS.mjs:1:789)
api | at Module._load (node:internal/modules/cjs/loader:1045:27)
api | at TracingChannel.traceSync (node:diagnostics_channel:315:14)
api | at wrapModuleLoad (node:internal/modules/cjs/loader:215:24)
api | at Module.require (node:internal/modules/cjs/loader:1304:12)
api | at require (node:internal/modules/helpers:123:16) {
api | code: 'MODULE_NOT_FOUND',
api | requireStack: [ '/api/src/app/index.ts', '/api/src/server.ts' ]
api | }
api |
api | Node.js v22.4.0
</code>
api | > @mern-monorepo/[email protected] start:dev /api
api | > pnpm tsx watch src/server.ts
api |
api | node:internal/modules/cjs/loader:1219
api | const err = new Error(message);
api | ^
api |
api | Error: Cannot find module '@/libs/shared/middlewares/error.middleware'
api | Require stack:
api | - /api/src/app/index.ts
api | - /api/src/server.ts
api | at Module._resolveFilename (node:internal/modules/cjs/loader:1219:15)
api | at resolve (/api/node_modules/.pnpm/[email protected]/node_modules/tsx/dist/register-CCR7NebB.cjs:2:554)
api | at resolveRequest (/api/node_modules/.pnpm/[email protected]/node_modules/tsx/dist/register-CCR7NebB.cjs:1:2758)
api | at /api/node_modules/.pnpm/[email protected]/node_modules/tsx/dist/register-CCR7NebB.cjs:2:583
api | at m._resolveFilename (file:///api/node_modules/.pnpm/[email protected]/node_modules/tsx/dist/register-Cehy_bZS.mjs:1:789)
api | at Module._load (node:internal/modules/cjs/loader:1045:27)
api | at TracingChannel.traceSync (node:diagnostics_channel:315:14)
api | at wrapModuleLoad (node:internal/modules/cjs/loader:215:24)
api | at Module.require (node:internal/modules/cjs/loader:1304:12)
api | at require (node:internal/modules/helpers:123:16) {
api | code: 'MODULE_NOT_FOUND',
api | requireStack: [ '/api/src/app/index.ts', '/api/src/server.ts' ]
api | }
api |
api | Node.js v22.4.0