2026年5月25日

2026年5月25日

DockerのUSER命令でセキュリティを強化する方法|非rootユーザーの作り方と権限エラーの対処

Docker のコンテナは、Dockerfile で指定しない限り root ユーザーでプロセスを動かします。Dockerfile に USER 命令を書いて専用の一般ユーザーに切り替えると、アプリに脆弱性があったときに攻撃者が得られる権限を小さくできます。この記事では、ユーザーの作り方、USER を書く位置、切り替え後に起きやすい権限エラーの直し方、USER だけでは足りない部分の補い方までを説明します。

USER 命令が何を変えるのか

USER は、それ以降の RUN・CMD・ENTRYPOINT を実行するユーザー(とグループ)を指定する命令です。書式は USER <ユーザー名>[:<グループ名>] または USER <UID>[:<GID>] です。

重要なのは「コンテナの中の root は、特別な設定をしていなければホストの root と同じ UID 0 で動いている」という点です。Docker は名前空間や cgroup、ケーパビリティの制限で隔離していますが、コンテナからの脱出につながる脆弱性や、ホストのディレクトリを誤ってマウントした場合には、root であることが被害を広げます。一般ユーザーで動かしておけば、仮にアプリが乗っ取られても書き換えられる範囲は限られます。

一方で、USER はコンテナを起動する人が docker run --user root で上書きできる設定です。「既定で安全に動く状態にする」ための設定であり、悪意ある利用者を止める壁ではないと理解しておいてください。

専用ユーザーを作って切り替える

ベースイメージによってユーザー作成コマンドが異なります。Debian・Ubuntu 系の例です。

# syntax=docker/dockerfile:1
FROM node:22-bookworm-slim

WORKDIR /app

# 依存関係のインストールは root のまま行う
COPY package.json package-lock.json ./
RUN npm ci --omit=dev

# 専用ユーザーを UID/GID を固定して作成(ログインシェルなし)
RUN groupadd --system --gid 10001 app \
 && useradd --system --uid 10001 --gid app --no-create-home --shell /usr/sbin/nologin app

# アプリのコードは root 所有のまま(実行ユーザーからは読み取りのみ)
COPY . .

# 書き込みが必要なディレクトリだけ app の所有にする
RUN mkdir -p /app/tmp && chown app:app /app/tmp

USER 10001:10001
EXPOSE 8080
CMD ["node", "server.js"]

Alpine 系では BusyBox のコマンドを使います。

FROM alpine:3.20
RUN addgroup -S -g 10001 app && adduser -S -D -H -u 10001 -G app app
USER 10001:10001

この例で意識しているポイントは次のとおりです。

  • **パッケージのインストールなど root が要る処理は USER より前に置く。** USER の後ろで apt-get install を実行すると権限エラーになります。
  • UID/GID を数値で固定する。 名前だけで作ると、ベースイメージの更新で割り当て番号が変わることがあります。ボリュームの所有者を合わせるときにも番号が決まっているほうが扱いやすくなります。
  • **USER には数値で書く。** Kubernetes の runAsNonRoot: true は、イメージの USER が名前だと root かどうかを判定できず、起動を拒否します。数値にしておくとそのまま検証が通ります。
  • アプリのコードは実行ユーザーから書き込めないようにする。 コードを root 所有のままにしておけば、乗っ取られてもコードの改ざんはできません。書き込みが必要なディレクトリ(キャッシュやアップロード先)だけを app の所有にします。全体を app の所有にしたい場合は COPY --chown=app:app . . と書けますが、その分だけ守りは弱くなります。

なお、公式 Node.js イメージには最初から node という一般ユーザーが用意されているように、ベースイメージによっては既存ユーザーを使えます。distroless 系イメージにも非 root 用のタグが用意されているものがあります。使うイメージの説明ページを確認してください。

切り替え後に起きやすいエラーと対処

一般ユーザーに切り替えると、それまで root だから通っていた操作が失敗するようになります。よくある症状を、起きやすい順に並べます。

1. Permission denied でファイルを書けない

最も多いのがこれです。ログに EACCES: permission denied や Permission denied が出たら、どのパスへの書き込みかを確認します。

# コンテナ内で実行ユーザーと対象ディレクトリの所有者を確認
docker exec -it <コンテナ名> id
docker exec -it <コンテナ名> ls -ld /app /app/tmp

原因はたいてい、COPY したファイルが root 所有のままになっていることです。書き込みが必要なディレクトリだけを COPY --chown= で所有者付きにするか、RUN mkdir -p /app/tmp && chown app:app /app/tmp を USER の前に入れます。

2. ボリュームやバインドマウントに書き込めない

バインドマウントでは、ホスト側のファイルの UID がそのままコンテナ内に見えます。ホストで UID 1000 のユーザーが作ったフォルダに、コンテナ内の UID 10001 は書き込めません。開発環境なら、ホスト側のフォルダの所有者を合わせるか、docker run --user "$(id -u):$(id -g)" で実行ユーザーをホストに合わせます。権限を 777 にして解決するのは避けてください。

名前付きボリュームは、空の状態で初めてマウントされたとき、イメージ内の同じパスの内容と所有者がコピーされます。そのため、イメージ側でディレクトリを作って chown しておくと、ボリュームも同じ所有者で始まります。すでに root 所有で作られたボリュームは自動では直らないので、一度だけ root で chown するか、作り直します。

3. 80番や443番で待ち受けられない

Linux では1024未満のポートを使うのに特権が必要な場合があります。Docker の既定設定ではコンテナ内でも使えることがありますが、ホストネットワークを使う場合や実行環境によっては拒否されます。アプリは 8080 などで待ち受け、-p 80:8080 でホスト側の番号に変換する構成にしておくと、環境による差を気にせずに済みます。

4. 起動時に root が必要な処理がある

公式イメージの中には、起動スクリプトを root で動かして初期化をした後、一般ユーザーに切り替えてからアプリを起動するものがあります。WordPress 公式イメージでも、Apache の親プロセスは root で起動し、リクエストを処理する子プロセスは www-data で動きます。自作イメージで同じことをしたい場合は、gosu や su-exec を使ってエントリポイントから権限を落とす方法が知られています。ただし構成が複雑になるので、まずは初期化処理をビルド時に済ませて、最初から一般ユーザーで起動できないかを検討してください。

本当に一般ユーザーで動いているか確認する

設定したつもりでも、Compose や起動コマンドで上書きされていることがあります。次の3点で確認します。

# イメージに設定されている USER
docker image inspect --format '{{.Config.User}}' my-app:latest

# 実際に動いているプロセスのユーザー
docker exec <コンテナ名> id

# ホストから見たプロセスの UID
docker top <コンテナ名> -o pid,uid,cmd

.Config.User が空なら root で動きます。Compose ファイルに user: があればそちらが優先されるため、両方を確認してください。

USER と組み合わせたい設定

USER で root を避けても、コンテナに与えられているケーパビリティや書き込み可能な領域はそのままです。次の設定を重ねると、乗っ取られたときにできることをさらに減らせます。

services:
  app:
    image: my-app:1.0.0
    user: "10001:10001"
    read_only: true
    tmpfs:
      - /tmp
    cap_drop:
      - ALL
    security_opt:
      - no-new-privileges:true
  • cap_drop: [ALL] は Linux ケーパビリティを外します。アプリが特定の権限を必要とする場合だけ cap_add で個別に戻します。
  • no-new-privileges は、setuid のプログラムなどを使って実行中に権限を引き上げることを防ぎます。
  • read_only: true はコンテナのファイルシステムを読み取り専用にし、書き込みが必要な場所だけ tmpfs やボリュームで用意します。

いずれも、アプリが想定外の動作をしないか、ステージング環境で動作確認してから本番に適用してください。さらにホスト側で守りを固めたい場合は、Docker デーモン自体を一般ユーザーで動かす Rootless モードや、コンテナ内の UID をホストの別の UID に対応付ける userns-remap という選択肢もあります。これらは対応する機能や制約が Docker のバージョンによって変わるため、導入時は公式ドキュメントで最新の状況を確認してください(2026年10月現在)。

まとめ

  • Dockerfile に USER を書かないとコンテナは root で動きます。専用ユーザーを作って切り替えるのが基本です。
  • root が必要なインストール処理は USER より前に置き、UID/GID は数値で固定します。
  • USER は数値で書くと、Kubernetes の runAsNonRoot でもそのまま検証できます。
  • 権限エラーは、書き込み先ディレクトリの所有者とマウント元の UID を確認して直します。777 での回避は避けます。
  • cap_drop・no-new-privileges・read_only を組み合わせ、USER だけに頼らない構成にします。

お気軽にご相談ください

お見積りへ お問い合わせへ
▲