Search code examples
pnpm

How do I make pnpm work as intended in a vscode dev container?


I am coding in a vscode dev container, where pnpm was installed as suggested in the docs, using curl:

RUN curl https://get.pnpm.io/install.sh | ENV="$HOME/.bashrc" SHELL="$(which bash)" bash -

Today, pnpm told me it needed an update and suggested using this command:

Update available! 8.7.4 → 8.7.5.

Changelog: https://github.com/pnpm/pnpm/releases/tag/v8.7.5

Run "pnpm add -g pnpm" to update.

So, I ran pnpm add -g pnpm, which did the update, but gave the following warning:

Nothing to stop. No server is running for the store at /home/magnus/.local/share/pnpm/store/v3

That spurred me to investigate further. I found the following:

1) First Finding

pnpm store path logs: /home/magnus/repositoryfolder/.pnpm-store/v3.

In other words, the global store is placed inside my repository, and not in ~/.local/share/pnpm/store/v3 which is the default.

Here from the docs:

store-dir

The location where all the packages are saved on the disk.

Default:

  • If the $PNPM_HOME env variable is set, then $PNPM_HOME/store
  • If the $XDG_DATA_HOME env variable is set, then $XDG_DATA_HOME/pnpm/store
  • On Windows: ~/AppData/Local/pnpm/store
  • On macOS: ~/Library/pnpm/store
  • On Linux: ~/.local/share/pnpm/store

If there is a home directory on the current disk, then the store is created inside it. If there is no home on the disk, then the store is created at the root of the filesystem. For example, if installation is happening on a filesystem mounted at /mnt, then the store will be created at /mnt/.pnpm-store. The same goes for Windows systems.

And here from a support thread:

When choosing a location for the global store, pnpm will start with ~/.pnpm-store. If pnpm cannot create a hard link from your project to that location, then it will try to find the directory with the smallest length to which it can link. It will start from your projects directory. So if your project is at /1/2/3/project/, pnpm will try to make a hard link in /1/2/3/project. Then in /1/2/3/. Then in /1/2. Let's say it fails to make one in /1/2. In this case the store will be at /1/2/3/.pnpm-store.

This code is at: https://github.com/pnpm/store-path

2) Other Findings

  • which pnpm logs: /home/magnus/.local/share/pnpm/pnpm

Question

  • Why does pnpm add pnpm -g not recognize where my store is located?
  • How do I fix it? Should I add something to the Dockerfile vscode uses to create the dev container?

Notes

I have this in .bashrc and .bash_profile:

# ----------------------------------------------------------------
# Needed to ensure pnpm binary is added to PATH.
# ----------------------------------------------------------------
SHELL='/bin/bash'

# PNPM_HOME is path to pnpm executable file (i.e. bin file)
# Had to set the path here, to its original default,
# because docker somehow overwrote it.
export PNPM_HOME="/home/magnus/.local/share/pnpm"
case ":$PATH:" in
  *":$PNPM_HOME:"*) ;;
  *) export PATH="$PNPM_HOME:$PATH" ;;
esac

And this in the Dockerfile:

RUN curl https://get.pnpm.io/install.sh | ENV="$HOME/.bashrc" SHELL="$(which bash)" bash -
ENV PATH="/home/$USER_NAME/.local/share/pnpm:${PATH}"

Solution

  • Cause of the issue

    The store and the node_modules directory must be on the same volume for pnpm to create hard links between the two successfully.

    Say you have a project on your local filesystem at /home/magnus/my-project. When you use Reopen in Dev Container in VSCode, it binds /home/magnus/my-project to /workspaces/my-project within the container.

    So when trying to resolve where to place the store, here's what happens:

    Step Result
    Check /home/magnus/.local/share/pnpm ❌ Different filesystem to /workspaces/my-project
    Check / ❌ Different filesystem to /workspaces/my-project
    Check /workspaces ❌ Different filesystem to /workspaces/my-project
    Check /workspaces/my-project ✅ Same filesystem as /workspaces/my-project

    So the store gets created at /workspaces/my-project/.pnpm-store/v3.

    Solution

    Instead of using VSCode's Reopen in Dev Container, I used Clone Repository in Container Volume to clone the repo into an isolated Docker volume with no bindings (this has the added bonus of improved disk performance). pnpm was then happy to use parent directories for the store.

    (I'm sure there's a way to solve this more reliably with the mounts option in devcontainer.json, but haven't figured that part out yet.)


    Here's a full solution for setting up a new project that worked for me:

    1. Create a new repo on github.com
    2. In VSCode, press Ctrl+Shift+P, choose Dev Containers: Clone Repository in Dev Container, and select your newly-created Github repo
    3. Follow the prompts to create a new devcontainer.json file
    4. Once the container opens, create .devcontainer/Dockerfile with these contents (modify as you see fit):
      FROM mcr.microsoft.com/devcontainers/javascript-node:20
      
      WORKDIR /workspaces/my-project
      
      ENV PNPM_HOME=/workspaces/pnpm
      RUN corepack enable pnpm
      
      COPY pnpm-lock.yaml ./
      
      # optional: only needed if you've patched any packages
      COPY patches patches    
      
      # pre-loads required packages from pnpm-lock.yaml into the store,
      # ready to be installed with `pnpm install`
      RUN pnpm fetch
      
    5. Update .devcontainer/devcontainer.json to include the following:
      {
          "name": "my-project",
          "build": {
              "dockerfile": "Dockerfile",
      
              // Runs docker from the project root instead of `.devcontainer`,
              // to ensure `COPY pnpm-lock.yaml ./` works
              "context": ".."
          },
      
          // --offline will use the packages pre-loaded into the store
          // during `RUN pnpm fetch` in the Dockerfile
          "postCreateCommand": "pnpm install --offline"
      }
      
    6. Commit and push