Dockerコンテナをroot権限なしで動かす方法は1つではありません。「デーモン自体をrootで起動しない」アプローチと「コンテナ内プロセスをrootで動かさない」アプローチの2軸があり、それぞれ目的と設定箇所が異なります。本稿ではRootlessモードからDockerfile USER命令、Kubernetes SecurityContextまで、Docker non-root実行に関わるすべての手法を体系的に整理し、実運用で遭遇するパーミッション問題の解決策までカバーします。

Dockerにおける「non-root」の2つの意味

「Docker non-root」と検索したとき、求めている情報は大きく2種類に分かれます。

1. デーモン側のnon-root(Rootlessモード)

Docker Engine(dockerdデーモン)自体を一般ユーザーの権限で起動する仕組みです。ホストOS上でdockerdがroot権限を持たないため、デーモンの脆弱性を突かれてもホスト全体への影響を限定できます。Linux kernel の user namespace を活用し、コンテナ内のrootをホスト上の非特権UIDにマッピングすることで実現しています。

2. コンテナ側のnon-root(USER命令・–userフラグ)

コンテナ内で実行されるアプリケーションプロセスのユーザーをrootから一般ユーザーに切り替える手法です。Dockerfileの USER 命令や docker run --user フラグで制御します。デーモン側がrootで動いていても、コンテナ内プロセスの権限を最小化できます。

日本語圏の技術記事はRootlessモード(デーモン側)の話題に偏る傾向がありますが、本番環境のセキュリティ強化にはコンテナ側のユーザー制御も同等以上に重要です。両方を組み合わせることで多層防御が実現します。

なぜDockerをroot権限なしで使うべきか

Dockerデーモンとコンテナの双方をrootで実行し続けるリスクは、実際のCVE(脆弱性報告)が繰り返し示しています。

コンテナブレイクアウト攻撃の実例

2024年に公開されたCVE-2024-21626は、コンテナランタイムruncのファイルディスクリプタリークを悪用し、コンテナからホストファイルシステムへ脱出できる深刻な問題でした(出典: GitHub Advisory)。2025年11月にもruncに3件の重大な脆弱性(CVE-2025-31133、CVE-2025-52565、CVE-2025-52881)が公開され、masked pathsやデバイスファイルのbind-mount処理の不備を突いたコンテナ脱出が可能であることが判明しています(出典: Sysdig)。

さらにDocker Desktop固有の問題として、CVE-2025-9074(CVSS 9.3)ではコンテナからホストファイルへの不正アクセスが可能でした(出典: Rescana)。

root実行が危険な3つの理由

リスクroot実行時の影響non-root化による軽減
コンテナ脱出ホストのroot権限を奪取されるuser namespaceにより非特権UIDにマッピング
ボリューム経由のファイル改ざんホストのファイルシステムを自由に操作可能書き込み権限がコンテナ内ユーザーに限定される
特権ポートの濫用1024番未満のポートを自由にバインド可能バインドにはCAP_NET_BIND_SERVICEが必要

コンテナ内のrootはホストのrootと同一のUID 0です。user namespaceを使わない限り、コンテナブレイクアウトに成功した時点でホスト全体の管理者権限が手に入ります。non-root実行は「万が一突破されたとき」の被害を最小化する最も基本的な対策です。

方法1 — Rootlessモードでデーモンをroot以外で起動する

動作の仕組み

Rootlessモードでは、以下のコンポーネントが連携してrootなしのコンテナ実行を実現します。

  • user namespace: Linux kernelの機能。コンテナ内のUID 0をホスト上の非特権UID(例: 100000)にマッピングします
  • RootlessKit: user namespaceの作成やネットワーク設定を担当するヘルパーツール
  • slirp4netns / pasta: ユーザー空間ネットワークスタック。rootなしでコンテナのネットワーキングを実現します。Docker Engine 25.0以降ではpastaドライバも利用可能で、slirp4netnsより高いスループットが報告されています

通常のDockerではdockerdがroot権限で動作しますが、Rootlessモードではdockerdプロセス自体が一般ユーザーの権限で稼働します。

セットアップ手順(Ubuntu / Debian)

# 前提パッケージのインストール
sudo apt-get install -y uidmap dbus-user-session

# rootful Dockerが動作中なら停止
sudo systemctl disable --now docker.service docker.socket

# /etc/subuid と /etc/subgid にサブUID/GIDが設定されていることを確認
# 最低65,536個の範囲が必要
grep $(whoami) /etc/subuid
# 出力例: testuser:100000:65536

# Rootlessモードのセットアップスクリプトを実行
dockerd-rootless-setuptool.sh install

# 環境変数を永続化(~/.bashrc に追記)
echo 'export PATH=/usr/bin:$PATH' >> ~/.bashrc
echo 'export DOCKER_HOST=unix:///run/user/$(id -u)/docker.sock' >> ~/.bashrc
source ~/.bashrc

# ログイン維持を有効化(再起動後もデーモンを自動起動するため)
sudo loginctl enable-linger $(whoami)

# 動作確認
docker info --format '{{.SecurityOptions}}'
# 出力に "rootless" が含まれていれば成功

/etc/subuid/etc/subgid にユーザーのエントリが存在しない場合は、管理者に依頼して追加する必要があります。

Docker Composeとの併用

Rootlessモードでは通常の docker compose コマンドがそのまま動作します。ただし DOCKER_HOST 環境変数が正しく設定されている必要があります。

# DOCKER_HOSTが設定されているか確認
echo $DOCKER_HOST
# unix:///run/user/1000/docker.sock のように表示される

# 通常通りComposeを利用
docker compose up -d

制約と注意点

Rootlessモードにはいくつかの制約があります。「Docker rootless デメリット」として検索されやすい内容を整理します。

制約詳細回避策
ストレージドライバcgroup v1環境ではoverlayfsが使えない場合があるcgroup v2への移行が推奨
特権ポート1024番未満のポートにデフォルトでバインド不可sysctl net.ipv4.ip_unprivileged_port_start=0 またはリバースプロキシ利用
ネットワーク性能slirp4netnsはNATを経由するためオーバーヘッドがあるDocker 25.0以降でpastaドライバを使用
AppArmor / SELinux一部のセキュリティモジュールと互換性問題プロファイルの個別調整が必要
–net=hostuser namespaceとの併用に制限があるブリッジネットワーク利用に切り替え
コンテナ内でのpingrawソケット作成にroot権限が必要sysctl net.ipv4.ping_group_range の設定変更

本番環境への導入前に、利用するすべてのコンテナイメージで動作検証を行うことが重要です。

方法2 — Dockerfileでコンテナ内ユーザーをrootから切り替える

Rootlessモードがデーモン側の対策であるのに対し、USER 命令はコンテナ内プロセスの権限を制御する手法です。

USER命令の基本パターン

FROM ubuntu:24.04

# rootの状態で必要なパッケージをインストール
RUN apt-get update && apt-get install -y --no-install-recommends \
    curl \
    && rm -rf /var/lib/apt/lists/*

# 非rootユーザーとグループを作成
RUN groupadd --gid 1001 appgroup \
    && useradd --uid 1001 --gid appgroup --shell /bin/bash --create-home appuser

# アプリケーションディレクトリの準備
WORKDIR /app
COPY --chown=appuser:appgroup . .

# 非rootユーザーに切り替え
USER appuser

CMD ["./start.sh"]

USER 命令以降の RUNCMDENTRYPOINT はすべて指定したユーザーで実行されます。パッケージのインストールなどroot権限が必要な操作は USER 命令より前に記述してください。

Alpine / Debian / Python向けの実践例

ベースイメージによってユーザー作成コマンドが異なります。

Alpine Linux(adduser):

FROM alpine:3.21
RUN addgroup -g 1001 appgroup \
    && adduser -u 1001 -G appgroup -D -h /home/appuser appuser
WORKDIR /app
COPY --chown=appuser:appgroup . .
USER appuser
CMD ["./app"]

Python(既存のnobodyユーザー利用の例):

FROM python:3.13-slim
RUN groupadd --gid 1001 appgroup \
    && useradd --uid 1001 --gid appgroup --create-home appuser
WORKDIR /app
COPY --chown=appuser:appgroup requirements.txt .
RUN pip install --no-cache-dir -r requirements.txt
COPY --chown=appuser:appgroup . .
USER appuser
EXPOSE 8000
CMD ["gunicorn", "app:app", "--bind", "0.0.0.0:8000"]

マルチステージビルドでの安全なユーザー切り替え

マルチステージビルドでは、ビルドステージをrootで実行し、最終ステージで非rootユーザーに切り替えるのが定石です。

FROM node:20-slim AS build
WORKDIR /app
COPY package*.json ./
RUN npm ci --only=production

FROM node:20-slim
RUN groupadd --gid 1001 appgroup \
    && useradd --uid 1001 --gid appgroup --shell /bin/bash --create-home appuser
WORKDIR /app
COPY --from=build --chown=appuser:appgroup /app/node_modules ./node_modules
COPY --chown=appuser:appgroup . .
ENV HOME=/home/appuser
USER appuser
EXPOSE 3000
CMD ["node", "server.js"]

ビルドステージで生成した成果物を COPY --from で取り込む際に --chown を付与することで、ファイル所有権の問題を防ぎます。

COPY –chownでファイル所有権を正しく設定する

COPYADD 命令で --chown を指定しないと、ファイルの所有者はrootになります。USER 命令で非rootユーザーに切り替えた後、アプリケーションが自身のファイルを読み取れないトラブルの原因になります。

# NG: 所有者がrootのままになる
COPY . /app

# OK: 所有者を非rootユーザーに変更
COPY --chown=appuser:appgroup . /app

方法3 — docker run / docker-compose.ymlでランタイム指定する

Dockerfileを変更できない場合や、同じイメージを異なるユーザーで実行したい場合にはランタイムでの指定が有効です。

–userフラグの使い方

# UID:GIDを数値で指定
docker run --user 1001:1001 nginx:latest

# ユーザー名で指定(コンテナ内の/etc/passwdにエントリが必要)
docker run --user appuser myapp:latest

# 現在のホストユーザーのUID/GIDで実行
docker run --user "$(id -u):$(id -g)" myapp:latest

--user フラグはDockerfile内の USER 命令を上書きします。イメージがrootで動作するよう作られている場合、パーミッションエラーが発生する可能性があるため注意が必要です。

docker-compose.ymlでのuser指定

services:
  app:
    image: myapp:latest
    user: "1001:1001"
    volumes:
      - app-data:/app/data

  nginx:
    image: nginx:latest
    user: "1001:1001"
    ports:
      - "8080:8080"
    volumes:
      - ./nginx.conf:/etc/nginx/nginx.conf:ro

volumes:
  app-data:

Docker Compose でuser指定する場合、ボリュームの所有権がコンテナ内ユーザーのUID/GIDと一致している必要があります。名前付きボリュームの場合は初回マウント時にDockerが自動的に所有権を設定しますが、バインドマウントではホスト側で事前に権限を調整してください。

方法4 — Kubernetes SecurityContextでnon-rootを強制する

Kubernetesではクラスタレベルでnon-root実行を強制する仕組みが整っています。

runAsNonRootとrunAsUser

apiVersion: v1
kind: Pod
metadata:
  name: secure-app
spec:
  securityContext:
    runAsNonRoot: true
    runAsUser: 1001
    fsGroup: 1001
  containers:
    - name: app
      image: myapp:latest
      securityContext:
        allowPrivilegeEscalation: false
        capabilities:
          drop: ["ALL"]
        readOnlyRootFilesystem: true
  • runAsNonRoot: true — コンテナがUID 0で起動しようとした場合、Podの起動を拒否します
  • runAsUser: 1001 — 明示的にUIDを指定し、Dockerイメージ側の USER 設定を上書きします
  • fsGroup: 1001 — ボリュームにマウントされたファイルのグループ所有権を設定します

runAsNonRoot を設定している場合、DockerイメージのUSER命令で非rootユーザーが指定されているか、Pod specで runAsUser に0以外の値を明示する必要があります。指定がないと container has runAsNonRoot and image has non-numeric user エラーでPodが起動に失敗します。

PodSecurityStandards(Restricted)との関連

Kubernetes v1.25以降、PodSecurityStandards(PSS)が正式機能としてPod Security Admission Controllerに統合されています。3段階のプロファイルのうち、Restrictedプロファイルでは以下がすべて必須です(出典: Kubernetes公式ドキュメント)。

  • runAsNonRoot: true
  • allowPrivilegeEscalation: false
  • capabilities.drop: ["ALL"]
  • seccompプロファイルの指定(RuntimeDefault または Localhost)

Restrictedプロファイルの適用はNamespace単位で行います。

apiVersion: v1
kind: Namespace
metadata:
  name: production
  labels:
    pod-security.kubernetes.io/enforce: restricted
    pod-security.kubernetes.io/warn: restricted

SOC 2やPCI-DSSなどのコンプライアンスフレームワークに準拠する本番クラスタでは、Restrictedプロファイルの適用が推奨されています。

non-root実行時に起きやすいパーミッション問題と対処法

non-root化で最も頻繁に遭遇するのがパーミッション関連のエラーです。代表的なケースと対処法を整理します。

ディレクトリ書き込みエラー

Permission denied: '/app/logs/app.log'

Dockerfile内で書き込みが必要なディレクトリの所有権を事前に設定します。

RUN mkdir -p /app/logs /app/tmp \
    && chown -R appuser:appgroup /app/logs /app/tmp
USER appuser

特権ポート(1024未満)にバインドできない

非rootユーザーは1024番未満のポートにバインドする権限を持ちません。

対処法1: アプリケーションのリッスンポートを変更する

# Nginxの場合、設定で8080番ポートを使う
EXPOSE 8080

対処法2: CAP_NET_BIND_SERVICE capabilityを付与する

docker run --cap-add=NET_BIND_SERVICE --user 1001:1001 myapp:latest

ただしcapabilityの追加はセキュリティリスクを高めるため、ポート変更が可能ならそちらを優先してください。

ボリュームマウントの権限不整合

ホスト側のディレクトリをバインドマウントする場合、ホストのファイル所有者とコンテナ内ユーザーのUIDが一致していないと読み書きできません。

# ホスト側でディレクトリの所有権を調整
sudo chown -R 1001:1001 ./data

# コンテナ側のUIDと合わせてマウント
docker run --user 1001:1001 -v ./data:/app/data myapp:latest

名前付きボリュームを使う場合は、init containerやentrypointスクリプトで所有権を調整する手法もあります。

パッケージインストールが失敗する

apt-getapk add などのパッケージマネージャーはroot権限が必要です。

# 正しい順序: root状態でパッケージをインストールしてからユーザーを切り替える
FROM ubuntu:24.04
RUN apt-get update && apt-get install -y --no-install-recommends \
    ca-certificates \
    && rm -rf /var/lib/apt/lists/*

RUN groupadd --gid 1001 appgroup \
    && useradd --uid 1001 --gid appgroup --create-home appuser

USER appuser
# これ以降はapt-getを実行できない

USER 命令の前にすべてのシステムレベルの操作を完了させてください。

RootlessモードとPodmanの比較

Docker Rootlessモードと同じく非root実行を実現するツールとしてPodman(検索ボリューム: 4,400)があります。設計思想が異なるため、用途に応じた選択が必要です。

比較項目Docker RootlessモードPodman
デフォルト動作rootful(Rootlessは追加設定が必要)デフォルトでrootless
デーモンの有無dockerdデーモンが常駐デーモンレス(fork/execモデル)
OCI準拠OCI互換完全OCI準拠
Docker Compose互換標準で利用可能podman-composeまたは podman compose(外部プロバイダ経由)
Dockerfile互換完全互換ほぼ完全互換(Containerfileも利用可能)
user namespaceRootlessモード有効時のみデフォルトで有効
systemdとの統合systemd user serviceとして管理podman generate systemd で生成可能
学習コストDocker経験者はそのまま移行可能CLIはDockerとほぼ同じだが一部差異あり

Podmanはrootless動作がデフォルトであり、デーモンレスアーキテクチャのためリソース消費が小さいという利点があります。一方、既存のDocker Composeワークフローとの互換性や、Docker専用の周辺ツール(Docker Desktop、Docker Buildx等)が必要な場合はDocker Rootlessモードが現実的な選択肢です。

non-root実行が正しく動作しているか検証する

設定が完了したら、実際にnon-rootで動作していることを確認します。

基本的な検証コマンド

# コンテナ内の実行ユーザーを確認
docker run --rm myapp:latest whoami
# appuser と表示されれば成功

# UID/GIDの詳細を確認
docker run --rm myapp:latest id
# uid=1001(appuser) gid=1001(appgroup) groups=1001(appgroup)

# rootでないことを確認
docker run --rm myapp:latest sh -c 'if [ "$(id -u)" -eq 0 ]; then echo "FAIL: running as root"; else echo "OK: running as non-root (UID=$(id -u))"; fi'

capabilityの確認

# コンテナのcapabilityを確認
docker run --rm --cap-drop=ALL myapp:latest cat /proc/1/status | grep Cap

# Dockerの全体セキュリティ設定を確認(Rootlessモードの場合)
docker info --format '{{.SecurityOptions}}'
# [name=seccomp,profile=builtin name=rootless] のように表示される

Kubernetes環境での検証

# Pod内のユーザーを確認
kubectl exec secure-app -- id
# uid=1001 gid=1001 groups=1001

# rootで起動しようとして拒否されることを確認
kubectl run test-root --image=nginx:latest --restart=Never \
  --overrides='{"spec":{"securityContext":{"runAsNonRoot":true}}}'
# Error: container has runAsNonRoot and image will run as root

セキュリティを高める追加のベストプラクティス

non-root実行に加え、以下の対策を併用することで多層的なセキュリティが実現します。

read-onlyファイルシステム

docker run --read-only --tmpfs /tmp --tmpfs /var/run \
    --user 1001:1001 myapp:latest

コンテナのファイルシステムを読み取り専用にすることで、不正なファイル書き込みやバイナリの改ざんを防止します。一時ファイルが必要な場合は --tmpfs でtmpfsをマウントしてください。

capabilityの最小化

# docker-compose.ymlでcapabilityをすべてドロップ
services:
  app:
    image: myapp:latest
    user: "1001:1001"
    cap_drop:
      - ALL
    security_opt:
      - no-new-privileges:true
    read_only: true
    tmpfs:
      - /tmp

cap_drop: ALL でLinux capabilityをすべて削除し、アプリケーションが必要とするものだけを cap_add で個別に追加する方針が最も安全です。no-new-privileges は子プロセスが親プロセスより高い権限を取得することを防ぎます。

seccompとAppArmor

Dockerはデフォルトでseccompプロファイルを適用し、コンテナ内から発行可能なシステムコールを制限しています。カスタムプロファイルを使ってさらに厳格化することも可能です。

# カスタムseccompプロファイルを適用
docker run --security-opt seccomp=custom-profile.json \
    --user 1001:1001 myapp:latest

総合的なDockerfile例

上記のベストプラクティスを統合したDockerfileの例を示します。

FROM python:3.13-slim AS build
WORKDIR /app
COPY requirements.txt .
RUN pip install --no-cache-dir --prefix=/install -r requirements.txt

FROM python:3.13-slim
# セキュリティアップデートを適用
RUN apt-get update && apt-get upgrade -y \
    && rm -rf /var/lib/apt/lists/*

# 非rootユーザー作成
RUN groupadd --gid 1001 appgroup \
    && useradd --uid 1001 --gid appgroup --create-home appuser

# アプリケーションファイルの配置
WORKDIR /app
COPY --from=build --chown=appuser:appgroup /install /usr/local
COPY --chown=appuser:appgroup . .

# 書き込み可能ディレクトリの準備
RUN mkdir -p /app/logs && chown appuser:appgroup /app/logs

ENV HOME=/home/appuser
USER appuser
EXPOSE 8000
CMD ["gunicorn", "app:app", "--bind", "0.0.0.0:8000"]

non-root実行はDockerセキュリティの出発点です。Rootlessモードによるデーモン側の保護、USER命令によるコンテナ側の権限最小化、Kubernetes SecurityContextによるクラスタレベルの強制を組み合わせることで、コンテナブレイクアウトや権限昇格のリスクを大幅に低減できます。自身の環境に合った手法を選択し、検証コマンドで動作を確認した上で本番運用に適用してください。