TypeScript

Fix dotenv: command not found in a NestJS start script

Get a NestJS start:dev script that calls dotenv -e running again, and avoid the dotenv 18 binary that now answers with a usage screen instead.

Mahdi Ben RhoumaMahdi Ben Rhouma
7 min read

TL;DR#

  • Cause: the script dotenv -e env/local.env -- nest start --watch calls a command named dotenv. The dotenv package in the project (version 10 in the question) is a library: it declares no executable, so npm links nothing into node_modules/.bin and the shell reports dotenv: command not found.
  • Fix: install the package that owns that syntax, dotenv-cli, as a dev dependency: npm install -D dotenv-cli. Or drop the wrapper and use nest start --env-file env/local.env.
  • New since September 2026: dotenv 18 ships its own dotenv binary with a different syntax (dotenv run -f …). With it, the same script no longer says "command not found"; it prints a usage screen and exits 1.
  • Reproduced on Windows 11 with Node v26.7.0 and npm 11.19.0, through Git Bash as the npm script shell, on 30 October 2026.

The error#

The question on Stack Overflow, dotenv: command not found in nestjs project, has "dotenv": "^10.0.0" in dependencies and this start script:

json
{
  "scripts": {
    "start:dev": "dotenv -e env/local.env -- nest start --watch"
  }
}

Reproducing it in a minimal NestJS project with dotenv 10.0.0 and no dotenv-cli, running the script through bash:

text
> dotenv -e env/local.env -- nest start
 
/usr/bin/bash: line 1: dotenv: command not found

The exit code was 127, the shell's "command not found" status. The NestJS CLI never started: the failure happens before nest is reached. The exact wording depends on the shell that runs the script. Under npm's default shell on Windows, cmd.exe, the same project printed cmd.exe's "not recognized" message for dotenv, in French on the test machine, and exited 1.

Why it happens#

npm run does not search node_modules for any package whose name matches the first word of the script. The npm run documentation says that "In addition to the shell's pre-existing PATH, npm run adds node_modules/.bin to the PATH provided to scripts." Only packages that declare a bin field in their own package.json get an entry in that folder (package.json bin reference).

The dotenv library is meant to be imported (require('dotenv').config()), not run. Its registry metadata for 10.0.0 has no bin field, and in the reproduction node_modules/.bin did not even exist after npm install dotenv@10.0.0:

text
$ ls node_modules/.bin
ls: cannot access 'node_modules/.bin': No such file or directory

The -e <file> -- <command> form comes from a different package, dotenv-cli. Its README documents dotenv -e .env2 -- <command with arguments> to load another file instead of .env. dotenv-cli declares two binaries, dotenv and dotenv-cli, and depends on the dotenv library internally. Having dotenv in package.json therefore installs the parser, but not the command the script calls.

Fix#

1. Install dotenv-cli as a dev dependency#

bash
npm install -D dotenv-cli

With Yarn, yarn add -D dotenv-cli. The accepted answer on the question also suggests npm install -g dotenv-cli. That works on the machine where it runs, but a fresh clone or a CI runner does not have it. A project dependency is linked into node_modules/.bin on every install, so prefer it.

After installing dotenv-cli 11.1.0, the folder holds the command the script needs:

text
$ ls node_modules/.bin
dotenv
dotenv-cli
dotenv-cli.cmd
dotenv-cli.ps1
dotenv.cmd
dotenv.ps1
node-which
node-which.cmd
node-which.ps1

The script itself does not change. -e names the file and -- separates dotenv-cli's options from the command it should run.

2. Or let the NestJS CLI load the file#

The current NestJS CLI has its own option, so the wrapper is not needed. In the start command source, --env-file [path] is described as "Path to an env file (.env) to be loaded into the environment." The start action turns each path into a --env-file= argument for the Node process that runs the compiled app.

json
{
  "scripts": {
    "start:dev": "nest start --watch --env-file env/local.env"
  }
}

This works on any Node release that has the flag: the Node.js CLI reference lists --env-file as added in v20.6.0. The reproduction used @nestjs/cli 12.0.11 on Node v26.7.0 and ran nest start --env-file env/local.env without --watch; the start action source passes the file in watch mode as well. With this option, neither dotenv nor dotenv-cli has to be in package.json for the start script.

3. Or, on dotenv 18, use dotenv's own command#

If you have already moved to dotenv 18, its built-in command can replace dotenv-cli, with its own syntax:

json
{
  "scripts": {
    "start:dev": "dotenv run -f env/local.env -- nest start --watch"
  }
}

The dotenv README documents -f, --file <paths> to "Load one or more files" and notes that "The -- separator is optional".

Verify the fix#

Run the script again and print one variable the file defines. In the reproduction, env/local.env held APP_PORT=3001 and DB_HOST=localhost, and src/main.ts logged both. A minimal NestJS 12 project (@nestjs/core 12.3.2, TypeScript 6.0.3) started with dotenv-cli installed:

text
> dotenv -e env/local.env -- nest start
 
APP_PORT=3001 DB_HOST=localhost

And without any wrapper, through the NestJS CLI option:

text
> nest start --env-file env/local.env
 
APP_PORT=3001 DB_HOST=localhost

If the values print as undefined, the command now runs but did not read the file you meant. In the reproduction the relative path env/local.env was written from the project root, the folder that holds package.json; check yours against that folder first.

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.

When dotenv 18 answers instead of the shell#

The question dates from 2021, when no version of the dotenv library had a binary. That changed with dotenv 18.0.0: its CHANGELOG lists "NEW: Dotenv now has a CLI." (PR #1022), released 17 September 2026. The registry's latest tag pointed to 18.0.9 on 30 October 2026, so a plain npm install dotenv in a new project installs the binary. A project pinned to ^10.0.0, as in the question, stays on 10.x and keeps the "command not found" error.

With dotenv 18.0.9 and the unchanged dotenv-cli script, the shell finds a dotenv command, and it rejects the arguments:

text
> dotenv -e env/local.env -- node print.js
 
Usage: dotenv run [--help] [-q|--quiet] [--debug] [--override] [--fast] [-f|--file <paths>] [--] <command> [args...]

It printed its full help after that line and exited 1, without running the command. Rewriting the script to dotenv run -f env/local.env -- node print.js fixed it:

text
◇ injected env (2) from env/local.env
APP_PORT=3001 DB_HOST=localhost

dotenv 18 and dotenv-cli in the same project#

Both packages now declare a binary called dotenv, and only one of them can own node_modules/.bin/dotenv. With npm 11.19.0, the same package.json and lockfile gave two different results:

  • installing dotenv@18.0.9 first and then npm install -D dotenv-cli linked dotenv-cli's script, and dotenv -e env/local.env -- … worked;
  • deleting node_modules and running npm ci linked dotenv/dist/index.cjs instead, and the same script printed the dotenv 18 usage screen and exited 1.

A script that works after a manual install and fails on a clean install is easy to misread as a broken CI cache. Two ways out that the reproduction confirmed: call dotenv-cli by its other binary name, dotenv-cli -e env/local.env -- nest start --watch, which no other package claims; or remove one of the two packages and use only its syntax.

Another dotenv on the PATH#

The shell's own PATH still comes first for anything npm did not link. On the test machine, the Python package python-dotenv 1.2.4 had installed a dotenv.exe in the Python Scripts folder. Before dotenv-cli was installed, that program answered the npm script instead of the shell:

text
Error: Invalid value for '-e' / '--export': 'env/local.env' is not a valid boolean. Recognized values: , 0, 1, f, false, n, no, off, on, t, true, y, yes

If the message comes from an unfamiliar tool, run which -a dotenv (or where dotenv in cmd.exe) to see which program the name resolves to. Installing dotenv-cli in the project fixed this case too: with python-dotenv still on the PATH, the npm script ran the project's dotenv-cli and printed both variables.

The NestJS --env-file option and a missing file#

nest start --env-file is stricter than dotenv-cli when the file is absent. With @nestjs/cli 12.0.11, renaming env/local.env away made Node stop before the app started:

text
C:\Program Files\nodejs\node.exe: env/local.env: not found

The exit code was 9. That matters in CI and in containers, where .env files are usually git-ignored and the variables come from the environment. The NestJS CLI source on its main branch now checks for the file first and starts without it after a "not found - starting without it" notice (start action). That was not the behaviour of 12.0.11 in this test, so check your installed version before relying on it, or keep --env-file in the development script only.

Neighbouring errors and the one that may come next#

The same mechanism, a local package whose command the shell cannot see, is behind tsc is not recognized as an internal or external command, where the fix is again to run the project's own binary through an npm script. If dotenv itself cannot be loaded inside a container, rather than not found as a command, see Cannot find module 'dotenv' in Docker Compose: there a bind mount hides the image's node_modules. Database tooling uses the same wrapper: the drizzle-kit migrate page runs its migration script through dotenv -e .env -- and needs the same dev dependency.

Once the script starts, a value that should come from the file can still be missing at runtime. If JwtModule then fails with secretOrPrivateKey must have a value, check that page's second cause: a constant that reads process.env.JWT_SECRET before ConfigModule.forRoot() has loaded the file. Loading the file in the start script, as above, puts the variables in process.env before any NestJS module is evaluated, which removes that ordering problem.

Frequently Asked Questions

|

Have more questions? Contact us

Still stuck? Paste your error.

Paste the error message or stack trace — we check it against every fix and tool on the site.

Matching happens in your browser. When you search, a cleaned error signature is sent so we can spot errors we do not cover yet — never the raw text, your IP address or your e-mail. Do not paste passwords or keys. Details

Written by

Mahdi Ben Rhouma
Mahdi Ben Rhouma

Full-Stack Dev — Next.js & Supabase

Solo developer building SaaS products with Next.js and Supabase. Writing about production patterns the official docs skip.

Remote