第11章:USERでroot回避:Nodeアプリを“普通ユーザー”で走らせる🙂👟
この章はひとことで言うと、**「コンテナの中でも “rootで動かさない” をデフォルトにしよう」**です😄 やることはシンプルなのに、事故った時の被害がかなり減ります🛡️✨
まず結論:最低限これだけやる😊✅
- Dockerfileの最後のほうで
USER nodejs(非root)にする - アプリが書き込むフォルダだけ、そのユーザーに書けるようにしておく📁✍️
COPY --chown=...を使って「コピーしたファイルの所有者」も揃える📦👤
これが “安全デフォルト” の第一歩です🚶♂️🔒
(Docker公式のビルド・ベストプラクティスでも、権限が要らないなら USER で非rootにしようと明記されています)(Docker Documentation)
なぜrootが怖いの?😇💣(超ざっくり)
Dockerコンテナは「隔離されてるから安全!」と思いがちですが、rootで動かすと次の問題が起きやすいです👇
- アプリの脆弱性(RCEとか)で侵入された時、コンテナ内で何でもできる権限を渡しがち😱
- もし「マウント」「設定ミス」「別の脆弱性」が重なると、被害がホスト側に波及するルートが増えます🔥
- とくに「書き込みできる場所」が広いほど、改ざん・設置・破壊がラクになります🧨
Docker公式も「コンテナはデフォルトroot(UID 0)で動くから、非rootにしよう」「UID/GID指定も有効」と説明しています(Docker)
図でイメージ:root回避は“被害半径”を縮める🗺️✂️
[攻撃者がアプリを乗っ取った…] 😈
|
v
(コンテナ内プロセスの権限で) できることが決まる
rootで実行: できること多い😱 → 事故がデカくなりがち
一般ユーザー:できること少なめ🙂 → 事故を小さくしやすい
実践1:まず「今rootで動いてる」を確認しよう👀🔍
コンテナに入って、これを打ちます👇
docker compose exec app sh -lc "id && whoami"
uid=0(root)と出たら、rootで実行中です😇💥uid=1001(nodejs)みたいに出たら、非rootで実行中です🙂✅
実践2:Node + TypeScriptの “非rootデフォルト” Dockerfile(鉄板構成)🧱✨
Docker公式のNodeガイド(最近更新)でも、専用ユーザーを作って USER で切り替える例が載っています(Docker Documentation)
ここでは「超よく使う形」にギュッとまとめます✂️😄
例:最小構成(まずは理解用)📦
FROM node:24-alpine
WORKDIR /app
## 非rootユーザーを作る(例:uid=1001)
RUN addgroup -g 1001 -S nodejs \
&& adduser -S nodejs -u 1001 -G nodejs \
&& chown -R nodejs:nodejs /app
## 依存ファイル → 先にコピー(キャッシュが効く)
COPY package*.json ./
## 依存インストール(ここはrootのままでもOK)
RUN npm ci
## アプリ本体をコピー(所有者も揃える)
COPY --chown=nodejs:nodejs . .
## ここが主役:非rootで実行!
USER nodejs
EXPOSE 3000
CMD ["npm", "run", "dev"]
ポイントは3つだけ🙂👇
/appをnodejsが書けるようにしてる(chown)📁COPY --chown=...で、コピー後の所有者ズレを防ぐ📦- 最後に
USER nodejsで、実行時は非root👟
実践3:本番寄り(マルチステージ)で “最後だけ非root” を徹底🏗️🔒
本番は「ビルドは重い・実行は軽い」に分けたいので、こうなりがちです😊
(Docker公式Nodeガイドでも、ステージごとにユーザー作成→ USER 切り替え例があります)(Docker Documentation)
例:productionステージで確実に非root化🧰
## =========================
## build stage
## =========================
FROM node:24-alpine AS build
WORKDIR /app
COPY package*.json ./
RUN npm ci
COPY . .
RUN npm run build
## =========================
## runtime stage
## =========================
FROM node:24-alpine AS runtime
WORKDIR /app
## 非rootユーザー作成
RUN addgroup -g 1001 -S nodejs \
&& adduser -S nodejs -u 1001 -G nodejs \
&& chown -R nodejs:nodejs /app
## 実行に必要なものだけコピー(所有者も揃える)
COPY --from=build --chown=nodejs:nodejs /app/dist ./dist
COPY --from=build --chown=nodejs:nodejs /app/package*.json ./
COPY --from=build --chown=nodejs:nodejs /app/node_modules ./node_modules
## 実行は非root!
USER nodejs
EXPOSE 3000
CMD ["node", "dist/server.js"]
実践4:Compose側で user を上書きできる(でも“保険”扱い)🧯
Composeには user: があって、**「コンテナの実行ユーザーを上書き」**できます。
ただし 基本はイメージ(Dockerfileの USER)で安全デフォルトにしておくのが王道です🙂
Docker公式Composeリファレンスでも「user は実行ユーザーを上書き。デフォルトはイメージ(Dockerfile USER)で、未指定ならroot」と説明されています(Docker Documentation)
例:開発だけ user を固定したい場合
services:
app:
build: .
user: "1001:1001"
よくある詰まりポイント集(ここが一番大事)🧨🛠️
1) EACCES: permission denied が出る😵
だいたいこれ👇
/appやnode_modulesや.cacheに書こうとしてるのに、所有者がrootのままCOPY . .のせいで、ファイルがroot所有になってる
対策🙂✅
COPY --chown=nodejs:nodejs ...を使うRUN chown -R nodejs:nodejs /appを「最後に一回」入れる(雑に解決しやすい)- それでもダメなら「どこに書こうとしてるか」をログで特定して、書き込み場所を絞る(次章でガッツリやる)📁🔍
2) npm がキャッシュ書けなくて死ぬ😇
非rootだと、npm が触りたい場所に権限がなくてコケることがあります。
対策の方向性🙂
- キャッシュや一時ファイルは
/tmpなど “書いていい場所” に寄せる - もしくは、必要なディレクトリだけ
nodejsが書けるように作っておく
(書き込み場所の設計は次章の主役です🧺🗂️)
3) 80番ポートで待ち受けしたい(でも非rootだと無理)🤔
Linuxでは 1024未満のポートは基本rootが必要になりがちです。
でも安心🙂
- コンテナ内は 3000 で待ち受け
- 外側で
-p 80:3000みたいにマッピングすればOK(入口だけ80)🚪✨ - もしくはリバースプロキシを前に置く(第26章〜で出るやつ)🕸️
4) 「じゃあ chmod -R 777 で良くない?」😇💥
それは最後の最後! 777は「誰でも何でも書ける」になって、root回避の意味が薄れます🥲
- “書ける場所” を最小にする
- それ以外は読めるだけに近づける
この方向が、隔離ロードマップの思想に合ってます🙂🔒
AI拡張(GitHub Copilot / OpenAI Codex)を使う時の「レビュー観点」🤖🧠✅
AIにDockerfileを書かせると、USER が抜けることが結構あります😂
なので、生成物を見たらここだけは必ずチェック!👀
USER nodejsが 最終ステージ(実行ステージ)にある?- ユーザー作成(
adduser/useradd)がある? /appの所有者が揃ってる?(chownorCOPY --chown)- 変な “万能権限” を付けてない?(777、
sudo導入、など) - ついでに:秘密をDockerfileに直書きしてない?(次のSecrets章で本格的にやるやつ)🔑💥
章末ミニ演習🎓✨(15分でできる)
演習A:root→非rootの差を体験する🙂↔️😱
whoami/idを確認/root/test.txtに書こうとして失敗するのを確認(非rootだと当然)/app/tmp.txtに書けるように/appの権限を整える- 最終的に「アプリが必要な場所だけ書ける」状態にする✅
演習B:事故の芽を潰す(Dockerfileレビューごっこ)🕵️♂️🔎
AIに「Node+TSのDockerfile作って」と頼む → その出力を見て
USERがないなら追加COPY --chownがないなら追加chmod 777があったら削除 …まで直せたら勝ち🏆😄
まとめ🎉
- rootで動かさないだけで、被害半径がかなり縮む🙂✂️
- やることは
USERと 書き込み場所の整理が中心📁 - Composeの
user:は便利だけど、まずは Dockerfileで安全デフォルトが王道🧱 - AI時代は「
USERが入ってるか」をレビューの最優先項目にしよう🤖✅
次章(第12章)は、この章で出てきた “書き込み場所どこにする問題” を、初心者でも迷わない形に設計していきます🧺🗂️✨