Search code examples
javascriptnode.jsdockerdiscord.js

Node.js require fails to find module only when running in docker


Edit: issue solved thank you @DavidMaze - commit of the fix here

While making an update to my discord bot i got a new issue, this issue does not happend when i run the bot locally but only when i run it in docker.

Node.js v19.7.0
node:internal/modules/cjs/loader:1093
throw err;
^

Error: Cannot find module '../database/birthdayCheck.js'
Require stack:

- /app/events/birthdayStatusChange.js
- /app/index.js
  at Module.\_resolveFilename (node:internal/modules/cjs/loader:1090:15)
  at Module.\_load (node:internal/modules/cjs/loader:934:27)
  at Module.require (node:internal/modules/cjs/loader:1157:19)
  at require (node:internal/modules/helpers:119:18)
  at Object.\<anonymous\> (/app/events/birthdayStatusChange.js:2:60)
  at Module.\_compile (node:internal/modules/cjs/loader:1275:14)
  at Module.\_extensions..js (node:internal/modules/cjs/loader:1329:10)
  at Module.load (node:internal/modules/cjs/loader:1133:32)
  at Module.\_load (node:internal/modules/cjs/loader:972:12)
  at Module.require (node:internal/modules/cjs/loader:1157:19) {
  code: 'MODULE_NOT_FOUND',
  requireStack: \[ '/app/events/birthdayStatusChange.js', '/app/index.js' \]
  }

docker and local versions of node and dependencies of node seem to be all the same, so this is really odd. I also tried running them from the same entry script.

It seems the issue starts at this merge Full code available here

I find it odd as there should be no change when running in the docker container.

Here is the code leading to loading the module:

firstly in index.js

// bot events handler
const eventsPath = path.join(__dirname, 'events');
const eventFiles = fs.readdirSync(eventsPath).filter(file => file.endsWith('.js'));

for (const file of eventFiles) {
    const filePath = path.join(eventsPath, file);
    const event = require(filePath);
    if (event.once) {
        bot.once(event.name, (...args) => event.execute(...args));
    } else {
        bot.on(event.name, (...args) => event.execute(...args));
    }
}

This loads the event in events/birthdayStatusChange.js which loads the module with the issue:

const { isBirthdayCheckedToday, setLastBirthdayChecked } = require('../database/birthdayCheck.js');

and the module that fails to load in docker can be found here

What am i missing?

I tried running from the same bash file docker is running in the entry point but i got the same results, no issue locally only on docker. I expected the bot to run without issue on docker.

edit - more info: Here is the Dockerfile:

FROM node:19.7.0

WORKDIR /app
COPY . .

RUN apt-get update && apt-get upgrade -y && \
    apt-get install -y --no-install-recommends \
    ffmpeg \
    build-essential && \
    npm install --production && \
    npm install sodium --production && \
    apt remove -y build-essential && \
    apt autoremove -y && \
    rm -rf /var/lib/apt/lists/*

ENV DISCORD_API_TOKEN=

ENTRYPOINT ["./docker.startup.sh"]

And the docker.startup.sh:

#!/bin/sh -e

cat > botinfo.js <<EOF
// token inserted via docker.startup.sh
exports.token = '${DISCORD_API_TOKEN}';
EOF

node index.js

And docker-compose.yml:

services:
  app:
    build:
      context:
        .
    restart: unless-stopped
    environment:
      - DISCORD_API_TOKEN=${DISCORD_API_KEY}
    volumes:
      - media:/app/sounds
      - database:/app/database

volumes:
  media:
  database:


Solution

  • In a comment you clarified:

    ...the sqlite database is saved to the database folder, and i mount it.

    That is, this volume mount

    volumes:
      - database:/app/database
    

    is hiding the same directory you're trying to import from the error message

    Error: Cannot find module '../database/birthdayCheck.js'
    

    The best solution here is to rearrange your application so that your code and data are stored in totally different places. In the commit you link to, you move this birthdayCheck.js file from the database directory to a utils directory. That leaves the database directory as purely a place for data, and there's no conflict with your application code.

    If you can tolerate losing your existing data it would probably work to remove the container and its volumes with docker-compose down -v. This would take advantage of a special case of Docker named volumes where, if you mount an empty volume over an image directory, the image contents are copied into the volume. However, this only happens the first time you use the volume – in your case, if you added the source file in the database directory after there was already data, the copy doesn't happen – and it doesn't happen on bind mounts or in other container environments like Kubernetes. This is technically an option, and it explains why this setup might have worked previously, but I'd avoid mounting anything at all over your image code.