(そんなことするかどうかは置いておいて)たとえばdocker-node-npm-scripti-permission-issue-demoをGithub codespaces上で起動して、npm ci
して、npm run build
を実行すると、permission denied, scandir '/root/.npm/_logs'
というエラーのWARNが発生する。
原因はnpm run build
の中身が npm run tsc
になっているため。rootユーザーで別のユーザーが所有権を持つディレクトリで実行すると問題になる。
解決方法はDocker containerを実行するユーザーをnode
に変更するとか、npm configで、npm config set cache /tmp
と設定するとか(export npm_config_cache=/tmp
でもいける雰囲気)、所有権を調整すると解決する。解決方法は他にもいろいろあると思う。
再現手順はdocker-node-npm-scripti-permission-issue-demoのcodespacesのVS codeの中でターミナルを開いて、以下のような感じでDocker containerを起動して、npm ci
とnpm run build
を実行するとエラーを再現することができる。動作環境はNode.js 16.18.0でnpmは8.19.2。たぶんNode.js 18.xでも発生する(npmのバージョンは変わらないので)。
docker build -f Dockerfile -t demo .
docker run -it --rm -v `pwd`:/current -w /current demo bash
root@71524f7f2bea:/current# npm ci
... (snip) ...
added 1 package, and audited 2 packages in 2s
found 0 vulnerabilities
... (snip) ...
root@71524f7f2bea:/current# npm run build
> docker-node-npm-scripti-permission-issue-demo@1.0.0 build
> npm run tsc
npm WARN logfile Error: EACCES: permission denied, scandir '/root/.npm/_logs'
npm WARN logfile error cleaning log files [Error: EACCES: permission denied, scandir '/root/.npm/_logs'] {
npm WARN logfile errno: -13,
npm WARN logfile code: 'EACCES',
npm WARN logfile syscall: 'scandir',
npm WARN logfile path: '/root/.npm/_logs'
npm WARN logfile }
> docker-node-npm-scripti-permission-issue-demo@1.0.0 tsc
> tsc
npm ERR! code EACCES
npm ERR! syscall mkdir
npm ERR! path /root/.npm/_cacache/tmp
npm ERR! errno -13
npm ERR!
npm ERR! Your cache folder contains root-owned files, due to a bug in
npm ERR! previous versions of npm which has since been addressed.
npm ERR!
npm ERR! To permanently fix this problem, please run:
npm ERR! sudo chown -R 1000:0 "/root/.npm"
npm ERR! Log files were not written due to an error writing to the directory: /root/.npm/_logs
npm ERR! You can rerun the command with `--loglevel=verbose` to see the logs in your terminal
以下は詳細。
npmのscriptは@npmcli/run-scriptが実行スクリプトで/usr/local/lib/node_modules/npm/node_modules/@npmcli
にある。これのバージョンは4.2.1なので、run-script.jsが実際の処理になる。これをrun-script-pkg.js、@npmcli/promise-spawnと辿っていくと、promise-spawn/lib/index.jsに到達する。
ここで実行しているinfer-ownerは、スクリプトを実行するディレクトリの所有権を推測して、ディレクトリのuid、gidを返してくれる。run-script.jsではこれを利用してchild_process.spawnをuid, gid付きで実行する。
codespacesのディレクトリはcodespace(uid:1000)がオーナーで、Docker containerのユーザーはデフォルトではroot(uid:0)なので、codespaceが所有するディレクトリでrootユーザーが処理を実行する形になる。
具体的にはrootユーザーがnpm scriptが実行したとき、uid:1000, gid:0でchild_process.spawnを実行する。ここまでは特に問題ない。
npm scriptの中でnpm run
を記述していると、run-script
の中でrun-script
を実行する状態になる(npm scriptもnpm runもrun-script
で実行するので)。
つまり、root(uid:0)ユーザーが、uid:1000, gid:0で実行した処理の中で、uid:1000, gid:0で処理を実行する。するとnpm cacheのディレクトリ、/root/.npm
に対してログを出力しようとしてパーミッションエラーとなる雰囲気。なぜrootユーザーのキャッシュディレクトリを参照するとエラーになるかは理解できていない。run-scriptを実行してるユーザーのuidでファイルアクセスしようとするためなのだろうか。
なので、Docker containerの実行ユーザーをrootではないユーザーに変更するか、rootユーザーを維持したければ、スクリプト実行前にnpm config set cache /tmp
みたいな感じで、キャッシュディレクトリを適当な場所に変更しておけば、問題は発生しない。
ところで、このinfer-ownerによる処理、feat: remove infer-ownerのプルリクエストで削除されることになった。@npmcli/promise-spawnの5.0以降は同じ問題は発生しなくなる。npm cliでは9.0.1で5.0がインストールされるようになる。Node.jsではdeps: upgrade npm to 9.1.0にて進行中のよう。
しかしながら、run-scriptの処理は(npm install / 詳細は未確認)でも利用されている。つまり今までは、npm installを実行するユーザーが誰であれ、インストール先のディレクトリのオーナーでインストールしてくれるということであり、インストールする実行ユーザーを気にしないで済んでいた。npm v9.0.1以降はそうはいかなくなる。
たとえば、Docker container中で、rootユーザーでnpm ci
したりして、node_modulesの中だけ所有権がrootになってしまったり、npm scriptで生成したファイルの所有権がrootユーザーになったり(生成したファイルを他ユーザーで削除しようとしてエラーになったり)などという問題が発生する可能性がある。
なのでnpm install、npm scriptの実行するユーザーは今のうちに見直しておくのが良いかもしれない。
という長いメモであった。