Dockerコンテナ入門:プロセス・隔離・ネットワークを“動く単位”として理解する

はじめに

Dockerを触り始めたとき、多くの人は「イメージをpullして、runして、動いた!」で満足できます。でも実務で困り始めるのはその次で、たとえばこんな場面です。

  • docker runしたのに、すぐコンテナが落ちる(exitしてる)
  • ログはどこ?プロセスは何が動いてる?
  • ネットワーク越しに繋がらない、ポートが開かない
  • 環境変数・ボリューム・ユーザー権限の設計が分からない
  • 「コンテナ=軽量VM」だと思っていたら、理解がズレて事故る

コンテナは“仮想マシン”ではなく、ホストOS上のプロセスを隔離して動かす仕組みです。だからこそ、プロセスの扱い、ファイルシステム、ネットワーク、ライフサイクル、ログ、シグナル……このあたりが分かると一気に運用が楽になります。

この記事では、Dockerコンテナを「コマンドを覚える」ではなく、どういう挙動の単位なのかとして理解し、現場で詰まりやすいポイントを手を動かしながら掴むことを目的にします。


座学

1) コンテナは“隔離されたプロセス”

コンテナは、ざっくり言うと「ホストOSの上で動くプロセスに対して、隔離(名前空間)と制限(cgroups)をかけたもの」です。
つまりコンテナの中で動いているのは、VMの中のOSではなく ホスト上のプロセスです。

この認識があると、次のことが自然に理解できます。

  • コンテナが落ちるのは、PID 1(メインプロセス)が終了したから
  • docker stopは“電源断”ではなく、シグナル送信→猶予→強制終了
  • ログはファイルに勝手に保存されるのではなく、基本は 標準出力/標準エラーに流す思想

2) PID 1の責務:落ちない・正しく終わる

コンテナは“メインプロセスが終了したらコンテナも終了”です。
ここで重要なのが PID 1 の扱いです。

  • PID 1がシグナル(SIGTERMなど)を受け取って正しく終了できないと、停止が遅い/強制終了になりやすい
  • 子プロセスを生成するアプリだと、ゾンビプロセス回収(reap)の問題が出る場合がある

実務では、Node/Go/Javaなどでも「コンテナで動かす」前提の起動方法(foregroundで動かす、SIGTERM対応する)にすると安定します。
また、必要なら --init を付けて軽量initを挟むのも有効です(ゾンビ回収やシグナル伝播を助ける)。

3) ファイルシステム:書ける場所と残る場所

コンテナのファイルシステムは「イメージ+実行時の書き込みレイヤ」です。
停止して作り直せば、書き込みは基本的に消えます。

  • 残したいデータ:ボリューム(またはbind mount)へ
  • 残さない前提のデータ:コンテナ内(tmpやキャッシュなど)へ

この線引きを曖昧にすると、DBのデータが消える・ログが消える・アップロードが消える、のような事故につながります。

4) ネットワーク:localhostの罠

コンテナ内の localhost はコンテナ自身です。
ホストの localhost とは別物なので、ここが混乱ポイントです。

  • ホスト → コンテナ:-p 8080:80 のようにポート公開が必要
  • コンテナ → ホスト:Mac/Windowsは host.docker.internal が便利なことが多い
  • コンテナ同士:同じネットワークなら サービス名で名前解決できる(Composeが強い)

5) ログは“stdoutへ”が基本

Dockerは、コンテナの標準出力/標準エラーをログとして扱います。
docker logs で見られるのはそのためです。

  • まずはアプリのログをstdoutへ出す
  • その上でログドライバや収集基盤へ送る(必要なら)

「コンテナ内にログファイルを出す」設計もできますが、運用が難しくなりがちです(ローテーション、永続化、収集など)。


ハンズオン

ここからは “コンテナの実態” を、短いコマンドで確認していきます。
(ローカルにDockerが入っている前提。どのOSでもだいたい動きます)

1) コンテナが落ちる理由=メインプロセスが終わる

まずは「一瞬で落ちる」例。

docker run --rm alpine echo "hello"

これは echo が終わったのでコンテナも終わります。
次に、動き続ける例。

docker run --rm -it alpine sh

sh が動き続けるのでコンテナも生き続けます。
別ターミナルで状態確認。

docker ps
docker ps -a

ここで「コンテナ=プロセス」という感覚をつかみます。

2) PID 1を観察する(何がメインプロセスか)

シェルに入って確認します。

docker run --rm -it alpine sh

中で実行:

ps

だいたい sh がPID 1になっているはずです。
このPID 1が終了したらコンテナも終了します。

3) stopとシグナルを体感する

別のコンテナを起動して、止め方を観察します。

docker run --name demo-stop -d nginx:alpine
docker ps

停止:

docker stop demo-stop

Dockerは基本的にSIGTERM→猶予→SIGKILLの流れです。
猶予を短くしてみます。

docker run --name demo-stop2 -d nginx:alpine
docker stop -t 1 demo-stop2

「プロセスが正しく終了できるか」が停止の品質を決める、というのがここで実感できます。

4) ログはどこに出る?(stdoutを見る)

ログ確認:

docker run --rm -d --name demo-log nginx:alpine
docker logs demo-log --tail 20

次に、標準出力に自前ログを出すコンテナ例:

docker run --rm --name demo-stdout alpine sh -c 'i=0; while true; do i=$((i+1)); echo "tick $i"; sleep 1; done'

別ターミナルで:

docker logs -f demo-stdout

この“stdoutに流す”設計が、運用をシンプルにします。

5) ポート公開と“localhostの別物感”

nginxを起動して、ホストからアクセスできるようにします。

docker run --rm -d --name demo-web -p 8080:80 nginx:alpine

ブラウザで http://localhost:8080 にアクセス。
次に、コンテナの中から localhost を叩くとどうなるか。

docker exec -it demo-web sh

中で:

wget -qO- http://localhost:80 | head

これは「コンテナ自身のnginx」を見ています。
この感覚がないと、API接続で “localhost地獄” になります。

6) ボリュームで“消えない”を体感する

永続化の感覚をつかみます。

docker volume create demo-data
docker run --rm -it -v demo-data:/data alpine sh

中で:

echo "persist" > /data/hello.txt
exit

もう一度別コンテナで見ます。

docker run --rm -it -v demo-data:/data alpine sh -c "cat /data/hello.txt"

コンテナを作り直しても残っている=ボリュームの役割、が体感できます。
逆に /tmp やコンテナ内の適当な場所に書いたら消える、も合わせて試すと理解が固まります。

7) ありがちな運用の型(最小ルール)

最後に、運用で事故を減らすための“型”をまとめます。

  • 1コンテナ1責務(無理に全部詰めない)
  • ログはstdoutへ
  • 永続化はボリュームへ(コンテナ内に頼らない)
  • 停止できるプロセス設計(SIGTERMで落ちる)
  • “localhost”の意味を常に意識する
  • 必要なら --init を使う(子プロセスを扱う場合)

まとめ

Dockerコンテナは「軽量VM」ではなく、隔離されたプロセスです。
この前提に立つと、コンテナの挙動が読みやすくなります。

  • コンテナはPID 1が死ねば終わる(だから“落ちる”理由が説明できる)
  • stopはシグナルで止める(だからアプリ側の終了設計が効く)
  • ファイルは基本消える(だから永続化はボリュームへ)
  • ネットワークは別世界(だからlocalhostに注意)
  • ログはstdoutへ(だから収集・運用が単純になる)

コマンドを暗記するより、「コンテナはどういう単位か」を理解しておくと、障害対応も設計も速くなります。


投稿日

カテゴリー:

投稿者:

タグ:

コメント

コメントを残す

メールアドレスが公開されることはありません。 が付いている欄は必須項目です