4
votes

When building an image that needs to be compiled from typescript, I get this error.

sh: 1: tsc: not found

The command '/bin/sh -c npm run tsc' returned a non-zero code: 127

Here is the relevant code:

docker-compose.yaml

version: '3.1'

services:
  nodeserver:
    build:
      context: .
      target: prod
    ports:
      - "3000:3000"
    volumes:
      - ./src:/app/src
      - ./public:/app/public
      - ./templates:/app/templates

Dockerfile

FROM node:15.11.0 AS base
EXPOSE 3000
ENV NODE_ENV=production
WORKDIR /app
COPY package*.json ./

RUN npm install --only=production && npm cache clean --force

##########################################################################################

FROM base AS dev

ENV NODE_ENV=development

RUN npm install --only=development

CMD npm run dev

##########################################################################################

FROM dev AS source

COPY dist dist
COPY templates templates
COPY public public

RUN npm run tsc

##########################################################################################

FROM base AS test

COPY --from=source /app/node_modules /app/node_modules
COPY --from=source /app/templates /app/templates
COPY --from=source /app/public /app/public
COPY --from=source /app/dist /app/dist

CMD npm run test

##########################################################################################

FROM test AS prod

CMD npm start

package.json

{
  "name": "nodeserver",
  "version": "1.0.0",
  "description": "",
  "main": "index.js",
  "scripts": {
    "start": "node ./dist/app.js",
    "deploy": "git add . && git commit -m Heroku && git push heroku main",
    "tsc": "tsc --outDir ./dist",
    "dev": "npm run ts-watch",
    "test": "npm run jest --runInBand",
    "ts-watch": "tsc-watch --project . --outDir ./dist --onSuccess \"nodemon ./dist/app.js\""
  },
  "jest": {
    "testEnvironment": "node"
  },
  "repository": {
    "type": "git",
    "url": "git+https://github.com/MiquelPiza/nodeserver.git"
  },
  "author": "",
  "license": "ISC",
  "bugs": {
    "url": "https://github.com/MiquelPiza/nodeserver/issues"
  },
  "homepage": "https://github.com/MiquelPiza/nodeserver#readme",
  "dependencies": {
    "@sendgrid/mail": "^7.4.2",
    "bcryptjs": "^2.4.3",
    "express": "^4.17.1",
    "handlebars": "^4.7.7",
    "jsonwebtoken": "^8.5.1",
    "lodash": "^4.17.20",
    "mongodb": "^3.6.4",
    "mongoose": "^5.11.19",
    "multer": "^1.4.2",
    "socket.io": "^4.0.0",
    "validator": "^13.5.2"
  },
  "devDependencies": {
    "@types/bcryptjs": "^2.4.2",
    "@types/express": "^4.17.11",
    "@types/jsonwebtoken": "^8.5.0",
    "@types/lodash": "^4.14.168",
    "@types/mongoose": "^5.10.3",
    "@types/multer": "^1.4.5",
    "@types/node": "^14.14.33",
    "@types/sendgrid": "^4.3.0",
    "@types/validator": "^13.1.3",
    "env-cmd": "^10.1.0",
    "jest": "^26.6.3",
    "nodemon": "^2.0.7",
    "supertest": "^6.1.3",
    "tsc-watch": "^4.2.9",
    "typescript": "^4.2.3"
  },
  "engines": {
    "node": "15.11.0"
  }
}

tsconfig.json

{
  "compilerOptions": {

    "target": "es5", 
    "module": "commonjs",
    "strict": true,
    "strictNullChecks": false,
    "esModuleInterop": true,
    "skipLibCheck": true,
    "forceConsistentCasingInFileNames": true,
  },
  "include": ["src"]
}

This dockerfile works:

FROM node:15.11.0 AS build

WORKDIR /app
COPY package.json .
RUN npm install
ADD . .
RUN npm run tsc


FROM node:15.11.0
WORKDIR /app

COPY package.json .
RUN npm install --production

ADD public ./public
ADD templates ./templates
COPY --from=build /app/dist dist

EXPOSE 3000
CMD npm start

I'm using this dockerfile for reference, from a Docker course: https://github.com/BretFisher/docker-mastery-for-nodejs/blob/master/typescript/Dockerfile I don't see what I'm doing wrong, the source stage should have the dev dependencies, among them typescript, so it should be able to run tsc.

Any help appreciated. Thanks.

EDIT:

In addition to using npm ci instead of npm install, I had to copy tsconfig.json to the working directory (and copy src directory instead of dist, which is created by tsc) for tsc to work properly. This is the modified source stage in the Dockerfile:

FROM dev AS source

COPY src src
COPY templates templates
COPY public public
COPY tsconfig.json tsconfig.json

RUN npm run tsc
1

1 Answers

2
votes

Use npm ci (or add package-lock.json to your .dockerignore file, or delete package-lock.json in your local environment before building). The why is answered here.

EDIT:

Here's what I believe is going on. Disclaimer, I'm not an expert on nodejs or npm -- in fact I'm something of a novice. And all of this is conjecture based on some experiments.

What's going wrong?

npm is not linking the binaries for the dev dependencies via sym links in node_modules/.bin because the package-lock.json file has gotten into a corrupted state where the (prod) dependencies are in lockfileVersion 2 format, and the dev dependencies are still in lockfileVersion 1 format.

Why is this happening?

Note: Making a bunch of assumptions here.

  1. Your local host using using npm 6, and the docker container is using npm 7. Because of this, the existing package-lock.json is in lockfileVersion: 1 which doesn't include a bin section for dependencies that have binaries. Version 2 does save the bin: section, which npm must use to determine what binaries to install/link.

  2. When you run the production dependency install (e.g. NODE_ENV=production npm install), using npm version 7, npm is upgrading the version of your package-lock.json to lockfileVersion: 2, part of this includes saving bin: sections for the dependencies that install binaries. Importantly, it updates only the production dependencies. Now the package-lock.json file is corrupted because it claims to be in version 2 format, but all the dev dependencies are either still in version 1 or at least don't have the bin: section correctly applied.

  3. When you now try to install your dev dependencies, npm sees that the package-lock.json is in lockfileVersion: 2, so it assumes that the dev dependencies have been upgraded as well (but they haven't been, or at least not correctly). It doesn't find the bin: sections because they don't exist and so it doesn't link the binaries to the node_modules/.bin/ directory.

You can perform a minimum reproduction of it using this Dockerfile:

FROM node:14 as npm6
WORKDIR /app
# Create a node project using npm 6 and install a dev dependency
# that contains a binary.
RUN npm init --yes && \
    npm install --save-dev typescript

FROM node:15 as npm7
COPY --from=npm6 /app/package*.json /app/
WORKDIR /app
# Install production dependencies, then all dependencies. This should
# link the binaries for typescript in (e.g. tsc) under node_modules/.bin.
RUN npm install -g [email protected] && \
    npm install --production && \
    npm install

# Causes error, tsc not found.
CMD ["npx", "-c", "tsc --version"]

I couldn't find an existing bug ticket so I created one here. Perhaps it will get fixed.