How to Diagnose vite: not found After npm ci in a Container Build — Opportunihub
Course Remote

How to Diagnose vite: not found After npm ci in a Container Build

Ahmed Tariq · Remote

At a glance

Type
Course
Organisation
Ahmed Tariq
Location
Remote
Work mode
Remote
Deadline
Rolling / not stated
Posted
21 Oct 2026

About this course

<p>I recently worked on a project where a Node.js application used Vite to compile its frontend. It worked in a browser-hosted development environment, but its image build failed after the move to Azure Container Registry. The build reported a missing Vite executable after an earlier dependency install had failed.</p> <p>The container build ended with a simple error:</p> <pre><code class="language-text">sh: 1: vite: not found </code></pre> <p>Vite already appeared in <code>devDependencies</code>. The build stage had run <code>npm ci</code> before calling the build script. The Dockerfile looked ordinary, so the first explanations all pointed at dependency selection or <code>NODE_ENV</code>.</p> <p>The missing binary came from an earlier failure. <code>npm ci</code> had tried to download hundreds of tarballs from a package host that only existed inside the browser-based development environment. It printed <code>Exit handler never called!</code>, left <code>node_modules</code> empty, and didn't give Docker a clear failure status. The following layer reached <code>npm run build</code> and reported the first missing executable it needed.</p> <p>The lockfile pointed npm at an unreachable host. This tutorial shows how to check whether the install completed, find the affected tarball URLs, and check the dependency layer before compiling the application. The repair changes the public-package URL prefix and adds build checks. Package versions, integrity values, application code, and Vite configuration stay the same.</p> <h2 id="heading-table-of-contents">Table of Contents</h2> <ul> <li><p><a href="#heading-before-you-begin">Before You Begin</a></p> </li> <li><p><a href="#heading-how-the-build-reached-the-failure">How the Build Reached the Failure</a></p> </li> <li><p><a href="#heading-step-1-check-whether-npm-ci-installed-anything">Step 1: Check Whethernpm ciInstalled Anything</a></p> </li> <li><p><a href="#heading-step-2-find-where-the-lockfile-would-download-packages">Step 2: Find Where the Lockfile Would Download Packages</a></p> </li> <li><p><a href="#heading-step-3-rewrite-only-the-known-public-package-url-prefix">Step 3: Rewrite Only the Known Public-package URL Prefix</a></p> </li> <li><p><a href="#heading-step-4-apply-the-guard-at-every-independent-install">Step 4: Apply the Guard at Every Independent Install</a></p> </li> <li><p><a href="#heading-step-5-verify-the-dependency-tree-and-the-trust-boundary">Step 5: Verify the Dependency Tree and the Trust Boundary</a></p> </li> <li><p><a href="#heading-references">References</a></p> </li> </ul> <h2 id="heading-before-you-begin">Before You Begin</h2> <p>You need a Node.js project with <code>package.json</code>, a version 2 or 3 <code>package-lock.json</code>, its container build log, and Docker. The example uses a supported Node 24 image and a Vite project that writes its build output to <code>dist</code>. Check your own dependency compatibility and output directory.</p> <p>Check whether the affected packages come from a public or private registry before changing any URLs. The <code>.invalid</code> hostname below stands in for the original internal host. Adapt that prefix only after reviewing the packages it serves.</p> <h2 id="heading-how-the-build-reached-the-failure">How the Build Reached the Failure</h2> <p>The application came from a browser-hosted development environment and later moved into an Azure Container Registry build. The development platform acted as a proxy for public npm packages. When it regenerated <code>package-lock.json</code>, it stored its own internal tarball URLs in the <code>resolved</code> fields.</p> <p>The failure crossed two environments:</p> <ol> <li><p>Inside the development environment, npm could reach the package proxy. The lockfile kept those proxy URLs.</p> </li> <li><p>The Azure Container Registry image builder received the same lockfile, but it couldn't resolve the proxy hostname.</p> </li> <li><p>The dependency install printed <code>Exit handler never called!</code> and left <code>node_modules</code> empty. Its failure status didn't stop the next Docker layer in this incident.</p> </li> <li><p>The next layer ran <code>npm run build</code>, which called Vite. With no installed Vite executable, the build reported <code>vite: not found</code>.</p> </li> </ol> <p>The checks below follow that chain backward from the missing executable to the package locations, then repair the URLs and guard each independent install.</p> <img src="https://cdn.hashnode.com/uploads/covers/61c08efcc4195d32e16a4e7a/a78d37d6-1fb7-4b28-b950-5d24f3b90d8d.png" alt="A lockfile points npm at an unreachable package host. npm leaves node_modules empty, and the next Docker layer reports that Vite is missing." style="display: block;" width="600" height="400" loading="lazy"> <p><em>Figure 1. The container build reported the last failure in the chain.</em></p> <h2 id="heading-step-1-check-whether-npm-ci-installed-anything">Step 1: Check Whether <code>npm ci</code> Installed Anything</h2> <p>The project used <code>npm ci</code>, which was the correct command for a repeatable container build. npm's documentation says that the command removes an existing <code>node_modules</code> directory before it starts the install. It also treats the lockfile as frozen instead of updating it.</p> <p>Those properties made this failure unusually clear. The build couldn't fall back to packages from an earlier local install. Once the tarball downloads failed, the dependency directory contained nothing useful.</p> <p>The Dockerfile separated dependency installation from source compilation. Step 3 compares that original sequence with the revised build.</p> <p>The later <code>vite: not found</code> message didn't show whether npm had installed the package. Start with the dependency directory in the same environment that failed:</p> <pre><code class="language-bash">test -d node_modules &amp;&amp; find node_modules -mindepth 1 -maxdepth 1 | head test -x node_modules/.bin/vite </code></pre> <p>In this build, the first check found no installed packages. The second confirmed that the Vite executable was missing. A missing binary can also result from omitting development dependencies, so inspect the directory before changing <code>NODE_ENV</code> or the package manifest.</p> <p>After a successful install, the directory listing should show package names. The <code>test -x</code> command prints nothing. Immediately after it, inspect its exit status:</p> <pre><code class="language-bash">echo $? </code></pre> <p>For a missing Vite executable, the expected result is:</p> <pre><code class="language-text">1 </code></pre> <p>A result of <code>0</code> means the executable exists and is executable.</p> <p><strong>Checkpoint:</strong> An empty dependency directory points to the install step. If other packages exist but Vite is missing, also check whether the install omitted development dependencies. These commands inspect the dependency directory without changing it.</p> <h2 id="heading-step-2-find-where-the-lockfile-would-download-packages">Step 2: Find Where the Lockfile Would Download Packages</h2> <p>A generalized entry looked like this:</p> <pre><code class="language-json">{ "node_modules/example-package": { "version": "1.2.3", "resolved": "http://packages.internal.invalid/npm/example-package/-/example-package-1.2.3.tgz", "integrity": "sha512-&lt;subresource-integrity-value&gt;" } } </code></pre> <p>The <code>.invalid</code> hostname above replaces the real internal hostname. The structure matches the historical lockfile.</p> <p>npm documents <code>resolved</code> as the location from which it obtained a package. For a registry package, that value can be a tarball URL. The configured registry on the build runner therefore didn't solve this incident. The lockfile already told npm where to fetch each tarball.</p> <p>I counted the affected entries in the historical file. Of 1,038 dependency entries, 528 carried the internal hostname. Each pointed at a host that didn't resolve in the external build environment.</p> <p>The npm debug output pointed to an earlier failure. The hostname failures appeared before <code>Exit handler never called!</code>. That order mattered. The exit-handler message described how npm ended, while the failed downloads explained why the dependency tree was empty.</p> <p>I inspected the lockfile as data instead of searching for one package name. This small script groups every HTTP <code>resolved</code> value by hostname:</p> <pre><code class="language-javascript">const fs = require('node:fs') const lock = JSON.parse(fs.readFileSync('package-lock.json', 'utf8')) const hosts = {} for (const entry of Object.values(lock.packages ?? {})) { if (!entry.resolved?.startsWith('http')) continue const host = new URL(entry.resolved).host hosts[host] = (hosts[host] ?? 0) + 1 } console.table(hosts) </code></pre> <p>That inventory showed that the problem affected 528 entries rather than one broken tarball. It also made the repair boundary clear. The package locations recorded in <code>resolved</code> needed to change, while versions and integrity values stayed the same. If the host list includes a private registry, stop here and identify which packages it serves before rewriting anything.</p> <p>The historical inventory contained this row. The table uses the replacement hostname from the example:</p> <table> <thead> <tr> <th>Host</th> <th>Resolved URLs</th> </tr> </thead> <tbody><tr> <td><code>packages.internal.invalid</code></td> <td>528</td> </tr> </tbody></table> <p>Your hostnames and counts will depend on your lockfile. This inventory counts HTTP tarball locations, so its total can differ from the number of dependency entries.</p> <p><strong>Checkpoint:</strong> Identify the unreachable host and confirm which of its packages come from the public npm registry. Record the affected count before choosing a replacement prefix. A hostname count alone doesn't establish that rewriting those URLs is safe.</p> <h2 id="heading-step-3-rewrite-only-the-known-public-package-url-prefix">Step 3: Rewrite Only the Known Public-package URL Prefix</h2> <p>Regenerating the lockfile can mix a registry repair with dependency updates. Keeping the package versions and integrity values unchanged makes the repair easier to review.</p> <p>The affected packages in this application came from the public npm registry. The development platform proxied them and preserved their integrity values. The historical repair replaced its URL prefix before the container install.</p> <p>For this walkthrough, save the following as <code>rewrite-lockfile.cjs</code>. It changes only <code>resolved</code> fields with the approved prefix. It checks for missing integrity values and unexpected paths on the old host before writing the file.</p> <pre><code class="language-javascript">const fs = require('node:fs') const oldPrefix = 'http://packages.internal.invalid/npm/' const newPrefix = 'https://registry.npmjs.org/' const oldHostname = new URL(oldPrefix).hostname const filename = 'package-lock.json' const lock = JSON.parse(fs.readFileSync(filename, 'utf8')) if (!lock.packages || typeof lock.packages !== 'object') { throw new Error('Expected a lockfile with a packages object (version 2 or 3)') } let rewritten = 0 for (const entry of Object.values(lock.packages)) { if (typeof entry.resolved !== 'string') continue if (!entry.resolved.startsWith(oldPrefix)) continue if (!entry.integrity) throw new Error('An affected package has no integrity value') entry.resolved = newPrefix + entry.resolved.slice(oldPrefix.length) rewritten += 1 } for (const entry of Object.values(lock.packages)) { if (typeof entry.resolved !== 'string') continue if (!entry.resolved.startsWith('http')) continue if (new URL(entry.resolved).hostname === oldHostname) { throw new Error('The old hostname remains under an unexpected URL prefix') } } fs.writeFileSync(filename, JSON.stringify(lock, null, 2) + '\n') console.log(`Rewrote ${rewritten} resolved URLs`) </code></pre> <p>When the dependency stage runs the script, its message should match the number of URLs that use the approved prefix. The small test project used to check this tutorial produced:</p> <pre><code class="language-text">Rewrote 40 resolved URLs </code></pre> <p>That test project is separate from the historical application, which had 528 affected entries. Use the inventory for your own file as the expected count. A zero count on an already-repaired file is expected. If an unrepaired file produces zero, check the prefix.</p> <p>The script doesn't establish that a package is public. Confirm that separately. npm's integrity check then verifies that the downloaded bytes match the lockfile.</p> <p>Keep local dependencies and configuration files out of the build context with a <code>.dockerignore</code>:</p> <pre><code class="language-text">node_modules dist .git .env* .npmrc </code></pre> <p>This example installs public packages. A private registry needs its own reachable endpoint and secure build-time authentication.</p> <p>The first block below shows the affected instructions from the original Dockerfile. The second is the complete tutorial build. It uses Node 24 and the guarded install, so check compatibility with your own application.</p> <h3 id="heading-before-original-install-and-build-sequence">Before: Original Install and Build Sequence</h3> <pre><code class="language-dockerfile">COPY package.json package-lock.json ./ RUN npm ci COPY . . RUN npm run build </code></pre> <h3 id="heading-after-url-rewrite-and-dependency-checks">After: URL Rewrite and Dependency Checks</h3> <p>Use two build stages so source changes don't invalidate the dependency-install stage:</p> <pre><code class="language-dockerfile">FROM node:24-slim AS dependencies WORKDIR /app COPY package.json package-lock.json rewrite-lockfile.cjs ./ RUN node rewrite-lockfile.cjs \ &amp;&amp; npm ci --include=dev \ &amp;&amp; test -x node_modules/.bin/vite FROM node:24-slim AS build WORKDIR /app COPY . . COPY --from=dependencies /app/node_modules ./node_modules COPY --from=dependencies /app/package-lock.json ./package-lock.json RUN npm run build </code></pre> <p>The revised Dockerfile makes each part of the install explicit:</p> <ul> <li><p>The URL rewrite replaces the approved public-package prefix before <code>npm ci</code> downloads packages.</p> </li> <li><p><code>--include=dev</code> makes the choice to install development dependencies explicit.</p> </li> <li><p>The Vite executable check stops the dependency stage if the install leaves Vite missing.</p> </li> <li><p>The separate dependency stage installs packages before the application build stage copies them and compiles the source.</p> </li> </ul> <p>The second stage copies the repaired lockfile after the application source. Otherwise, <code>COPY . .</code> would put the original lockfile back into the build image. The <code>.dockerignore</code> also prevents local <code>node_modules</code> from overwriting the dependencies from the first stage.</p> <p><code>--include=dev</code> includes Vite when it's a development dependency, even if the build environment sets <code>NODE_ENV=production</code>. This image is a build environment, so it needs the compiler and build tools. Choose a supported Node version compatible with your project.</p> <p>The historical lockfile comparison found 528 changed fields, all named <code>resolved</code>. It found zero changed <code>version</code> or <code>integrity</code> fields and no remaining internal hostname. The example script above also leaves unrelated fields unchanged, although JSON formatting can change.</p> <img src="https://cdn.hashnode.com/uploads/covers/61c08efcc4195d32e16a4e7a/16ca3441-7987-43cd-af39-d75ac51dbf00.png" alt="The rewrite changes only each affected resolved URL. Package versions and integrity values stay the same." style="display: block;" width="600" height="400" loading="lazy"> <p><em>Figure 2. The portability repair changes package locations while preserving dependency identity.</em></p> <p>The executable check makes the dependency stage fail if Vite is still missing. Docker stops at that stage instead of reaching the application build.</p> <p><strong>Checkpoint:</strong> Check the rewrite count against your inventory, confirm that the approved old hostname is gone, and compare versions and integrity values before and after the repair. The dependency stage must complete both <code>npm ci --include=dev</code> and the Vite executable check. Step 5 builds the image and verifies the application output.</p> <h2 id="heading-step-4-apply-the-guard-at-every-independent-install">Step 4: Apply the Guard at Every Independent Install</h2> <p>The continuous integration job and the container image builder ran separate installs. Fixing the workflow runner didn't change the <code>npm ci</code> inside the Dockerfile. The image build received the original repository files and created its own dependency layer in another environment.</p> <p>The rewrite belongs immediately after the dependency files enter the install environment. <a href="https://docs.docker.com/build/cache/optimize/">Docker's cache guidance</a> recommends installing dependencies before copying frequently changing source files. In the example, the dependency stage has that boundary. A source-only change rebuilds the application stage.</p> <p>Apply the same guard before each independent install:</p> <ul> <li><p>the continuous integration type-check install</p> </li> <li><p>the container build install</p> </li> <li><p>any runtime-stage install in a multi-stage Dockerfile</p> </li> </ul> <img src="https://cdn.hashnode.com/uploads/covers/61c08efcc4195d32e16a4e7a/5bb8c97d-e76e-4da9-b7c4-efd51a248eca.png" alt="The repository lockfile enters the CI runner and container builder separately. Each environment rewrites and checks the URLs before running npm ci." style="display: block;" width="600" height="400" loading="lazy"> <p><em>Figure 3. A workflow repair doesn't automatically repair the install inside a container build.</em></p> <p>This placement also prevented a misleading partial fix. A type check could otherwise pass while the image build failed because the two jobs used the same lockfile in separate installs.</p> <h2 id="heading-step-5-verify-the-dependency-tree-and-the-trust-boundary">Step 5: Verify the Dependency Tree and the Trust Boundary</h2> <p>This repair was safe because the affected entries represented public npm packages behind a transparent development-platform proxy. The integrity strings remained present, and the rewrite didn't change any package version.</p> <p>The same substitution would be unsafe for private packages, patched packages, or a registry that serves different bytes under the same package version. A portable build should keep those packages on a reachable private registry and configure authentication for that registry.</p> <p>Before applying this pattern elsewhere, I would inventory every <code>resolved</code> hostname and separate public packages from private ones. I would then compare package versions and integrity values before and after the change. The build should fail if an unapproved hostname remains.</p> <p>An npm CLI issue records the same <code>Exit handler never called!</code> message after package downloads from an internal registry timed out. That message doesn't identify the package location by itself. In this incident, the tarball URLs and failed hostname established the cause.</p> <p>Build the image and verify the output:</p> <pre><code class="language-bash">docker build --progress=plain -t vite-install-check . docker run --rm vite-install-check sh -c \ 'test -x node_modules/.bin/vite &amp;&amp; test -s dist/index.html' </code></pre> <p>The build must pass <code>npm ci</code>, the Vite executable check, and <code>npm run build</code>. The final command checks the expected Vite output in the resulting image. Adapt the output check if your project uses a different directory. The image in this example compiles the application. Choose a separate serving stage if you need a production web server.</p> <p>A successful minimal example doesn't validate every dependency in an existing application. Run the same build against the project you intend to ship. When a tool is missing, check the install result and its earlier errors before changing the build tool.</p> <h2 id="heading-references">References</h2> <ul> <li><p><a href="https://docs.npmjs.com/cli/v11/configuring-npm/package-lock-json/">npm documentation: package-lock.json fields</a></p> </li> <li><p><a href="https://docs.npmjs.com/cli/v11/commands/npm-ci/">npm documentation: npm ci</a></p> </li> <li><p><a href="https://github.com/npm/cli/issues/9739">npm CLI issue: Exit handler never called after registry timeouts</a></p> </li> <li><p><a href="https://docs.docker.com/build/cache/optimize/">Docker documentation: Optimizing build cache</a></p> </li> <li><p><a href="https://nodejs.org/en/about/previous-releases">Node.js release status</a></p> </li> <li><p><a href="https://diagrams.mingrammer.com/">Diagrams library used to create the figures</a></p> </li> </ul>

How to apply

  1. 1 Read the full details above and confirm you meet the eligibility criteria.
  2. 2 Prepare your documents — an updated CV, and any cover letter, proposal or certificates required.
  3. 3 Click Apply on official site to complete your application on Ahmed Tariq’s official page.
  4. 4 Submit as early as possible — many close once filled.
Apply on official site

Sourced from freecodecamp. Always verify details on the official website. Opportunihub never charges you to apply.

Frequently asked questions

How do I apply for How to Diagnose vite: not found After npm ci in a Container Build?

Review the full details and eligibility on this page, prepare your documents, then use the “Apply on official site” button to complete your application on Ahmed Tariq’s official page.

Is this opportunity remote or location-based?

This opportunity is remote-friendly and open to applicants who can work from anywhere.

Is How to Diagnose vite: not found After npm ci in a Container Build free to apply for?

Opportunihub lists this Course for free. Legitimate Courses do not ask for payment to apply — never pay a fee to submit an application.