Node without Node Installed
We use Node and NPM from a designated Docker image to create close parity between local development, pre-production, and production environments. Local installations are strongly discouraged to prevent version drift and promote reproducibility.
To support this workflow, we use a set of wrapper scripts that allow developers to run common Node and NPM commands as if they were installed locally, without directly using Node or NPM on their machine.
📁 resolve_parent_directory.bash: Canonical Path Resolution
This script identifies the real, absolute parent directory of a target file or symlink—including resolving any symbolic links in its path. It’s primarily used by wrapper scripts to find supporting files relative to the physical location of the script being executed.
Behavior:
- Accepts a file path as input (typically
$0) - Resolves symbolic links recursively
- Normalizes relative paths to absolute
- Outputs the physical parent directory path
This ensures that related assets (config, shared functions, templates) can be found reliably, regardless of how or where a script is invoked.
Suggested filename: resolve_parent_directory.bash
Suggested location: $HOME/etc/resolve_parent_directory.bash
Usage in other scripts: Source it directly using an absolute path:
Script:
🛠️ Preset Runner Scripts: Handling -it for Docker CLI
We provide three wrapper presets, each with a different approach to Docker’s interactive terminal (-it) behavior. These scripts use their filename ($0) to determine which containerized tool (e.g., npm, npx, node) to invoke.
All presets rely on a shared runtime script (run-cli.sh) and the resolve_parent_directory function to locate related files. To ensure consistent behavior, all presets and supporting scripts should be stored in the same directory, such as:
Suggested storage directory:
$HOME/etc/
This structure ensures symlinked tools (like npm or node) can resolve everything they need at runtime.
🧩 autonamedInteractive
Always uses -it and is ideal for interactive tools (like npx or shells).
Suggested filename: autonamedInteractive
Storage directory: $HOME/etc/
Usage: Link to commands that always expect a TTY
🧩 autonamedInteractiveWith0Args
Only includes -it when no arguments are passed; useful for tools like npm.
Suggested filename: autonamedInteractiveWith0Args
Storage directory: $HOME/etc/
Usage: Link to flexible tools that switch modes depending on input
🧩 autonamedNoninteractive
Never includes -it; good for node or other non-interactive tasks.
Suggested filename: autonamedNoninteractive
Storage directory: $HOME/etc/
Usage: Link to commands like node, tsc, or eslint
🧱 run-cli.sh: Shared Container Execution Logic
This script contains the shared logic used by all preset wrappers. It centralizes the logic for container execution, mounts, environment variables, and image configuration—allowing wrapper scripts like autonamedInteractive to stay minimal and maintainable.
Suggested filename: run-cli.sh
Suggested location: $HOME/etc/ (stored alongside your preset scripts)
Script:
The run-cli.sh script is doing a lot of the heavy lifting for us. It first determines whether we have a complete enough environment. Once it looks like the needed support tools and directories are there it makes sure it knows who we are so that it never runs as root but only our personal user and group. Then it does a few things to make sure we have a decent environment for running npm and node by making sure we can find the cache folders and the config files for it. It then makes sure we use a decent and safe path in the container that includes the needed tool locations. Then it configures the web socket address needed by vitest.
After that it runs the big whopper of a docker command to start the container with the proper image and all the mounts we needed and the right user and a bunch of environment settings we need in order for everything to work. Keep in mind this is a work in progress so we might be missing something if nobody has tried the tool and it has new needs.
As a general fudge for when we are experimenting, I added support for an EXTRA_DOCKER_ARGS environment variable. You can put things in there like EXTRA_DOCKER_ARGS=“-p 9229:9229” if you need to open the debug port for example.
For those who want a more complicated list of what that docker command does for reference:
- We will run a command using this image and a new container.
- DOCKER_INTERACTIVE_TTY says to use -it or not to redirect terminal lines for an interactive run.
- We run as ourselves to avoid problems running as root. Doesn’t work with every image.
- Add a bunch of names to the container’s DNS resolver which all point to the outgoing gateway
- Apply the various mounts we need as determined in the earlier scripting
- Start us in the START_DIR which is usually the current directory in the container & host
- Sets a couple of things that are needed to run an Azure function locally
- A hack that lets me run my function app but is porbably unneeded outside of that
- Makes our HOME variable in the container the same as our HOME on linux
- Sets the PATH based on what we decided so far
- Attempts to get color output when possible
- Expects to always work ONLY for NODE’s dev environment
- Forces the log level to info
- Passes the NPM token through to the container so we can use it for npm install
- Sets a variable I need to make vitest work correctly in vscode
- Sets a variable that might only be for Jason that says where the copilot preferences are
- Sets a git command hack that lets me avoid problems with always thinking every machine is new
- Anything from the EXTRA_DOCKER_ARGS we set (like for the debug port maybe)
- Use the image we were told to use from the environment
- Run the command we got from the environment and send it all of the args we got passed
🔗 Creating Linked Commands in Your ~/bin
To use these wrapper presets easily from anywhere, we recommend setting up a personal ~/bin directory and adding it to your shell’s PATH.
Step 1: Create the bin directory (if it doesn’t exist)
Step 2: Add it to your PATH
Edit your shell’s startup script (e.g., ~/.bash_profile, ~/.bashrc, or ~/.profile) and add:
Then either restart your terminal or reload the config:
Step 3: Create symlinks to the presets
From within the ~/bin directory, link tool names to the appropriate preset script in $HOME/etc/:
Note: Tools like
eslintwill only work if they’re already installed in your project’snode_modules.
🧹 Removing nvm, Node.js, and npm from Your Local Machine
Once you’ve adopted the container-based Node workflow, uninstall all local versions of Node.js, npm, and nvm. Retaining them creates ambiguity in execution paths and increases the risk of bypassing the standardized container environment.
⚠️ This section assumes Node and npm were installed via the nvm script (e.g.,
curl ... | bash). If other installation methods were used (e.g., Homebrew, apt), additional steps may apply.
Step 1: Uninstall all Node.js versions
Repeat nvm uninstall <version> for any additional listed versions.
Step 2: Remove nvm itself
Typically, NVM_DIR is ~/.nvm, but run echo $NVM_DIR to confirm.
Step 3: Remove nvm references from shell startup scripts
Edit the appropriate file for your shell (~/.bashrc, ~/.bash_profile, ~/.zshrc, etc.) and remove lines like:
Then restart your terminal or run source ~/.bashrc (or equivalent) to apply changes.