Question: npm run does not work as expected on a docker container

I'm building a react app with vite and I'm deploying it with docker.
When deploying the container, npm run build runs but does nothing, even doing it inside the container manually won't work.
I get this output but the actual build does not happen:
$ npm run build

> frontend@0.0.0 build
> tsc && vite build

$

Thing is if I run tsc && vite build it works just fine.
Same goes with npm run dev or any other script.
Here's more info:

Tried with both Node Alpine and Debian (I'm actively developing in Alpine).
Tried creating the Node user and setting permissions accordingly since I read that npm might behave weird if ran by the root user.

$ npm config get ignore-scripts
false



These are my docker files
FROM node:20-bullseye
WORKDIR /app/frontend/
COPY package.json package-lock.json* /app/frontend/
RUN npm install
RUN npm install -g vite
RUN npm install -g typescript

COPY . .
RUN chown -R node:node /app/frontend/

USER node

CMD ["npm", "run", "build"]

docker-compose.yaml:
version: "3.4"
name: fisy-prod
services:
  backend:
    volumes:
      - static:/static
    env_file:
      - ./.env
    build:
      context: ./backend
      dockerfile: Dockerfile.prod
    ports:
      - "8000:8000"
  frontend:
    build:
      context: ./frontend
      dockerfile: Dockerfile.prod
    volumes:
      - frontend:/app/frontend/dist
  nginx:
    build:
      context: ./nginx
    volumes:
      - static:/static
      - frontend:/var/www/frontend
    ports:
      - "5173:80"
    depends_on:
      - backend
      - frontend
volumes:
  static:
  frontend:

I scouted the web all morning without really finding anything that might point to the issue. Would appreciate it if someone that has experienced this could help.


Answer:
Your Compose file has volumes: blocks that replace all of the image content with content from named volumes.  This apparently works the first time you run the container, since Docker copies content from the image into an empty volume; but if you rebuild your application, the volume will not get updated, and the old content in the volume will replace your image content.
One important part of this problem is that your frontend container doesn't really do anything.  Its CMD is to build the application, but that will immediately exit.  This doesn't need to be a long-running container and it doesn't need to be listed in your Compose file.
What I'd do here is to use a multi-stage build to compile the frontend, then COPY all of the content into an Nginx-based image.  For example:
FROM node:20-bullseye AS frontend
WORKDIR /app/frontend/
# Note, run with the project root as the build context
COPY frontend/package.json frontend/package-lock.json ./
...
RUN npm run build

FROM nginx:1.25
COPY nginx/default.conf.tmpl /etc/nginx/default.conf.tmpl
COPY backend/static/ /static/
COPY --from=frontend /app/frontend/dist/ /var/www/frontend/

Now the final image contains all of the pieces you need, so you don't need to try to share files between containers; that means you can remove all of the volumes:.  The final image build also encapsulates the front-end build, so you don't need a separate frontend build-only container.  This leaves you with a Compose file
version: "3.8"
services:
  backend:
    env_file:
      - ./.env
    build:
      context: ./backend
      dockerfile: Dockerfile.prod
    ports:
      - "8000:8000"
  nginx:
    build:
      context: .  # needed to COPY files from subdirectories
      dockerfile: nginx/Dockerfile
    ports:
      - "5173:80"
    depends_on:
      - backend