Fix: sh: tsc: not found in Docker build
Two different causes produce the identical "tsc: not found" — here is how to tell them apart, and the multi-stage fix for both.
TL;DR#
sh: 1: tsc: not found (exit code 127) inside a Docker build means the
tsc binary isn't in node_modules/.bin in that stage — either because
typescript (a devDependency) was skipped by a production-only install, or,
less obviously, because it was installed but a two-step install corrupted
package-lock.json badly enough that npm never linked its binary. Give the
stage that compiles TypeScript full devDependencies in a single npm ci
call, then copy just the compiled dist/ output into your slim runtime
stage.
The error#
A Dockerfile that compiles TypeScript before starting the app fails partway through the build:
sh: 1: tsc: not found
The command '/bin/sh -c npm run tsc' returned a non-zero code: 127This exact failure is a common Stack Overflow question
— the same npm run tsc script that works fine on a developer's machine
fails only inside the container, where node_modules/.bin/tsc simply isn't
there to run. In the original report, the Dockerfile did try to install dev
dependencies before compiling — a dev stage explicitly runs
npm install --only=development — so this isn't always as simple as "dev
dependencies were never installed." Two different, independent causes
produce the identical symptom, and telling them apart determines which fix
actually applies.
Why it happens#
typescript is almost always declared under devDependencies, since it's a
build-time tool, not something the running app imports. There are two
distinct, independent ways it can end up missing.
Cause 1: dev dependencies genuinely never installed#
The most common version: a Dockerfile sets ENV NODE_ENV=production before
npm install, or passes an older --only=production-style flag (or its
modern replacement) — and every dev dependency, typescript included, is
skipped outright. One answer on the original Stack Overflow thread names
this directly: "It's probably a NODE_ENV environment variable problem...
If you set this way, the dependencies in devDependencies will not be
installed." If that ENV line appears anywhere earlier in the Dockerfile —
even in a base stage a later stage builds FROM — every plain npm install
after it inherits the same behaviour, and node_modules/.bin/tsc — the
symlink npm creates for packages that expose a binary — is simply never
written, because the package that owns it was never fetched at all.
Cause 2: dev dependencies installed, but their binaries never linked#
The accepted answer on the same question describes a subtler failure that
happened even though typescript was being installed: the Dockerfile
installs production dependencies first, in one npm install call, and dev
dependencies afterwards, in a second, separate npm install --only=development
call against the same package-lock.json. If the local machine that
generated the lockfile used an older npm than the one running inside the
image, that first production-only install can silently upgrade the
lockfile's format for the production section while leaving the dev section
in the old format. npm then reads a lockfile that claims to be one version
but is only half-migrated, and fails to generate the bin symlink for the
newly installed dev packages — typescript shows up under
node_modules/typescript, yet node_modules/.bin/tsc still doesn't exist.
Running npm ci instead of two separate npm install calls avoids this
class of corruption entirely, since npm ci always installs the exact,
single-version tree recorded in the lockfile in one pass rather than
mutating it across two calls.
Fix#
1. Give the build stage devDependencies, then discard them#
The standard pattern is a multi-stage Dockerfile: one stage installs everything and compiles, a second, smaller stage copies out only what's needed to run.
# ---- build stage: has devDependencies, compiles TypeScript ----
FROM node:22-slim AS build
WORKDIR /app
COPY package*.json ./
RUN npm ci
COPY . .
RUN npm run build
# ---- runtime stage: production deps only, no compiler ----
FROM node:22-slim AS runtime
WORKDIR /app
ENV NODE_ENV=production
COPY package*.json ./
RUN npm ci --omit=dev
COPY --from=build /app/dist ./dist
CMD ["node", "dist/app.js"]The build stage never sets NODE_ENV=production, so npm ci installs
typescript and tsc runs normally. The runtime stage installs with dev
dependencies stripped out and never calls tsc at all — it just runs the
already-compiled JavaScript that was copied over.
2. If you can't restructure the Dockerfile yet, force dev dependencies back in#
As a smaller, temporary fix on a single-stage Dockerfile, override the
production-only install for just the step that compiles, with
--include=dev. A plain npm install -D typescript isn't reliable here —
if NODE_ENV is set to production anywhere earlier in the stage, npm's
omit setting still defaults to skipping dev dependencies regardless of
which package you name, whereas --include=dev explicitly overrides that
default and reinstalls the full tree, dev dependencies included:
RUN npm ci --include=dev
RUN npx tsc
RUN npm ci --omit=devThis unblocks the build without a rewrite, but it means the compiler and its
transitive dependencies stay in your production node_modules unless you
clean them up afterwards — the multi-stage approach above avoids that
trade-off entirely, keeps the final image smaller, and is worth migrating to
once the immediate build failure is unblocked.
3. Use npm ci, not npm install, in every stage#
Regardless of the devDependencies issue, prefer npm ci over npm install
in Docker. npm ci requires a package-lock.json, deletes any existing
node_modules first, and installs the exact versions the lockfile records —
which avoids the lockfile-format mismatch that can also leave binaries like
tsc unlinked even when the package itself is present.
Verify the fix#
Rebuild without cache to rule out a stale layer:
docker build --no-cache -t myapp .The build stage's npm run build (or npx tsc) step should now print
compiled output instead of sh: 1: tsc: not found. Confirm the final image
is still slim by checking it doesn't carry the compiler:
docker run --rm myapp sh -c "ls node_modules/.bin/tsc 2>&1 || echo 'tsc not in runtime image — correct'"If you followed the multi-stage pattern, that should print
tsc not in runtime image — correct. Also confirm the compiled output
actually runs, not just that it exists — docker run --rm myapp node dist/app.js (or whatever your entry point is) should start the app the same
way CMD does, since a successful tsc compile with an unrelated startup
issue is a separate class of problem from the one this fix addresses.
One email a month — the latest explained errors
What the site published that month — mostly Next.js, Supabase and Node errors, explained. You confirm by email; unsubscribe any time in one click.
If the error changes after this fix#
Once the compile step succeeds, some projects hit a Node-version mismatch
instead — code that compiles locally on Node 20 throws a different error
inside a container pinned to an older Node base image. That's a distinct
problem from the missing compiler covered here, and the fix is pinning your
FROM node: tag to match your local major version rather than touching
devDependencies again. Two related but distinct failures are worth telling
apart from a missing tsc: if the container instead fails to start the
compiled output because the image's CPU architecture doesn't match the one
it was built for, see
Fix 'exec format error' on AWS Fargate: ARM vs x86 images;
if docker exec complains a command isn't found inside a running
container (rather than during the build), that's usually the tool you're
trying to run simply not being present in that particular base image, not a
devDependencies problem at all — covered in
Fix OCI runtime exec failed: executable file not found.
A Dockerfile that separates "install everything and build" from "run only
what's needed" avoids this whole class of error — and keeps your final image
smaller, since the TypeScript compiler and its dependencies never ship to
production. If you're setting up the wider development environment around
this Dockerfile — the docker-compose.yml for local Postgres, hot-reload
volumes, and the rest of a day-to-day dev loop — see
Docker Tutorial 2026: Dev Environment with Compose
for the pieces that sit outside the build stage itself.
Further reading: Fix Firebase Functions Deploy: Missing: X from lock file. Also see Jenkins "Invalid agent type docker specified" Fix.
Frequently Asked Questions
Still stuck? Paste your error.
Paste the error message or stack trace — we check it against every fix and tool on the site.
Continue Reading
Fix Cannot find module '/dist/main' in a Docker build
Find out which file the builder stage really emitted, then fix the Dockerfile order and the TypeScript output layout so node dist/main starts.
Fix Cannot find module 'dotenv' in Docker Compose (Node)
Reproduced step by step in Compose: why npm install in the Dockerfile is not enough under a bind mount, and why a new package still goes missing after a rebuild.
Fix Docker Compose "unknown flag: --iidfile" Build Error
The flag Compose added is real; the CLI parsing it is the part that is too old. Find which docker binary your build actually runs, then fix that one.


