Docker で環境変数をホストからコンテナに渡す方法（ホスト OS 側からゲスト OS に渡す方法各種）

> Docker のコンテナ（ゲスト OS）内のアプリやスクリプトなど、**ホスト OS 側から[システム環境変数](https://ja.wikipedia.org/wiki/%E7%92%B0%E5%A2%83%E5%A4%89%E6%95%B0)をゲスト OS に渡したい**。

つまり macOS や Linux で言うと `export` したシェル変数や `env` コマンドで表示される値を外部から渡したい。`LC_ALL` とか `LANG` とかタイムゾーンとかアクセストークンとか SSH の鍵とか色々。

しかし[「`docker` `compose` `環境変数` `ホストOS` `ゲストOS` `渡す` `方法` `各種`」で Qiita 記事に絞ってググって](https://www.google.com/search?q=site:qiita.com+docker+compose+%E7%92%B0%E5%A2%83%E5%A4%89%E6%95%B0+%E3%83%9B%E3%82%B9%E3%83%88OS+%E3%82%B2%E3%82%B9%E3%83%88OS+%E6%B8%A1%E3%81%99+%E6%96%B9%E6%B3%95+%E5%90%84%E7%A8%AE)も、まとまったものがなかったので、自分のググラビリティとして。

:::note info
**... <sub><sup>ど</sup></sub> ... Docker?**

恐れずに <font color="darkorange">**Docker を一言で説明すると「Linux 環境の閉鎖空間で、Linux のプログラムを実行するもの」**</font>です。

<details><summary>（... ... ん？ macOS や Windows でも使えるんだが？）</summary>

確かに、Mac や Windows でも Docker は使えます。しかし、実際には[仮想マシン（VM）](https://ja.wikipedia.org/wiki/%E4%BB%AE%E6%83%B3%E6%A9%9F%E6%A2%B0)上で Linux 環境を起動して Docker を実行しているだけなのです。

これが「どの環境でも動く」（なぜなら仮想 Linux 環境上で動かしているから）という理屈なのです。

Windows の場合、WSL2 もしくは Hyper-V を必要とするため気付きやすいのですが、macOS の場合は Docker Desktop をインストールするだけで動いちゃうので気付かないで使っている人が多い気がします。

そのため「`docker` は軽量と聞いたのに、そうでもない」という体感があるのは、Win や macOS では VM 上で Linux マシンを起動し、そこでアプリを実行したのと同じだからです。

常に Linux OS（のカーネル）ぶんのメモリは食ってるし、VM を介した速度やパフォーマンスとの違いがありません。Hyper-V は重いからと、たとえ WSL2 上で動かしたとしても、目ん玉が飛び出るような速度が出るわけではありません。

実は Windows の Docker 限定ですが「Windows 環境の閉鎖空間で、Windows のプログラムを実行する」、Docker Windows Container というモードも、あるにはあります。

この場合はローカルの Windows カーネルを使うため、ローカルで Windows アプリを実行したのと大して差がなく実行できます。Windows 以外は触ったことがない（Linux/UNIX もよくわからない）組織では、しれっと、このモードのことを言っていたりします。

[Docker Hub で MS 社が Windows のベースイメージを公開している](https://hub.docker.com/r/microsoft/windows)のを見て、「お！」と思うも、Linux や macOS で動かないと経験された人も多いのではないでしょうか。分かりづらいのですが、よくよく読むとビルドされた同じバージョンの Windows 上、かつ Windows Container モードでしか動かないと書かれています。

しかし、<font color=darkorange>**世間一般でいう Docker は Linux カーネル上で動かす方の Docker がデファクト・スタンダード**</font>となっています。

Docker 社も MS 社も、囲い込みに<ruby>躍起<rt>やっき</rt></ruby>になっており、[名称・制約・仕様の変更も多い](https://learn.microsoft.com/ja-jp/virtualization/windowscontainers/about/)ため、Docker で Windows Container を扱うのは長い目で見てオススメしません。

また、「`docker` は、どの環境でも簡単に実行できると聞いたのに、そうでもない」という体感があるのは、トラブル時の切り分けに意外と高い基礎知識を必要とするからです。

Linux、OS（カーネル）、サーバーなどの基礎知識、コンパイル（アーキテクチャごとにバイナリが異なる理由やRosetta2 が必要な理由）を理解していないといけないなど、意外に敷居が高かったりします。これが、「ゆうて、さほど便利じゃない」と感じる原因だったりします。

<font color=darkorange>**それでも Docker（もしくは Podman）を使うメリットは大きい**</font>です。特に、上記のような **Linux をベースとしたサーバー関連の基礎知識の底上げをするための勉強にはもってこいだから**です。

この記事では、Linux の方の Docker を主体に説明します（詳しくは [Docker の基本](#%E6%89%80%E6%84%9F)参照）。
</details>

:::

## TL; DR <sub><sup>（今北産業）</sup></sub>

1. **`docker compose` を使う場合**（旧 `docker-compose`）<br>オススメは、「外部ファイルに環境変数を用意」しておき `docker-compose.yml` 内で指定してコンテナ起動時に読み込ませる方法です。ホスト OS 側で設定済みの環境変数も指定可能です。<br>`Dockerfile` のビルド時に外部から環境変数を渡したい場合は、`args` 経由で宣言された `ARG` の変数に値を渡せます。[[詳細](#docker-composeyml-%E3%81%AB%E8%A8%98%E8%BC%89%E3%81%97%E3%81%9F%E5%A4%96%E9%83%A8%E3%83%95%E3%82%A1%E3%82%A4%E3%83%AB%E3%81%AE%E7%92%B0%E5%A2%83%E5%A4%89%E6%95%B0%E3%82%92%E6%B8%A1%E3%81%99)]

2. **`docker compose` を使わない場合**（旧 `docker-compose`）<br>オススメは、「外部ファイルに環境変数を用意」しておき `docker run` コマンドの `--env-file` オプションでコンテナ起動時に「絶対パス」で読み込ませる方法です。こちらも、ホスト OS 側で設定済みの環境変数も指定可能です。[[詳細](#%E3%82%B3%E3%83%9E%E3%83%B3%E3%83%89%E3%81%A7%E5%A4%96%E9%83%A8%E3%83%95%E3%82%A1%E3%82%A4%E3%83%AB%E3%82%92%E6%8C%87%E5%AE%9A%E3%81%97%E3%81%A6%E7%92%B0%E5%A2%83%E5%A4%89%E6%95%B0%E3%82%92%E6%B8%A1%E3%81%99)]

3. この記事では `docker run`, `docker build`, `docker compose run`, Dockerfile, docker-compose.yml での「環境変数の渡し方」を説明しています。詳しくは [TS; DR](#ts-dr%E8%A9%B3%E7%B4%B0) を参照ください。なお、[オーケストレーション](https://ja.wikipedia.org/wiki/%E3%82%AA%E3%83%BC%E3%82%B1%E3%82%B9%E3%83%88%E3%83%AC%E3%83%BC%E3%82%B7%E3%83%A7%E3%83%B3_(%E3%82%B3%E3%83%B3%E3%83%94%E3%83%A5%E3%83%BC%E3%82%BF))・ツールについては docker-compose のみで、`docker swarm` や [Kubernetes](https://ja.wikipedia.org/wiki/Kubernetes) などについては言及していません。

Docker のイメージやコンテナに環境変数を渡したい場合、Dockerfile 経由、`docker-compose.yml` 経由、`.env` ファイル経由、コマンド引数経由などがあり、落ち着いて紐解いていけば難しくないのですが、いささか煩雑です。まずは、下記「[環境変数の渡し方の概要と基本構文](#%E7%92%B0%E5%A2%83%E5%A4%89%E6%95%B0%E3%81%AE%E6%B8%A1%E3%81%97%E6%96%B9%E3%81%AE%E6%A6%82%E8%A6%81%E3%81%A8%E5%9F%BA%E6%9C%AC%E6%A7%8B%E6%96%87)」をご覧ください。

:::note info
**ホスト側で設定済みの環境変数が、何をしても渡せない場合**
<sub>Docker 本体や OS のアップデート待ちが発生していないか確認してください（特に Win や macOS 環境の場合）。セキュリティ・アップデートが含まれていると、適用 & 再起動するまでロックされて渡されません。
またビルドやコンテナ起動といった操作をする前に、<font color=darkorange>渡したい環境変数の設定が、そもそも適用されているか `env` コマンドなどで確認してください</font>。
特に VSCode のターミナルから操作する場合です。VSCode に限らず、ターミナルは基本的に起動時の環境変数しか保持していないからです。</sub>
:::

:::note info
**急に `docker compose` と言われても**
<sub>「Docker のコンテナに関して調べにきたのに、急に `docker compose` と言われても」と思った方も多いと思います。詳しくは [TS; DR](#ts-dr%E8%A9%B3%E7%B4%B0) で後述しますが、docker のコンテナを起動する際に、よく使う設定を記述したり、複数のコンテナを記述したりできるコマンドです。`docker compose version` とコマンドを叩いてバージョンが表示されたら使えます。Docker のイメージ作成とコンテナ起動に慣れてきて、毎回の起動（引数の設定）が面倒くさくなりシェルのスクリプトを書きたくなったら、`docker compose` を活用するタイミングです。</sub>
:::

:::note info
**`docker-compose` と `docker compose` の違い**
<sub>`docker-compose` と `docker compose` は基本的に同じものです。`docker-compose` は Python で実装されたもので、`docker compose` は Go 言語で実装された `docker` のサブコマンド（Docker プラグイン）です。`compose` サブコマンドは、`docker-compose` の v2 と同等の機能を提供しますが、`docker` と同じ Go 言語で実装されたものであるため数倍速く、別途 Python も必要としません。**今後は `docker compose` に統一されていきます**が、この記事では `docker-compose` と表記しています。なお、Win、macOS の場合は Docker Desktop に内包されており、Linux 環境の場合は [compose](https://github.com/docker/compose) のバイナリを [Docker プラグインのディレクトリに設置する](https://docs.docker.com/compose/install/linux/)必要があります。お使いのパッケージ・マネージャーで `docker-cli-compose` や `docker-compose-plugin` といったパッケージ名で検索してみてください。</sub>
:::

:::note warn
🐒 &nbsp;【<font color=darkorange><strong>注意点</strong></font>】

環境変数を渡せる先には３箇所あります。「イメージ」「コンテナ」と「設定ファイル（`Dockerfile`）」の３箇所です。

<ol>
<li>「イメージ」に渡す場合は、変数はイメージに焼き付け（埋め込み）ます。<br>そのため、そのイメージから作成されたコンテナ全てに反映されるため、値に注意します。イメージに焼き付けたくない値や上書きしたい値はコンテナ起動時に渡します。</li>
<li>「コンテナ」に渡す場合は、イメージから「コンテナ」を作成 or 起動する際に渡します。<br>コンテナをイメージ化しない限り、コンテナが消滅すると渡した値も消えます。</li>
<li>「設定ファイル」とは、Docker イメージのビルド時に使う <code>Dockerfile</code> です。<br><code>Dockerfile</code> 内で定義した <code>ARG</code>（<code>Dockerfile</code> 内変数）に値を外部から渡せます。Dockerfile 内で、<code>ARG</code> の変数を、さらに <code>ENV</code> に代入すると「イメージ内の環境変数」として埋め込むことができます。しかし、<font color="darkorange"><code>RUN</code> 内で <code>ARG</code> の値を書き換えても、他のステップには反映されない</font>ので注意します。同様に、<code>docker-compose.yml</code> から環境変数を <code>Dockerfile</code> のビルド時に渡したい場合も、<code>args</code> 経由で <code>ARG</code> 変数に渡し <code>Dockerfile</code> 内で利用したり、<code>ENV</code> に代入してイメージに焼き付けることができます。詳しくは <a href="#ts-dr%E8%A9%B3%E7%B4%B0">TS; DR</a> 参照。</li>
</ol>

しかし、<strong>どのような方法であっても環境変数で渡すということは、同じコンテナ内にあるサードパーティ製のライブラリからも参照できてしまう</strong>ということに留意します。

例えサードパーティに悪意がなくても、デバッグ情報をダンプ出力する際に、環境変数も出力することが多くあるためです。つまり、<a href="https://ja.wikipedia.org/wiki/%E7%B6%99%E7%B6%9A%E7%9A%84%E3%82%A4%E3%83%B3%E3%83%86%E3%82%B0%E3%83%AC%E3%83%BC%E3%82%B7%E3%83%A7%E3%83%B3" target="_blank">CI</a> などでエラーログに出力されてしまう、などに注意する必要があります。GitHub の GitHub Actions などでは、環境変数として渡したい場合は <code>Environment secrets</code> に登録することで渡せ、標準出力・標準エラー出力に同一のものがあった場合は、マスクしてくれます。

- あわせて読みたい: [Docker Composeの環境変数ではなくsecretsで秘密情報を扱う](https://qiita.com/myabu/items/89797cddfa7225ff2b5d#_reference-b678197ab6e7b9c98072) @ Qiita

:::


### 環境変数の渡し方の概要と基本構文

以下はコマンド毎にポイントをまとめたものです。各々の詳細な説明は [TS; DR](#ts-dr%E8%A9%B3%E7%B4%B0) をご覧ください。

#### **`docker run` コマンド**

- **コマンド引数で環境変数を「コンテナに」渡す**<br>（`-e` `--env`）

    ```bash:変数名と値を指定して渡す
    docker run --env [ 変数名=値 ] [ その他のオプション ] <イメージ名> [ コマンド ]
    docker run -e    [ 変数名=値 ] [ その他のオプション ] <イメージ名> [ コマンド ]

    例) docker run -e hoge=$fuga -e piyo="hogera" alpine env
    ```

    ```bash:ホストの環境変数のまま渡す
    docker run --env [ ホスト側変数名 ] [ その他のオプション ] <イメージ名> [ コマンド ]
    docker run -e    [ ホスト側変数名 ] [ その他のオプション ] <イメージ名> [ コマンド ]

    例） docker run -e hoge -e piyo alpine env
    ```

    ```json:VSCodeのRemote-Containersなどでホストの環境変数のまま渡す（.devcontainer/devcontainers.json設定例）
    {
        "runArgs": [
            "--cap-add=SYS_PTRACE",
            "--security-opt",
            "seccomp=unconfined",
            "--env=[ ホスト側変数名 ]"
        ],
    }
    ```

- **コマンド引数から外部ファイルで環境変数を「コンテナに」渡す**<br>（`--env-file`）

    ```bash:環境変数をファイルで渡す（絶対パスであることに注意）
    docker run --env-file [ ファイルパス ] [ その他のオプション ] <イメージ名> [ コマンド ]

    例) docker run --env-file "$(pwd)/myEnvFile" alpine env
    ```

    ```json:VSCodeのRemote-Containersなどでホストの環境変数をファイルで渡す（.devcontainer/devcontainers.json設定例）
    {
        "runArgs": [
            "--cap-add=SYS_PTRACE",
            "--security-opt",
            "seccomp=unconfined",
            "--env-file=./.devcontainer/devcontainer.env"
        ],
    }
    ```

    ```ini:./.devcontainer/devcontainer.envの中身の例
    # ホスト側で定義済みの環境変数
    LANG
    LC_ALL
    # .env 内で定義する環境変数
    HOGE="fuga"
    ```

#### **`docker build` コマンド**

> `docker build` 時に外部ファイルで環境変数は渡せません。`--build-arg` オプションで間接的に渡す必要があります。

- **ビルド時にコマンド引数で「Dockerfile の ARG に」環境変数を渡す**<br>（`--build-arg`）

    ```bash
    docker build --build-arg [ 変数名=値 ] .
    ```

    - 上記で指定した変数は `Dockerfile` 内の `ARG` で定義した同名の変数の値と置き換わります。記載すると「Dockerfile 内で」変数として使える。`RUN` 時の動的な値、例えば `curl` でダウンロードするアーキテクチャ名やバージョン情報などを渡すのに便利。

        ```shell-session:変数my_app_version（値は$hoge）をDockerfileに渡す
        $ hoge='v1.0.0'
        $ osarch=$(uname -m)
        $ docker build \
            --build-arg name_base=php:alpine \
            --build-arg my_app_version=$hoge \
            --build-arg my_arch=$osarch \
            --build-arg my_sample='sample' \
            .
        ```

        ```dockerfile:Dockerfile(simple)
        # 外部からの受付とデフォルト値（FROM ブロック外の宣言は事前に行う）
        ARG name_base=php
        
        # ----------------------------------------------------        
        # FROM ブロック開始
        # ----------------------------------------------------        
        FROM ${name_base}
        # FROM 内での利用宣言（宣言しておかないと受け取れない）
        ARG my_app_version
        ARG my_arch
        ARG my_sample
        # 変数を環境変数にセット
        ENV VERSION_APP=${my_app_version}
        ENV ARCH_TYPE=${my_arch}
        # 変数を実行に利用
        RUN echo "$my_sample"
        ...
        ```

        ```dockerfile:Dockerfile（マルチステージ・ビルドの例）
        # 外部からの受付とデフォルト値（下記２つの FROM ブロックに適用できる）
        ARG name_base=php
        ARG variant=alpine
        ARG my_app_version=dev

        # ----------------------------------------------------        
        # FROM ブロック開始
        # ----------------------------------------------------        
        FROM ${name_base}:${variant} AS stage1
        # FROM 内での利用宣言（宣言しておかないと受け取れない）
        ARG my_app_version
        ARG my_arch
        # 変数を環境変数にセット
        ENV VERSION_APP=$my_app_version
        # 変数を実行に利用
        RUN echo $my_arch

        # ----------------------------------------------------        
        # 別の FROM ブロック開始
        # ----------------------------------------------------        
        FROM ${name_base}:${variant} AS finalstage
        # FROM 内での利用宣言（宣言しておかないと受け取れない）
        ARG my_sample
        # 変数を環境変数にセット
        ENV MY_SAMPLE=$my_sample
        # 変数を実行に利用
        RUN echo $my_sample
        ...
        ```
    - <sup><font color=darkorange>注意</font>：`ARG` は厳密には環境変数ではなくグローバル変数なのですが、「外部から与えられた変数」という意味でここでは環境変数と同じような扱いをしています。`ARG` で宣言/定義した変数を `ENV` の環境変数に渡すと、ビルドされたイメージに環境変数として埋め込めます。ビルド時のアーキテクチャ名、 `git` のコミット ID やバージョン情報といった**センシティブな値でないもの**に利用します。アクセストークンやパスワードなどには使わないことをおすすめします。「別にイメージは公開しないから」ということもあるかもしれませんが、意識が `docker` いっちゃってる時に `git push` のつもりが `docker push` と間違って打ってしまいインターネッツに公開されちゃうなどもありえるからです。</sup>

#### `Dockerfile` ファイル

- **Dockerfile に環境変数を記載（設定）して「イメージに」渡す**
  - `ENV` 命令／宣言文を使う
      - ２通りの記述方法があります
          1. `ENV [変数名1]=[値1] [変数名2]=[値2] ... [変数名n]=[値n]`
          2. `ENV [変数名] [値]`
      - <font color=darkorange>イメージに焼かれる</font>ので注意
      - ホスト OS 側の環境変数を直接は利用はできないが `docker build` 時に `ARG` 経由で受け取ることができる。（上記 `docker build` or tl;dr 参照）

- **環境変数を外部ファイルで渡す**
   - 直接的な外部の**ファイル渡しは無い**
   - 別途コンテナ内のスクリプトでファイルを読み込む工夫が必要

#### `docker-compose run` コマンド

- **環境変数を引数で「コンテナに」渡す**<br>（`-e`）

    ```bash
    docker-compose run -e [ 変数名 ]=[ 値 ] [ その他のオプション ] <サービス名> [ コマンド ]

    例） docker-compose run -e hoge=fuga -e piyo=mogera alpine env
    ```

    - ロング・オプション（`--env`）は無い

- **環境変数を外部ファイルで渡す**
    - オプションによる外部の**ファイル渡しは無い**
    - `docker-compose.yml` 経由では可能

#### `docker-compose.yml` ファイル

- **`docker-compose.yml` に環境変数を記載（設定）して「コンテナに」渡す**
    - `environment:` 変数に設定する
    - ホスト OS 側の環境変数も利用可能

- **環境変数を外部ファイルで「コンテナに」渡す**
    - `env_file:` 変数にパスを設定する
    - ホスト OS 側の環境変数も利用可能

### 動作確認済み環境

<details><summary>詳細</summary><div><br>

- macOS Mojave（OSX 10.14.4）

```shell-session:docker_v18.09.0
$ docker version
Client: Docker Engine - Community
 Version:           18.09.0
 API version:       1.39
 Go version:        go1.10.4
 Git commit:        4d60db4
 Built:             Wed Nov  7 00:47:43 2018
 OS/Arch:           darwin/amd64
 Experimental:      false

Server: Docker Engine - Community
 Engine:
  Version:          18.09.0
  API version:      1.39 (minimum version 1.12)
  Go version:       go1.10.4
  Git commit:       4d60db4
  Built:            Wed Nov  7 00:55:00 2018
  OS/Arch:          linux/amd64
  Experimental:     false
```
```shell-session:docker-compose_v1.23.2
$ docker-compose version
docker-compose version 1.23.2, build 1110ad01
docker-py version: 3.6.0
CPython version: 3.6.6
OpenSSL version: OpenSSL 1.1.0h  27 Mar 2018
```

---

</div></details>

## TS; DR（詳細）

- [Docker の基本](#%E6%89%80%E6%84%9F)（お暇な人のゴロ寝読み向け）
- あわせて読みたい:
    - [忙しい人のための Docker と Docker compose](https://qiita.com/it_tsumugi/items/00075bd40b48d8911b7b) @ Qiita
    - [Docker で MySQL サーバーを構築するときの環境変数の設定](https://qiita.com/pachi-dragon/items/8d248c518eb25cd9cf68) @ Qiita
    - [proxyの罠](https://qiita.com/cohpen/items/d307dfb8b540e3976d37#proxy%E3%81%AE%E7%BD%A0) | 社内でGrowiを立ち上げてみた @ Qiita

---

> <sub>🐒 &nbsp; Docker では通常「ホストOS」「ゲストOS」という呼び方はしません。ここではわかりやすくするため、Docker のエンジン本体を走らせている OS を「ホストOS」、Docker のベース・イメージ（コンテナ内の OS）を「ゲストOS」と呼んでいます。</sub>

Docker のベース・イメージが Alpine Linux の場合、デフォルトの環境変数は以下のようになります。この環境変数に**任意の環境変数をコンテナ起動時に指定したい**のです。

```shell-session:デフォルトの「環境変数」の確認（AlpineOSコンテナ内の場合）
$ # Default env（現在の環境変数を出力して終了）
$ docker run --rm alpine env
PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin
HOSTNAME=b08ff260c4b4
HOME=/root
```

<details><summary><code>docker run --rm alpine env</code> の簡易説明</summary><div>

```bash:runコマンドの基本構文
docker run [ オプション ] <イメージ名> [ 実行コマンド ]
```
```bash
docker run --rm alpine env
```

- `docker run` コマンド
  - イメージ名からコンテナを作成＆起動するコマンドです。
- `--rm` オプション
  - 実行終了後にコンテナを削除（`remove`）する指定です。わかりづらいですが、ロングオプション（`--` で始まるオプション）です。
- `alpine` イメージ
  - 作成するコンテナのイメージ名を指定しています。ここでは Alpine Linux のシンプルな Docker イメージ `alpine` を指定しています。`docker build -t <イメージ名> .` で作成した俺様イメージ名も指定できます。
- `env` コマンド
  - コンテナ内で実行するコマンドを指定しています。ここでは Linux/Unix の、現在の環境変数を一覧で確認する `env` コマンドを指定しています。

</div></details>

### `docker run` 時に環境変数を渡す方法

#### コマンドの引数で環境変数を渡す

> `-e` `--env` オプションで**変数を引数渡し**
> ホスト OS の環境変数名で指定も可能（下記の場合 `MY_SECRET`）

```shell-session:環境変数を引数で渡す例
$ # ホスト OS 側で環境変数をセット
$ export MY_SECRET='MySecretIsHimitsu'

$ # 任意の環境変数を --env ロング・オプションで渡す（-e オプションでも可）
$ docker run --rm \
>   --env HOGE="fuga" \
>   --env foo="bar" \
>   --env MY_SECRET \
>   alpine \
>   env

PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin
HOSTNAME=b08ff260c4b4
HOGE=fuga
foo=bar
MY_SECRET=MySecretIsHimitsu
HOME=/root
```

#### コマンドで外部ファイルを指定して環境変数を渡す

> `--env-file` オプションで**変数をファイル渡し**

```shell-session:環境変数をファイルで渡す例
$ # ホスト OS 側で定義済みの環境変数（例）
$ echo $MY_ENV_VARIABLE
piyo

$ # 変数を記載したファイルの用意
$ cat ./my_env_file.txt
HOGE='FUGA'
foo=bar
MY_ENV_VARIABLE

$ # 引数ファイルを指定して環境変数を確認
$ docker run --rm \
>   --env-file ./my_env_file.txt \
>   alpine \
>   env
PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin
HOSTNAME=b7b5301cba78
HOGE='FUGA'
foo=bar
HOME=/root
MY_ENV_VARIABLE=piyo
```

### Dockerfile 経由で環境変数を渡す方法

#### Dockerfile に記載した環境変数を「イメージ」に渡す（焼き付ける）

> `ENV` 命令（宣言文）で**変数の指定渡し**

```shell-session
$ ls
Dockerfile

$ # Dockerfile の内容確認（ENV の書き方の違いに注目）
$ cat Dockerfile
FROM alpine
ENV HOGE='FUGA' PIYO='PIYO PIYO'
ENV hoo bar

$ # イメージのビルド
$ docker build --tag my_alpine .
...（省略）

$ # ビルドしたイメージのコンテナ内の環境変数を確認
$ docker run --rm my_alpine env
PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin
HOSTNAME=c1f67f6ff882
HOGE=FUGA
PIYO=PIYO PIYO
hoo=bar
HOME=/root 
```

- <font color="darkorange">**注意①**</font>：Dockerfile に記載すると環境変数をイメージに焼き付けることになります。アクセストークンやパスワードなど**流出すると、たいへんな事態になる変数の場合は注意**します。<br>この環境変数は `-e` `--env` オプションで上書きできます。そのため、**Dockerfile に記載する `ENV` の変数はデフォルト値として設定**し、利用時に上書きするのがベターな方法です。
- <font color="darkorange">**注意②**</font>：**Dockerfile 内でホスト側の環境変数は直接呼び出せません**。<br>`ENV my_path=$PATH` と指定した場合は、**ビルド時のゲスト OS 側の環境変数**が使われます。ホスト側の環境変数を使いたい場合は、次項を参照ください。

#### Dockerfile 「に」環境変数を渡す（`docker build`時）

> `--build-arg` で `ARG` 受け取り。そして `ENV` に渡す。

前述の `ENV` 宣言方式で、Dockerfile に記載した変数の値をコンテナに渡すことができました。しかし、動的に値を渡したい場合があると思います。

例えばバージョン番号などです。特に `git` でタグ付けした `git describe --tags` で取得できるバージョン情報を渡したいことも多いのではないでしょうか。

このように、ホスト OS 側からコンテナ（ゲスト OS）に環境変数を渡すには後述する `docker-compose` コマンドを利用するのが楽です。

しかし、**`docker-compose` コマンドを使いたくない場合**は、`docker build` 時に `--build-arg` オプションを指定すると、`Dockerfile` 内で `ARG` で受け取ることができます。

```shell-session
$ cat Dockerfile
FROM alpine
ARG my_app_version
ENV VERSION_APP=$my_app_version

$ hoge='fuga'
$ docker build --tag my_alpine --build-arg my_app_version=$hoge .
...
$ docker run --rm my_alpine env
PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin
HOSTNAME=3706b1c92dc5
VERSION_APP=fuga
HOME=/root

```

#### Dockerfile で外部ファイルの環境変数を渡す

> Dockerfile 自体は**外部ファイルの読み込みをサポートしていません**

<details><summary>どうしても Dockerfile で外部ファイルから環境変数を設定したい場合</summary><div><br>

ネットには環境変数を設定するスクリプトを `COPY` もしくは `ADD` 命令でコピーし `ENTRYPOINT` の起動時に実行させるなどの方法もありますが、オススメしません。

やはりイメージに書き込まれることになるため、`docker run <イメージ名> cat <コピーしたファイルのパス>` などで設定値が見えてしまうからです。

どうしても Dockerfile で完結させたい場合は、`-v` や `--volume` オプションで環境変数の設定ファイルをマウントさせて、スクリプトで実行させる必要があります。

また、Dockerfile と同じ階層に置かないといけないなどの Docker の制限などもあり、メンテナンス性がすこぶる悪くなります。

しかし、逆に言えば、**Dockerfile のディレクトリ内で完結できるということでもある**ので、以下で何をしているのかがわかり、スクリプト群も外部に一般公開しないのであれば、シンプルと言えばシンプルです。情報をイメージに渡すボキャブラリーとしては有用です。

```shell-session:外部ファイルをボリュームとしてマウントし環境変数を設定する
$ # 最低限必要なファイル
$ ls
Dockerfile		run_something.sh

$ # Dockerfile の中身確認（run_something.sh をイメージに COPY していないことに注目）
$ cat Dockerfile
FROM alpine
ENTRYPOINT /my_volume/run_something.sh

$ # 環境変数を設定するスクリプトの中身（コンテナ起動時に実行される）
$ cat run_something.sh
#!/usr/bin/env sh
export HOGE=FUGA
export foo=bar
export MY_SECRET='MySecretIsHimitsu'
env

$ # イメージのビルド
$ docker build -t my_alpine .
...（省略）
Successfully tagged my_alpine:latest

$ # マウントさせずにコンテナを起動した場合の環境変数を確認（enrtypoint を上書きして env コマンド実行）
$ docker run --rm --entrypoint 'env' my_alpine
PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin
HOSTNAME=efba0e3f7a89
HOME=/root

$ # カレントディレクトリを /my_volume にマウントさせてコンテナ起動（Dockerfile の entrypoint が働く）
$ docker run --rm -v "$(pwd)":/my_volume my_alpine
HOGE=FUGA
HOSTNAME=715cc5bd1fa4
SHLVL=2
HOME=/root
foo=bar
PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin
PWD=/
MY_SECRET=MySecretIsHimitsu

```

実は Dockerfile の外部ファイル読み込みとしての [`INCLUDE` や `IMPORT` 命令の実装要望は長いこと（2013 年から）ディスカッションされていました](https://github.com/moby/moby/issues/735#issuecomment-120493444)が却下されました。

- Issue #735 "[Proposal: Dockerfile add INCLUDE](https://github.com/moby/moby/issues/735)" | moby @ GitHub

これは、**Dockerfile は特定のイメージを作成することに特化したもの**であり、`INCLUDE` や `IMPORT` 命令をサポートすると依存が増えることになるため、**シンプルさに欠けるから**のようです。

そのため、`docker-compose` や `vagrant`[^vagrant] といった[オーケストレーション](https://ja.wikipedia.org/wiki/%E3%82%AA%E3%83%BC%E3%82%B1%E3%82%B9%E3%83%88%E3%83%AC%E3%83%BC%E3%82%B7%E3%83%A7%E3%83%B3_(%E3%82%B3%E3%83%B3%E3%83%94%E3%83%A5%E3%83%BC%E3%82%BF))系のソフト経由で設定することをオススメします。

なお、間違えやすいコマンドに `docker import` がありますが、これは `docker pull` のローカル版です。つまり DockerHub 上にないイメージを利用したい場合に、`docker save` で保存したアーカイブされた Docker イメージをインポートするコマンドです。

---

</div></details>

[^vagrant]: [Vagrant（ソフトウェア）](https://ja.wikipedia.org/wiki/Vagrant_(%E3%82%BD%E3%83%95%E3%83%88%E3%82%A6%E3%82%A7%E3%82%A2)) @ Wikipedia


### `docker-compose run` 時に変数を渡す方法

:::note info
<sub><sup>`docker-compose run` と `docker compose run` のハイフン（`-`）違いですが、基本的に同じものです。</sup></sub>
<sub><sup>現在の `docker` CLI には `docker-compose` の [v2 と同等のコマンドが標準で実装されています](https://www.docker.com/blog/announcing-compose-v2-general-availability/)。つまり `docker compose` は `docker-compose` のエイリアスになります。この記事では検索と下位互換のため `docker-compose` のまま記載していますが、`docker compose` とサブコマンドに置き換えてお読みください。
</sup></sub>
:::

#### `.env` ファイルで環境変数を渡す

`docker-compose` v1.28 以降であれば、`docker-compose.yml` ファイルと同じ階層に、環境変数を記載した `.env` ファイルを設置すると、デフォルトで読み込みます。

```shell-session
$ cat .env
FOO=bar

$ cat docker-compose.yml
version: "3.9"
services:
  sample:
    image: alpine
    environment:
      FOO: hoge
    entrypoint: ["echo", "$FOO"]

$ # .env が environment より優先されることに注目
$ docker compose run --rm sample
bar
```

注意点として、`.env` のドット・ファイルであるため隠しファイルになります。ユーザーによっては混乱を招く（どこで設定された値かわからず色々弄られる）ため、後述する `--env-file` オプションや `env_file` 指示子で明示する方が無難でしょう。

#### コマンドの引数で環境変数を渡す

> `-e` オプションで**変数を引数渡し**

- `docker-compose` コマンドには `--env` のロング・オプションはありません。

```shell-session:docker-compose実行時に引数で渡す例
$ # docker-compose.yml の内容確認
$ # 環境変数を何も指定していない点に注目
$ cat docker-compose.yml
version: "3"
services:
  my_app:
    image: alpine

$ # デフォルトのコンテナ内の環境変数確認
$ docker-compose run --rm my_app env
PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin
HOSTNAME=d7df4af36a24
TERM=xterm
HOME=/root

$ # 環境変数をオプションで引数渡し
$ docker-compose run \
>   --rm \
>   -e HOGE=FUGA \
>   -e foo=bar \
>   my_app \
>   env

PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin
HOSTNAME=33dd92872107
TERM=xterm
HOGE=FUGA
foo=bar
HOME=/root

```

#### コマンドで外部ファイルを指定して環境変数を渡す

> `docker-compose.yml` の `env_file:` で**変数をファイル渡し**

**`docker-compose run` のオプションには外部ファイルの指定がありません**が、`docker-compose.yml` の `env_file:` 変数にファイルのパスを指定すると環境変数を外部ファイルで設定することができます。これにより、`docker-compose up` などでも外部ファイルで環境変数を指定することができます。（次項参照）

### `docker-compose.yml` 経由で変数を渡す方法

#### `docker-compose.yml` に記載した環境変数を渡す

> `environment:` 変数で**環境変数を指定渡し**
> ホスト OS の環境変数名で指定も可能（下記の場合 `MY_SECRET`）

```yaml:docker-compose.ymlの設定例（配列形式で渡す。コロンに注意）
version: "3"
services:
  app:
    image: alpine
  environment:
    HOGE: FUGA
    foo: bar
    MY_SECRET:
```

<details><summary>実行例</summary><div><br>

```shell-session:実行例（配列形式の例。コロンに注意）
$ # docker-compose.ymlの用意
$ ls
docker-compose.yml

$ cat docker-compose.yml
version: "3"
services:
  my_app:
    image: alpine:latest
    environment:
      HOGE: FUGA
      foo: bar
      MY_SECRET: 

$ # ホスト OS 側で環境変数をセット
$ export MY_SECRET='MySecretIsHimitsu'

$ # オプション引数を渡さずにコンテナ内の環境変数を確認
$ docker-compose run --rm my_app env
PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin
HOSTNAME=8cbcd1863cd2
TERM=xterm
HOGE=FUGA
foo=bar
MY_SECRET=MySecretIsHimitsu
HOME=/root
```

---

</div></details>

```yaml:docker-compose.ymlの設定例（辞書形式で渡す。イコールの有無に注意）
version: "3"
services:
  app:
    image: alpine
  environment:
    - HOGE=FUGA
    - foo=bar
    - MY_SECRET
```

<details><summary>実行例</summary><div><br>

```shell-session:実行例（辞書形式の例。イコールの有無に注意）
$ # docker-compose.yml を用意
$ ls
docker-compose.yml
$ cat docker-compose.yml
version: "3"
services:
  my_app:
    image: alpine:latest
    environment:
      - HOGE=FUGA
      - foo=bar
      - MY_SECRET

$ # ホスト OS 側で環境変数をセット
$ export MY_SECRET='MySecretIsHimitsu'

$ # オプション引数を渡さずにコンテナ内の環境変数を確認
$ docker-compose run --rm my_app env
PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin
HOSTNAME=961a6c9b0ca6
TERM=xterm
HOGE=FUGA
foo=bar
MY_SECRET=MySecretIsHimitsu
HOME=/root

```

- <font color="darkorange">**注意**</font>： `docker-compose.yml` のインデントやスペースの入れ方を間違えると、`must be a mapping not an array`エラーが出やすいので注意。

    ```
    ERROR: In file './docker-compose.yml', service 'environment' must be a mapping not an array.
    ```

- [environment](https://docs.docker.com/compose/compose-file/#environment) | Version3 | Compose file reference | Docker Compose @ docs.docker.com 

---

</div></details>

#### `docker-compose.yml` に記載した外部ファイルの環境変数を渡す

> `env_file:` 変数で**環境変数を指定渡し**
> ホスト OS の環境変数名で指定も可能（下記の場合 `MY_SECRET`）

```shell-session
$ # 最低限のファイル一覧
$ ls
docker-compose.yml	my_env_file.env

$ # docker-compose.yml の内容確認
$ cat docker-compose.yml
version: "3.7"
services:
  my_app:
    image: alpine
    # 環境変数をファイルで指定
    env_file: my_env_file.env

$ # ホスト OS 側で環境変数をセット
$ export MY_SECRET='MySecretIsHimitsu'

$ # 変数を記載したファイルの用意（ポイントは MY_SECRET で、値がないのでホスト OS の環境変数を使う）
$ cat my_env_file.env
HOGE=FUGA
foo=bar
MY_SECRET

$ # 外部ファイル読み込み後の環境変数確認
$ docker-compose run --rm my_app env
PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin
HOSTNAME=d324b0014cfb
TERM=xterm
HOGE=FUGA
foo=bar
MY_SECRET='MySecretIsHimitsu'
HOME=/root
```

### VSCode の Remote-Containers にホストの環境変数を渡す際の注意点

> VSCode が認識していないとコンテナにも渡されない。

VSCode には、`Remote-Containers` という拡張機能を入れると、Docker で開発環境を構築することができます。つまり、Docker 内にワークスペースをマウントし、VSCode からリモートでアクセスできます。これにより、ローカルに色々とインストールする必要もなくなり、使い捨ての環境が作れます。

さて、この VSCode + Remote-Containers 拡張機能 + Docker を使っていて「あれ？ローカルの環境変数が渡されないな」という場合、大抵の場合 VS Code がローカルの最新の環境変数を読み込めていない可能性があります。

VSCode は、VSCode 自身が起動した時点の環境変数を利用するからです。

コンテナ上でリポジトリを開く（`Open in Container` でコンテナを起動する）前に、VS Code のターミナル上で（ローカルの）環境変数を `env` コマンド確認してみてください。新規でセットした環境変数が反映されていない場合は、VSCode を再起動すると反映されます。

特に **macOS でシェルを `bash` で利用している場合などは注意が必要**です。OS のデフォルトのシェルが `zsh` に変更されているので、VSCode も `zsh` の設定を読み込むためです。その場合は、VSCode のターミナル設定でデフォルトのシェルを `bash` に変更します。

> `⌘ + Shift + P` から `Terminal: Select Default Profile` を開き、`bash` を選択。

## 所感

### 取り扱い注意な値

> コンテナ内のアプリに、アクセス・トークンをハード・コーディングせずに渡したい

ちょっとした開発で Docker コンテナ内のアプリやスクリプトからセンシティブな値、つまり「触る際に注意が必要な値」を扱う必要がありました。

アクセス・トークン、SSH や OpenSSL に必要な秘密鍵などの、**どんな場合においてもハード・コーディングする（プログラム内に書いちゃう）のはご法度**な設定値です。

そのため、一般的なアプリの場合は「呼び出す時に引数で渡す」か「外部ファイルとして読み込ませる」のが定石とされます。これは基本的に Docker の場合でも同じです。

:::note info
**🐒&nbsp;&nbsp;<strong>Docker のイメージとコンテナの違い**

Windows、macOS、Linux など、OS をマシンにインストールする時に USB や CD などからインストールすると思います。

関係的に USB や CD などのメディアを「イメージ」、そこから作られた（マシンにインストールされた）ものが「コンテナ」に相当します。<a href="https://ja.wikipedia.org/wiki/%E4%BB%AE%E6%83%B3%E6%A9%9F%E6%A2%B0">VM</a> などの場合は、イメージから作られたインスタンスが「コンテナ」に相当します。

しかし、これは**動きが似ているというだけで、仕組みは異なります**。これらと違うのは、Docker の場合は OS（カーネル）が入っていません。

Docker のイメージは、どちらかと言うと他で言う「スナップショット」や「スリープ処理」（現在のメモリの状態をファイルとして保存したもの）に近いものです。つまりカーネルさえ動いていれば、その上に保存したメモリを展開（コンテナとして起動）すれば動くのと似た働きをします。

そのため「イメージに何を含めるのか」によってメモリの消費量に大きく影響します。Linux のフレーバー（<a href="https://ja.wikipedia.org/wiki/Linux%E3%83%87%E3%82%A3%E3%82%B9%E3%83%88%E3%83%AA%E3%83%93%E3%83%A5%E3%83%BC%E3%82%B7%E3%83%A7%E3%83%B3">ディストリビューション</a>）を丸ごと使ってしまうと、使いもしないものまで入ってしまうので、そのぶんメモリを消費します。詳しくは後述。
:::

### 環境変数でデータを渡すことの考察

:::note info
🐒&nbsp;結論から言うと「変数がどのように渡され、他者からも見ることができるか」を理解したうえで、開発用やローカルのマシンなどで非公開で動かすぶんには、環境変数渡しでも問題ないと思います。つまり、マシンが手元にあり、Docker 単独もしくは docker-compose で事足りる範囲での開発用途なら、ということです。
**問題は、管理が広範囲に及ぶ場合**です。例えば、1 つのコンテナに色々詰め込み過ぎて、実は意図しないサービス（プロセス）が立ち上がっているのに、プロセス一覧に埋もれて気づかない場合です。他にも Swarm や Kubernetes など、複数マシン・複数ネットワーク・複数コンテナといったクラウディでプロダクション・レベルでの利用です。その場合は Swarm や Kubernetes などが持つ暗号化の機能を使う必要がありますが、筆者は開発環境用、アプリのビルド用、CI の実行用にしか利用していないので、割愛します。
:::

一般的なアプリの場合、<font color="darkorange">セキュリティの観点から「センシティブなデータ」の環境変数経由での受け取りは推奨されません</font>。他のアプリからも見れてしまうためです。

しかし Docker の場合は、環境変数を通してコンテナ内にデータを渡すことを多くみかけます。他にも [GitHub Actions](https://github.co.jp/features/actions)、[Travis CI](https://travis-ci.org/)、[Circle CI](https://circleci.com/ja/) や [Jenkins](https://ja.wikipedia.org/wiki/Jenkins) といった [CI](https://ja.wikipedia.org/wiki/%E7%B6%99%E7%B6%9A%E7%9A%84%E3%82%A4%E3%83%B3%E3%83%86%E3%82%B0%E3%83%AC%E3%83%BC%E3%82%B7%E3%83%A7%E3%83%B3)/[CD](https://ja.wikipedia.org/wiki/%E7%B6%99%E7%B6%9A%E7%9A%84%E3%83%87%E3%83%97%E3%83%AD%E3%82%A4) などはデプロイ（成果物をリリースする際の公開先へのアップロード）に必要なアクセストークンなどを環境変数で渡しています。

コンテナの元となるイメージには焼き付けない、つまり「センシティブなデータ入りでイメージをビルドしない」というのはハードコーディングに通ずるのでわかります。しかし「環境変数経由でなく、なぜファイルをマウントして読み込ませないのかな」と思いました。

結論から言うと、ファイルをマウントさせる方法でも、環境変数経由でも構いません。なぜなら**リスク自体は変わらないから**です。

ファイルをマウントする場合は、マウント・ポイント（マウント先）の仕様決め、フォーマット、ファイルのアクセス権、etc. を考えると、環境変数で渡した方が実装が<ruby>楽<rt>らく</rt></ruby>でしょう。逆に、公開鍵などは、コンテナ起動時に `-v` などでマウントした方が楽でしょう。

どちらの方法であっても、**同じ環境下にあるなら他のアプリやサービスからもアクセスできてしまう**ということを念頭に置く必要があります。これが共通するリスクです。

「いや、そもそもアプリやサービスごとにユーザとアクセス権を設定して、ファイルにもアクセス権を付けて制限させればいいじゃん」と思うかもしれません。同じサーバ内で色々と動かす場合の必須プラクティスですね。

しかし、Linux 管理者は長年の経験から、引き継ぎの煩雑さ、ドキュメント嫁に叱られるメンテナの多さなどの経験から[ポカよけ](https://ja.wikipedia.org/wiki/%E3%83%9D%E3%82%AB%E3%83%A8%E3%82%B1)を考えてきた結果、ある答えにたどり着きました。

「他のアプリを入れなければいい」です。

そして、その先にあるのがコンテナ技術です。[UNIX 哲学](https://ja.wikipedia.org/wiki/UNIX%E5%93%B2%E5%AD%A6#%E8%B5%B7%E6%BA%90)にも似た、「1 つのコンテナは 1 つのシンプルな目的だけに徹する」ことでリスク分散するだけでなく、各々が特化したサービスを提供しているため、安定性やメンテナンス性も向上します。

#### 「1 つのコンテナに詰め込まない」って、どういうこと？

:::note info
🐒 「1 コンテナ、1 アプリ」のルーツは、Linux に慣れ親しんだ人向けに一言で説明すると「`ENTRYPOINT` で指定した**自分のアプリのプロセスが、コンテナ内の `PID 1` になるから**」です。

つまり、通常 `PID 1` で動いている `init` や `systemd` のようなプロセスを管理してくれるサービス（デーモン）がコンテナ内に存在せず、変わりに自分のアプリが `PID 1` になるということです。

そのため、`SIGTERM` などのシグナルをコンテナ内の他のプロセスにも送る責任を負うことになり、**全てのプロセスが終了したのを確認してから自身を終了する必要があります**。色々と詰め込んでしまうと、下手をすると[ゾンビプロセス](https://ja.wikipedia.org/wiki/%E3%83%97%E3%83%AD%E3%82%BB%E3%82%B9#%E7%B5%82%E4%BA%86%E7%8A%B6%E6%85%8B)が生まれる原因にもなりかねません。

さらには、コンテナ内の複数プロセスを適切に処理したいがために [Tini](https://github.com/krallin/tini) などの軽量版 INIT を入れたりと、コンテナ内の環境構築の沼にハマると「VM を使った方が早い」となりかねません。しかも、せっかくの Docker のメリットが活かせません。

現在は [`--init` オプション](https://docs.docker.com/engine/reference/commandline/run/)や [`init` 指示子](https://docs.docker.com/compose/compose-file/compose-file-v3/#init)で `tini` を手軽に差し込めるようになっているものの、「1 コンテナ、1 アプリ」は変わりません。まずは **1 つの機能だけをしっかり提供できるシンプルなコンテナを作成すること**に注力した方がいいでしょう。
:::

まず、先に言っておくと「コンテナに詰め込まない」と言っても「**開発に使う場合に限り**、コンテナには必要なものは詰め込んでおいた方が楽なことが多い」という前提があります（ここで言う「開発に使う」とは「コンテナ内に開発環境を作ってリモートでコンテナ内で作業する」と言う意味ではなく、「開発中にトライ＆エラーをする場所」としての利用を指します）。

特にテストなどで**再現性が重視される場合に、「使っては捨てる」ことで威力を発揮します**。逆に安定した開発環境が欲しい場合は、VM の方が開発に向いているケースもあります。

「コンテナに詰め込まない」というのは、1 つの機能（サービス）として提供できる状態のコンテナを指します。

例えば、[CI](https://ja.wikipedia.org/wiki/%E7%B6%99%E7%B6%9A%E7%9A%84%E3%82%A4%E3%83%B3%E3%83%86%E3%82%B0%E3%83%AC%E3%83%BC%E3%82%B7%E3%83%A7%E3%83%B3) での実行や、機能の単発利用、サービスとして稼働させる場合においての話しです。

つまり「製品のみ」の入ったコンテナにするということです。この場合、この後説明するようにハイバーバイザー型にはない恩恵が受けられます。

これだけを読むと「いや、コンテナうんぬんでなくても開発ツールは一緒にデプロイ（出荷）しないよwwww」と思うかもしれません。

しかし、そこがポイントではありません。

Web 関連で例えると、俺様 Web アプリを作った場合は、PHP や Python ならランタイムとスクリプトだけ。Go や Rust ならビルドしたバイナリだけ、といった具合です。

つまり、Apache などの Web サーバや SQL のサーバだけでなく、目的に必須ではないなら bash すら入れていない「製品のみが動くコンテナ」にするのが理想ということです。

そして Apache、SQL、俺様 Web アプリなどを、各々を別々のコンテナにします。

利用する際は、同じ Docker の閉鎖的なネットワーク内でそれらを起動して利用するのが理想的な使い方なのです（繰り返しますが開発の場合はこれに限りません）。

そのため Docker そのものに慣れてきて、セキュリティがいささか心配になってきたら、次に docker-compose に慣れるのが Docker（もしくはコンテナ技術）のメリットを本格的に感じることができると思います。

すると、「いやいや。それは聞いたことあるんだけど、数ギガ近くあるイメージを複数立ち上げるとメモリがパンパンになるから、1 つのイメージに入れた方がメモリ節約にもなるし」と思うかもしれません。

しかし Docker において、実は、それは「Word や Excel のアプリごとに Windows パソコンを用意する」ような考え方と同等なのです。

なぜなら、イメージが数ギガ近くあるのは、Ubuntu や Debian など **OS が丸ごと入ったイメージの上にアプリやサービスを入れているから**です。

また、逆にイメージは小さいのに Windows や macOS ではメモリがパンパンになるのは、[VM](https://ja.wikipedia.org/wiki/%E4%BB%AE%E6%83%B3%E6%A9%9F%E6%A2%B0) 上で Linux を動かし、その上で Docker を起動しているからです。つまり、直接 Docker を起動しているのではなく、<font color=darkorange>**仮想 Linux のためにメモリを確保（仮押さえ）しているから**</font>なのです。

:::note info
一口メモ： **Docker は仮想環境アプリではありません**。それに近しい動きはするものの、「Linux アプリ」を Linux 上の閉鎖空間で実行するアプリです。そのため、Win や macOS では VM が別途必要なのです。

Python ユーザなどは `venv` が「閉鎖空間での実行＝＝仮想環境」であるため、なおさら違いがわかりづらいと思います。しかし、Python を「仮想環境プログラム言語」と言われたら違和感があると思います。そんな程度の違いです。
:::

少し話しを戻します。

先に、「アプリごとに Windows パソコンを用意するようなもの」と言いました。それでは「Windows のカーネル（根幹機能）と Word と必要な DLL <ruby>だけ<rt>・・</rt></ruby>の入った OS」だったらどうでしょう？

ブラウザどころかメモ帳や MS ペイントすら入っていません。かなりサイズは小さくなると感じると思います。ってか、もう[ワープロ](https://ja.wikipedia.org/wiki/%E3%83%AF%E3%83%BC%E3%83%89%E3%83%97%E3%83%AD%E3%82%BB%E3%83%83%E3%82%B5)状態です。

そこから、さらに Windows のカーネルすらもブッこ抜いてみます。つまり「Word と必要な DLL だけが入ったもの」と「Windows のカーネル」を分けるということです。

そして Windows のカーネルの上に、「Word と必要な DLL だけが入ったもの」や「Excel と必要な DLL だけが入ったもの」などが入れ替えられるとアプリのコンテナは最低限で済みます。

この状態であれば「Excel <ruby>だけ<rt>・・</rt></ruby>アップグレード」なども簡単にできると感じると思います。もぅ、ファミコンや Nintendo Switch のカセットやカードみたいな発想です。

そして最後に「Word と Excel で共通の DLL がある」場合は、それらはメモリ上で共有することで、実メモリの使用量を抑えることができます。

実は、このカセットのような仕組みは UNIX が元々持っていた機能がベースにあります。

UNIX には元々 [chroot](https://ja.wikipedia.org/wiki/Chroot) という「最低限の OS 環境の上にユーザを置く」仕組みがあります。これが Linux/UNIX 系の「コンテナ技術」のルーツです。これを、よりスリムにして**アプリ単位でまで落とし込んだコンテナ技術が Docker** です。

#### Docker の原点は chroot を活用した Web ページ

Windows ベースの一般ユーザには `chroot` は馴染みがないかもしれません。しかし、普段から目にしていると思います。

> `https://www9.plala.or.jp/hogehoge/index.html`

例えば、インターネット・プロバイダが提供しているホームページのサービスで、上記のような URL を見たことはないでしょうか。この場合は「ぷらら」のユーザーが、ホームページを公開しているアレです。

この URL の本質的な意味は、`plala.or.jp` ドメインの `www9` サーバーに登録されている `hogehoge` ユーザーの、ホームディレクトリの `www` フォルダにある `index.html` を、`https` のポート（Apache などのサービス）にリクエストしていることになります。

昔は、ホームページのデータは FTP などで上げていました。この時「他のユーザーのディレクトリが見えるも、自分以外のユーザーのディレクトリには入れない」といった経験をしたことがある人も多いのではないでしょうか。

また、高負荷の処理は他のユーザーに迷惑をかけるから<ruby>云々<rt>うんぬん</rt></ruby>といった話しも聞いたことがあると思います。「1 つのサーバーに複数人でシェアしているから」という話しです。

同じマシンで動いているのに、他のユーザーが何をしているかは基本的に知ることができず、自分の PHP や Ruby スクリプトを実行しても、他の一般ユーザーは止めることすらできないことを不思議に思ったことはないでしょうか。

これは **Unix をベースとした OS は、1 つのマシンを複数人で「同時に」利用することが前提である**ためです。（もちろん Windows Server にも似た機能があります）

そして、ディレクトリ名は見えても中身を参照できないことから、「ああ。ユーザーのアクセス権で制御しているのね」と思うも、ある時からログインすると自分のホーム・ディレクトリがルート・ディレクトリになっており、以前のように他の上位のディレクトリを参照することすらできくなったということはないでしょうか。

自分のホームディレクトリをカプセル化されて、そこから外にはアクセスできなくなったような状態です。

サーバー系の OS には、ユーザーに「ユーザー専用の空間」を提供して、その中で行っている内容は他のユーザーには見えない仕組みがあるのです。Unix 系の OS において、このような仕組みの 1 つが `chroot` です。

ゆうて、同じマシンにいるので、CPU やメモリといったリソースは共有です。そのため、誰かが高負荷をかけていたら（何をしているかはわからないが）影響を受けるという理屈なのです。

ここで Docker を理解するために**アホみたいな話しですが、実は重要な事実**を再認識する必要があります。

それは「どのようなコマンドやプログラムも実行しないと動かない」ということです。

#### カーネルさん出番だす

「どのようなコマンドやプログラムも実行しないと動かない」というのは、コマンドやプログラムを実行するには「プロセス化しないといけない」ということでもあります。

そして、これらプロセスを管理するのが OS のカーネルです。

:::note info
カーネルとは「何かに覆われており、直接は触れないが、何かの根幹となるもの」です。日本語で「核」「（牡蠣などの）身」「種」といった意味を持ちます。

OS においては、OS の基本を構成するプログラム群のことで、「シェル（殻）を通してでしか触れないプログラム」のことです。Windows のエクスプローラーや macOS の Finder などもディレクトリにアクセスするためのシェルの 1 種です。

他にも、機械学習の世界では「直接はわからないが、関数を通して現れた、データの根幹となるデータ（特徴）」をカーネルと呼んだりするのも同じ語源です。トウモロコシの「コーン」（"Corn")も "Kernel" の "Kern" から来ており、皮をむくと出てくる実を `Corn Kernel` と呼ぶのも理解できるのではないでしょうか。
:::

カーネルのうち、プロセスを管理するプログラムが 1 番目のプロセスで動いている `init`、`systemd`、`launchd` などです。通常 `PID 1` と呼ばれますが、Windows の場合は[プロセス番号は 4 の倍数で振られる](https://devblogs.microsoft.com/oldnewthing/20080228-00/?p=23283)ため、プロセス番号 4 番で動いている `System` がそれに該当します。

Linux や macOS（UNIX系）であれば `ps -p 1` で確認できます。

Windows の場合は `tasklist /FI "PID eq 4"`（ターミナル）もしくは `get-process -Id 4`（PowerShell）で確認できます（Windows のタスクマネージャーは、これらコマンドの GUI 版のシェルとも言えます）。

`PID 1` プロセスの主な役割は、OS 側から届いた `SIGTERM` や `SIGKILL` といった終了シグナルを、管理している子プロセスに伝達させることです。

これにより、各々の子プロセスが「後処理」を行い、全員が無事終了したことを確認してから、OS に終了準備ができたことを通知することができます。これを `Graceful Shutdown` と呼びます。

このため、`PID 1` は「すべてのプロセスの親プロセス」と言っていいでしょう。

そして `chroot` は、そことユーザーのプロセスの間に入り、他のユーザーから隔離する（`PID 1` と子プロセスの間に入り、子プロセス間の通信を管理する）機能です。

この機能を利用すると、ユーザー・ディレクトリ（`/usr/myname`）を、あたかも `/`（ルート・ディレクトリ）のように見せることができます。これが `change root` 略して `chroot` の語源です。

そのため、`chdir` や `ls` などを実行して（プロセスを走らせて）も、隔離されているため `/usr/myname` より上のディレクトリは参照できないし、他のユーザーのプロセスも見ることはできない、といったことができます。

「Python でいう `pyenv` の OS 版のようなもの」と言えばピンとくるでしょうか。

これは、他のユーザーが何をしているのか覗こうとコマンドを打った（プロセス化した）としても、カーネルがブロックしてくれるメリットが生まれます。

しかし、`chroot` には問題もありました。`pyenv` などにもある悩みと似ているのですが、閉鎖された空間なので、最低限必要なファイルやライブラリのコピーを、ユーザーごとの空間（この場合、ユーザーのディレクトリ）に設置しておかないといけないことです。ユーザー毎にパーティションを切って、OS を入れる感覚に近いかもしれません。つまり `chroot` は「汎用的にカプセル化する」用途には向いていないものだったのです。

その後、[cgroups](https://ja.wikipedia.org/wiki/Cgroups) という「必要なものをグループ化」する仕組みが Linux に組み込まれ、「状態 A の環境」「状態 B の環境」といったものが作れるようになりました。そして、それらの環境を作成しコンテナ化する [LXC](https://ja.wikipedia.org/wiki/LXC)（Linux Containers）というソフトが生まれました。

`LXC` は、体感的には Linux 版の仮想マシンに近いものです。しかし、ハードウェアを仮想化したものと言うよりは、厳密には「Linux 環境をカプセル化して仮想化する」ためのものです。そのため、**`LXC` のカーネルはホストのものを利用（同じカーネルを共有）します**。

先のファミコンや Switch のカセットやカードの例のように、Linux カーネル上に「カプセル化された環境」を載せて、プロセスを隔離しつつ動かせるということです（実際には、プロセスの処理ごとにアクセス範囲を判断してブロックしているだけなのですが、結果として隔離されている動きになります）。

そして、「じゃぁ、カーネルも入れられるようにしちゃおうぜwwww」と、ハードウェアへのアクセスだけを仮想環境につなげることで、VirtualBox のようなハードウェアを仮想化する技術が [KVM](https://ja.wikipedia.org/wiki/Kernel-based_Virtual_Machine) です。

結局のところ、どの技術も「親となるプログラム（プロセス）があって、子となるプログラム（プロセス）を制御している」と言う大きな仕組みは一緒です。

このような概念を活用したものを総称して「コンテナ技術」と呼びます。

「カプセル」と言わずに「コンテナ」と呼ばれるのは、一度作成した環境を別の Linux に持ち運べたり、積み上げたり（コンテナに、機能を追加し新たなコンテナを作成したり）できることを目的としているからです。

そして、Docker は、そこから派生したプログラムの 1 つです。

:::note info
`KVM` は「カーネル（OS）」をコンテナ化するもの、`LXC` は「Linux のシステム」（環境）をコンテナ化するもの、**Docker は「Linux アプリケーション」をコンテナ化するもの**と<ruby>棲<rt>す</rt></ruby>み分けるといいでしょう。
:::

元々 `LXC` は「セキュリティー目的」というより「利便性」から生まれたものなので、初期の頃はセキュリティ的な問題を持っていました。Docker も初期の頃（Docker v1 以前）は `LXC` をベースにしていましたが、プロセスをより強固に監視する独自の実装をすることにより、現在では `LXC` とは別のものになっています。

これが Docker のセキュリティに繋がっているのですが、逆に、この「プロセス管理」という仕組みを理解していないと痛い思いもします（特に、Docker の場合、コンテナ内では `PID 1` で走るプロセスは、`ENTRYPOINT` で指定したプロセス（プログラム）だからです）。

ここで「カーネルはプロセスを実行しているユーザーを見て隔離している」という点に注目します。

Docker には「コンテナ内の実行ユーザーを `root` にしない」というベスト・プラクティスがあります。これは、ホスト OS（Docker が動いているマシンの OS）が Linux などの場合に特に重要です。

Docker のコマンドを実行する際、`sudo docker ...` と毎回 `sudo` を（`root` 権限がない限り）付けないといけないことにお気づきでしょうか。

つまり、<font color=darkorange>**Docker のプログラム自体は `root` 権限で動いている**</font>のです。

そのため、コンテナ内のプロセスのユーザーが `root` だと Docker のコンテナを <ruby>`privileged`<rt>特権</rt>モード</ruby>で動かした場合、**カーネルもそのプロセスを `root` ユーザーとしてみなして実行してしまう**ため、危険とされるのです。

つまり、せっかくの隔離機能が無駄になってしまうのです。

Docker のドロップインになる（入れ替え互換を持つ）[Podman](https://podman.io/) が最近好まれるのは、
デフォルトで `rootless` で実行する（`root` 権限で実行しない）からです。そのぶん権限トラブルの[ポカヨケ](https://ja.wikipedia.org/wiki/%E3%83%9D%E3%82%AB%E3%83%A8%E3%82%B1)になります。

わかれば単純なのですが。

ホスト OS が macOS や Windows の場合は、`root` というユーザーはデフォルトで存在しないし、管理者権限もないため割と安易に `root` ユーザーのままのコンテナを実行しがちです。

しかし、それらは仮想 Linux 上で Docker を動かしていることや、コンテナの実行 OS が Linux になった場合のことを考えると、やはり `root` ユーザーでコンテナを実行するのはリスクを孕んでいるため、よろしくありません。

以降は「コンテナ内のユーザーは `root` ではないこと」を前提に考えてお読みください。

#### なんちゃって Docker は 60 行のコードで作れる

さて、コンテナの概念は UNIX や Linux の世界では昔からあり、Docker は、その仕組みを利用したアプリの成功者の 1 つでしかないことはお伝えしました。

実は、この `chroot` の仕組みというか、概念を踏まえると、Go 言語の場合 60 行以内でコンテナ・アプリを作ることができます。

つまり、呼び出し元のアプリが呼び出したアプリのプロセスを監視・管理することで「なんちゃって `chroot`」を実装できるのです。Docker や Kubernetes などが Go で作られているのも同じ理由です。

以下は英語の[ライトニングトーク](https://ja.wikipedia.org/wiki/%E3%83%A9%E3%82%A4%E3%83%88%E3%83%8B%E3%83%B3%E3%82%B0%E3%83%88%E3%83%BC%E3%82%AF)の動画ですが、実際にコーディングしながら **19 分**で「なんちゃって Docker」を作っています。コードが出来上がっていく流れをみると「あー、隔離ってそういうことなのか」と気づきがあると思います。

- [Building a container from scratch in Go - Liz Rice (Microscaling Systems)](https://youtu.be/Utf-A4rODH8) | Container Camp @ Youtube
- ソースコード: https://gist.github.com/julz/c0017fa7a40de0543001 @ Gist

#### Docker を改めて考える

前述のように、Docker は基本的に（[ポカ](https://ja.wikipedia.org/wiki/%E3%83%9D%E3%82%AB%E3%83%A8%E3%82%B1)や意図して設定しない限り）ホスト OS 側の設定値やファイルにはコンテナからはアクセスできません。

また、他のコンテナの設定値やファイルにも直接アクセスできません。[サンドボックス](https://ja.wikipedia.org/wiki/%E3%82%B5%E3%83%B3%E3%83%89%E3%83%9C%E3%83%83%E3%82%AF%E3%82%B9_(%E3%82%BB%E3%82%AD%E3%83%A5%E3%83%AA%E3%83%86%E3%82%A3))に近いものだからです。その為に Docker を使っているとも言えます。

そして Docker の場合「１コンテナ１プロセスが推奨されている」と言われます。これはコンテナごとにシンプルに１つのサービスのみを実行させる、と同義です。例えば Apache だけ、MySQL だけ、といったことです。

これは同じ Docker ネットワーク内に機能ごとにサーバーを設置するのと同じことになり、機能の入れ替え、拡張やメンテナンスが楽になるメリットを産みます。

<details><summary>■ Docker ネットワークとは</summary>

:::note info
🐒 &nbsp; ここで言う「同じ Docker ネットワーク内」というのは、<strong>Docker の仮想的な LAN のこと</strong>です。

Docker には、コンテナが起動したときに接続できるネットワーク先（仮想 HUB or 仮想ルーターのようなもの）がデフォルトで 3 つあります。

<code>bridge</code>、<code>host</code>、<code>none</code> です。

これは、コンテナ起動時に「どの HUB の LAN くちに挿してネットワークに参加するかが選択できる」と想像すると把握しやすいと思います。

<code>bridge</code> が、その Docker の仮想ネットワークで、デフォルトの接続先です。特に指定がない場合は <code>bridge</code> に接続され、ほかのコンテナと同じ LAN に参加します。コンテナ起動時に <code>--network host</code> オプションを付けると、ホストと同じネットワークに参加します。

1. <code>bridge</code> に挿した場合は、Docker コンテナ専用の LAN に参加（デフォルト動作）
    - <code>docker run --rm -it --network bridge alpine ifconfig</code>（Docker ネットワークの IP が振られる。デフォルトで `172.17.0.0/16` の範囲で自動割り当て）
2. <coe>host</code> に挿した場合は、ホストと同じ LAN に参加
    - <code>docker run --rm -it --network host alpine ifconfig</code>（ホストマシンと同じネットワークの IP が振られる）
3. <code>none</code> の場合は、誰もいない LAN に参加
    - <code>docker run --rm -it --network none alpine ifconfig</code>（お一人様ネットワークの <code>127.0.0.1</code> だけが振られる）

接続可能なネットワーク先は <code>docker network ls</code> コマンドで確認することができ、専用の LAN 網（ネットワーク）も作ることもできます。具体的には <code>docker network create mynetwork</code> コマンドで作ったネットワークは、`docker run --network mynetwork mycontainer` でコンテナをそのネットワークに参加させることができます。

<code>--network none</code> ですが、「ネットワークにつながらなくていいアプリをコンテナ内で処理させたい」場合（例えば、画像の変換など）に利用します。つまり、知らないうちにアプリがネットワーク接続してデータを外部へ送信するような懸念を排除してくれます（そのかわり、外部からもポートを介してのアクセスはできなくなります）。

また、<code>docker-compose</code> でコンテナを起動した場合は、専用の仮想 LAN が自動作成され <code>docker-compose.yml</code> ファイル内に記載されているコンテナは、同じネットワーク内で起動します。

もちろん、YAML ファイル内でネットワークを複数定義し、各々のコンテナを割り振るといったこともできます。

例えば、デフォルトの `172.17.0.0/16` でなく `172.16.238.0/24` のセグメント（サブネット）の仮想 LAN 環境でコンテナを起動したい場合は、`docker-compose.yml` は以下のようになります（別途ネットワークを定義し、コンテナを割り当てていることに注目）。

```yaml
services:
  demo:
    image: alpine
    networks:
      - mynet

networks:
  mynet:
    ipam:
      driver: default
      config:
        - subnet: "172.16.238.0/24"
```

```shell-session
$ docker compose run --rm  demo ip addr show eth0
24: eth0@if25: <BROADCAST,MULTICAST,UP,LOWER_UP,M-DOWN> mtu 1500 qdisc noqueue state UP 
    link/ether 02:42:ac:10:ae:02 brd ff:ff:ff:ff:ff:ff
    inet 172.16.238.2/24 brd 172.16.238.255 scope global eth0
       valid_lft forever preferred_lft forever
```

このように複数コンテナの設定を一括で管理できるので「同じホスト OS」上に「複数コンテナを起動する」場合は <code>docker-compose</code> が使えると便利です。最新の Docker CLI（Win や macOS の最新の Docker Desktop）であれば、標準で同梱されています。

このような、複数コンテナを一元的に管理するツールを「<a href="https://ja.wikipedia.org/wiki/%E3%82%AA%E3%83%BC%E3%82%B1%E3%82%B9%E3%83%88%E3%83%AC%E3%83%BC%E3%82%B7%E3%83%A7%E3%83%B3_(%E3%82%B3%E3%83%B3%E3%83%94%E3%83%A5%E3%83%BC%E3%82%BF" target="_blank">オーケストレーション</a>・ツール」と呼びます。

このオーケストレーション・ツールには、同じホスト OS 内で Docker ネットワークを作れる <code>docker-compose</code>（現 `docker compose`） だけでなく、エンタープライズ版（事業向け）のオーケストレーション・ツールに、「同じネットワーク」内にある「複数のホスト OS」で仮想ネットワークを作る `docker swarm` や、「異なるネットワーク」間の「複数のホスト OS」上で仮想ネットワーク作る <a href="https://ja.wikipedia.org/wiki/Kubernetes" target="_blank">Kubernetes</a> などがあります。
:::

---

</details>

重要なことなので復習ですが、「コンテナごとにシンプルに１つの機能のみを実装させて使う」と言うのは、例えば Nginx などのプロクシ・サーバー／Apache などの Web サーバー／SQL サーバー／俺様 Web API サーバーなどを、各々にコンテナを立てるという意味です。

:::note info
<sub><sup>Nginx（エンジンX）は、正確にはプロクシ・サーバーではなくリバース・プロクシのサーバーです。本来プロクシは「LAN 内から外部にアクセス」があった場合の操作をするものです。キャッシュされていれば外部アクセスせずにキャッシュを返す、などです。逆に、リバース・プロクシは「外部のネットワークから LAN 内にアクセス」があった場合の操作をするものです。「ポート 5963 に `/foo/bar` のアクセスがあった場合は、LAN 内の `192.168.1.123:8080/foo/bar` にアクセスした結果を返す」といった<strong>外部からの窓口的な操作をするためのもの</strong>です。 つまり、Nginx のコンテナを起動させておいて OS は全てのアクセスを Nginx に向ければ、Docker ネットワーク内の転送をよしなにしてくれるだけでなく、他のコンテナは 1 つのサービスしか提供していないので、ポートのバッティング（重複）なども気にする必要がないといったメリットが生まれます。<br>この記事では「内外のネットワークの窓口を管理するもの」という意味で、便宜的に Nginx もプロクシと呼んでいます。なお、Docker でよく使われるリバース・プロクシには Nginx 以外に、[Traefik Proxy](https://traefik.io/traefik/)、[HAProxy](https://ja.wikipedia.org/wiki/HAProxy) など[色々あります](https://stackshare.io/nginx/alternatives)。</sup></sub>
:::

コンテナをサービスごとに個別に立てられれば、コンテナが開けないといけないポートも該当サービス用のポートだけでいいためシンプルになります。これは「当てたパッチが他のサービスに影響しないか」といった心配も減ります。

慣れないうちは、どうしても同じコンテナに複数サービスを入れたくなります。

「だって PHP や Go のビルトイン・サーバは大量のアクセスには向かないので、Apache と同じコンテナに入れたい」と思うかもしれません。

もちろん、それでも結構です。しかし、結局のところ、大量アクセスの<ruby>ハンドリング<rt>交通管理</rt></ruby>をしているのが Apache というだけで、PHP のプロセスは次々と作成されていきます（特に CGI 版の場合）。

また、そのコンテナが落ちた場合、Apache の下にいるプログラムが全て一緒に落ちます。さらに、そのコンテナにさまざまな機能（サービス）が凝縮されていた場合、負荷分散や増設がしづらくなります。俗に言う「[SPOF](https://ja.wikipedia.org/wiki/%E5%8D%98%E4%B8%80%E9%9A%9C%E5%AE%B3%E7%82%B9)」の 1 つになりかねないのです。

そこで Docker は、アクセス数が増えたら比例して複数の PHP や Go のコンテナを立てるという考え方をします。そして、Nginx のコンテナなどで負荷分散するという考え方をします。

つまり、接続が空いているコンテナに回したり、各々のコンテナに[持ち回りさせる](https://ja.wikipedia.org/wiki/%E3%83%A9%E3%82%A6%E3%83%B3%E3%83%89%E3%83%AD%E3%83%93%E3%83%B3)ことで[ロードバランス](https://ja.wikipedia.org/wiki/%E3%82%B5%E3%83%BC%E3%83%90%E3%83%AD%E3%83%BC%E3%83%89%E3%83%90%E3%83%A9%E3%83%B3%E3%82%B9)するということです。コンテナ同士が共通の DB さえ参照していれば、プロキシにより外部からは 1 つのサービスとして見えます。

これをサポートするのが docker-compose、Docker Swarm や Kubernetes といったオーケストレーション・ツールです。Kubernetes などは自動的に起動するコンテナを増減してくれます。

例えば、docker-compose の場合、`--scale` オプションで特定のサービス（コンテナ）を複数個起動させることができます。

```bash:myappサービスを3つ起動させて、Nginxなどのロードバランサーで持ち回りさせる例
docker compose up --scale myapp=3 --detach
```

- [具体的なサンプル](https://gist.github.com/KEINOS/c8d91d9436fa5603c031724aae1c1fbe) | gist @ GitHub

:::note info
🐒&nbsp;&nbsp;**つまりはクラスター**
docker-compose は、同じマシン内で Docker ネットワークを組むもの。Docker Swarm は同じ LAN 内の別々のマシンで Docker ネットワークを組むもの。Kubernetes は別々のネットワークの、別々のマシンで Docker ネットワークを組むもの、といった認識から入ると良いと思います。

どの方法であっても、外部から見ると 1 つの Docker ネットワークに見せることができます。つまり、外部からは 1 つのポート（例えば Web など）にアクセスしていても、実は裏側（Docker ネットワーク内）では余力のあるコンテナにつながっているという動作になります。そのため、Docker 関連では Apache より Nginx が好まれるのは、プロクシ機能（外部からの接続を内部のマシンに接続・転送する機能）が充実しているからです。どちらも Web サーバーなのですが。もっと高度なプロクシ機能を求める場合は Traefik などを検討するといいでしょう。

このような 1 つのサービスを複数マシンで提供する仕組みをクラスタと言います。もちろん複数サービス（複数のポート）も同様の仕組みで提供できます。

以下の動画は、英語で、いささかクセがあるのですが、Kubernetes の軽量版である <a href="https://k3s.io/">K3S</a> を使って、複数台の<ruby>ラズパイ 4<rt>Raspberry Pi 4</rt></ruby>でクラスタを組み、Minecraft サーバをゼロから建てる方法を具体的に紹介しています。

- <a href="https://youtu.be/X9fSMGkjtug" target="_blank">i built a Raspberry Pi SUPER COMPUTER!! // ft. Kubernetes (k3s cluster w/ Rancher)</a> @ Youtube

Kubernetes などのオーケストレーションに興味を持つも、微妙に実感がわかない方は、ざっと流れを観るだけでもピンとくると思います。注意として、<font color=darkorange>**ラズパイでクラスターを組んだとしても、処理速度が爆速になることはありません**</font>。同時アクセスに耐えられるだけで、処理速度そのものはラズパイの速度のままです。

:::

このように（`docker-compose` などで）複数コンテナを同じ Docker ネットワークで起動させるというのは、各々の機能が独立したサーバーとして、同じ LAN 内で動いているのと同義です。

そのため、「コンテナ間のやりとりが必要」な場合は「サーバー間のやりとりと同じ実装が必要」ということになります。つまり、UNIX ドメインのソケットが使えず、基本的に INET ドメインのソケットを利用することになります。

- [調べなきゃ寝れない！と調べたら余計に寝れなくなったソケットの話](https://qiita.com/kuni-nakaji/items/d11219e4ad7c74ece748) @ Qiita

「INET ドメインのソケット」と言うのは「ネットワークカードにつながった通信くち」という意味です。

UNIX/Linux では、データをやりとりする窓口を「ソケット」と呼び、その通信先がネットワークカードの場合を「INET ドメイン」と呼びます。

また、UNIX/Linux ではソケットは実体のないファイルとして作成され、その**ファイルを読み書きすることで、ソケットの接続先にデータを送受信します**。

そのため Linux の場合、Bluetooth、シリアル・ポートや USB など、各種デバイスの通信も、このソケット（のファイル）を読み書きすることでアクセスできます。そして、読み書きする際のバイナリ・データの書式が、俗に言う「通信[プロトコル](https://qiita.com/KEINOS/items/cb7f629268b90ef6493d)」です。

そして、ネットワーク・カード・デバイスに接続されたソケットが「INET ドメインのソケット」になります。

「ドメイン」とは、「属する」とか「所属する」とか「下に何かがぶら下がっている組織」的なものを言います。そのため、「INET ドメイン」と呼ぶのは、ネットワークカードに属するプロトコルが複数あるからです。

例えば `HTTP`, `SSH`, `FTP` などです。そして、**各々のプロトコル用に用意したソケットをポートと呼びます**。

いずれのポートも、読み込むとバイナリ・データが流れてきたり、書き込めるだけなのですが、利用するプロトコル（通信ルール）がわかっているので、通信できる（サービスと疎通ができる）という塩梅です。

まとめると、「INET ドメインのソケットを利用する」というのは「任意のポートで、インターネットで使われるプロトコルで通信する」ということです。

具体的には 8080 ポートで待機する WebAPI を作り、専用のコンテナを立て、他のコンテナから 8080ポートに HTTP リクエストを送り、レスポンス結果を利用する、といったイメージです。

:::note info
Linux において、デバイスのソケットは（シンボリックリンクなどのように）実態がないとは言えファイルです。そのため複数プロセスが同時に同じファイルにアクセスできません。そこで、代表してファイルを開き、ファイルの読み書きを一手に請け負うプログラムも存在します。例えば [MIDI](https://ja.wikipedia.org/wiki/MIDI) やオーディオ・デバイスの場合は [JACK](https://ja.wikipedia.org/wiki/JACK_Audio_Connection_Kit) などです。
:::

さて、閑話休題。

問題は、各々のコンテナの API を利用する際の認証です。

もちろん、Docker ネットワークと言う閉ざされたネットワーク内での通信なので、認証無しで通信しても構わないことが多くあり、一般的な面もあります。（実装が楽になりますし）

しかし、コンテナのサービスを外部にも公開することを考えてセキュリティを重視した場合、コンテナ間通信の認証も考える必要があります。

一般的には「OAuth などを実装し、各々のコンテナにアクセストークンを取得させる」「SSL/TSL 通信で行う」などです。つまり、同じ Docker ネットワーク内に、意図しないプロセスがダミー・リクエストをしたり、通信を傍受しているコンテナがある可能性を考えて通信を制限したり、暗号化するケースです。

とは言え、全てのコンテナが OAuth のサーバー・クライアントの実装だけでなく、アクセストークン取得や共通暗号鍵の取得フローまでも実装が必要となると大変です。

そこで、ホスト OS 側から各々のコンテナに直接アクセストークンや暗号鍵を渡すことで、実装が楽になります。

つまり、**１つのコンテナにあれこれと機能（アプリやサービス）を詰め込みすぎないのであれば、コンテナ起動時に環境変数で渡しても Docker の仕組み的に問題ない**ということです。

もちろん、<font color="darkorange">環境変数に限らずファイルのマウントでも同じことが言えるのですが、**メリット・デメリットを理解せずに盲目的に使うのは危険**</font>です。

くどいのですが、中で何をしているかわからない[プロプライエタリ](https://ja.wikipedia.org/wiki/%E3%83%97%E3%83%AD%E3%83%97%E3%83%A9%E3%82%A4%E3%82%A8%E3%82%BF%E3%83%AA%E3%82%BD%E3%83%95%E3%83%88%E3%82%A6%E3%82%A7%E3%82%A2)なブラックボックスタイプのライブラリや、得体の知れないサードパーティのライブラリを使っていたり、メンテナンスされていない（脆弱性の Issue や PR が放っておかれている）ものを一緒に入れているコンテナの場合は、大いに注意が必要ということでもあります。

他にもアプリの脆弱性をつかれて、コンテナにマイニングのアプリをインストールされたりと、考えるとキリがありません。

Docker 内で使うアプリのライブラリすら、GitHub や Gitlab などにフォークしたものをわざわざ使う人がいます。これは「知らぬ間に得体のしれないものが入れられないように」というリスク管理のためで、フォークを本家に追随する手間やアプデ内容を毎回チェックする手間を惜しんでもセキュリティが気になるからです。

しかし、**これは Docker やコンテナに限らず「サーバーの注意事項」と、何ら変わらない**ことにお気づきでしょうか。

何度も言及していますが、Docker のプラクティスの 1 つに「`root` 権限を持たないユーザを作成して、Dockerfile の最後に `USER` で指定しておく」というものがあります。

しかし、この設定はコンテナ内でパッケージがインストールできなくなるため、開発用途に使っている場合は `root` のままの人も多いかもしれません。コンテナ起動時に毎回 `--user root` を指定しないといけないため、面倒だからです。

`root` 以外のユーザにする**この設定は[ポカヨケ](https://ja.wikipedia.org/wiki/%E3%83%9D%E3%82%AB%E3%83%A8%E3%82%B1)の役割をするもの**です。

特に `--privileged` オプションを付けてコンテナを実行すると、コンテナ側からホストにアクセスできるようになります。この時、コンテナのユーザが `root` ユーザだと `root` でホストにアクセスしていることになります。

「（そんな wwww `--privileged` なんて、わざわざ付けたりしないよ）」と思うかもしれません。

しかし、そもそもホストのデバイスへのアクセスが必要な IoT 関連の記事には `--privileged` オプションを付けるものがあります。

また、機械学習系のコンテナといった GPU へのアクセスが必要なものは `--privileged` オプション必須になります。そのような[コンテナにマイニング・アプリが組み込まれていた](https://www.trendmicro.com/en_us/research/19/e/infected-cryptocurrency-mining-containers-target-docker-hosts-with-exposed-apis-use-shodan-to-find-additional-victims.html)ら、ただでさえ Docker で（微々たるものとはいえ）リソースを食われているのに、貴重なリソースをタダ乗りされることになります。

また、古い Windows や Mac の記事や掲示板では、データの永続化のためにハードディスクに触れないと吸った揉んだする系で、やれ `--link` オプションを付けろだの、`--privileged` を付けてみろだのといった案内があったりします。

よくわからず試行錯誤の末、動いたものの、他の人もなぜ動いているのかわからないままだったり、さらりと `docker-compose.yaml` に追加されちゃっていたり。

結局、「Docker だから安全」「VM だから安全」「CentOS だから安全」といったことはないのです。

結局は、サーバーの基礎知識と体で覚えた経験が必要なのです。で、いろいろと吸った揉んだした苦い経験をした人が Docker の利便さに気づき、「（同じ試行錯誤するなら） Docker 便利だよ」と、せっかちさんなエンジニアに言っているだけなのです。

### 使っては捨てろ（写経よりは乱取り）

こんなトッ散らかった文を、よくぞここまで読んでいただきありがとうございます。

そんな我慢強いアナタに、最後に Docker（or Podman）に関してアドバイしたいことがあります。

ここまでセキュリティのことを、いささかうるさくゴチャゴチャと言って来ました。

しかし、そんな状態のものを業務の本番に使うようなことはないと思います。しかも、このセキュリティの話しは Docker に限らず、VM（仮想マシン）やパソコンであっても同じことが言える内容だと思います。

セキュリティは忘れてはならない事項ですが、**Docker 初心者なら、まずは Docker のメリットを活かして、触って覚える方が良い**と思います。

VirtualBOX や VMWare などの VM は、仮想的なパソコンと同じです。そのため、ハードと同じようにイメージからインスタンスを作成して、そのインスタンス上に環境構築したら、それを**使い続けることがメイン**です。

しかし、**Docker の場合は、使い捨てがメイン**と考えた方がいいです。

つまり、シンプルな用途ごとに Dockerfile を作ることから始めるのです。

---

<details><summary></summary><div><br>

【WIP 書きかけ】

<h4>Docker のコンテナを小分けにする理由の細かい事</h4>

この記事を読まれているということは、Docker は仮想マシンの１種またはそのたぐいであることはご存知だと思います。そして [VirtualBOX](https://www.virtualbox.org/) や [VMWare](https://www.vmware.com/jp.html) といった仮想マシン系アプリを見聞きしたこともあると思います。

VirtualBOX や VMWare は[ハイパーバイザ](https://ja.wikipedia.org/wiki/%E3%83%8F%E3%82%A4%E3%83%91%E3%83%BC%E3%83%90%E3%82%A4%E3%82%B6)型と呼ばれ、ハードウェアを仮想化して、その上に任意の OS を乗せる技術です。

`Docker` と何が違うのかと言うと、恐れずに言うなら **`Docker` は「プロセス」を仮想化した技術である**ということです。

どういうことか。

例えば、以下の Bash スクリプトを実行したとします。

```bash:sample.sh
#!/bin/bash

while true
do
  sleep 60
done
```

上記は `sleep` を 60 秒おきに繰り返す無限ループです。当然メモリに常駐して永遠と処理を繰り返すことになります。つまり「このスクリプトのプロセスが走っている」ということです。試しに確認してみましょう。

```shell-session:サンプル・スクリプトの実行とプロセスの確認
$ # スクリプトの確認
$ cat ./sample.sh
#!/bin/bash

while true
do
  sleep 60
done

$ # スクリプトに実行権限を与える
$ chmod +x ./sample.sh

$ # スクリプトを実行し、"&" で裏で動かす
$ # 下記 4144 は、PID（プロセスID）4144 番で動いているという意味です
$ ./sample.sh &
[1] 4144

$ # 現在動いているプロセスから、PID 4144 があるか確認する
$ ps -p 4144
  PID TTY           TIME CMD
 4144 ttys001    0:00.01 /bin/bash ./sample.sh

$ # 無限ループなのでプロセス ID を指定して kill で強制終了する
$ kill 4144
[1]+  Terminated: 15          ./sample.sh
```

どのプログラムも実行するとコンパイルなどを経て「カーネルに優しいバイナリの状態」になり、メモリに呼び出されプロセスとして指示があるまで待機します。

:::note info
<sub><sup>
🐒 &nbsp; <strong>【カーネルとは】</strong> OS におけるカーネルとは「OS の基幹となる小さなプログラムの集合体」です。
特に OS とハードウェアの接点、つまりソフトウェアとハードウェアの仲介を担います。この上に、ドライバであったり、プログラムが載っかって、よりマクロな機能を OS として提供して行きます。
英語では <code>Kernel</code> と書き、「何かの中核もしくは本質となるもので、何かに覆われており直接触ることができず、見つけるのが大変なもの」と言う意味を持ちます。日本語で言えば「<ruby>種<rt>タネ</rt></ruby>」「核」「（牡蠣などの）身」と言った意味を持ちます。
このことから、「ユーザーは直接触ることができず、シェル（殻）を通さないと触れない OS の中核となるプログラム群」を（OS の）カーネルと呼んでいます。他にも、機械学習では、そのデータのままではわからない（見えない）ものから、「データの核」となりうる新しいデータの列（特徴）を出力する「カーネル関数」がありますが、同じ語源です
ちなみに、トウモロコシの実である <code>corn</code> の語源が <code>Kern-nel</code> の <code>kern</code> です。
</sup></sub>
:::

そして、待機しているプロセスは、PID（プロセス ID）順に呼び出されます。カーネルは、そのたびにバイナリデータを CPU の種類に合わせて実行（処理を命令）します。

通常この PID 順に実行する処理を担うのは `PID 1` で起動して走っているプログラムです。OS によって異なりますが、`initd` `systemd` `launchd` などです。

このことから、「OS が異なっても、同じカーネルと同じ CPU であれば実行できる状態になったもの」が「プロセス」と言えます。

<strong>「じゃぁ、このメモリ上にあるコンパイル済みのプロセスをイメージに落として、別途メモリに読み込ませればいいじゃん」</strong>というのが `Docker` です。そして `Docker` は、コンテナのプロセスが通信するのをコントロールすることで、カプセル化しているのです。

つまり、VirtualBOX や VMWare などのように OS やハードウェアを丸ごとシミュレーション（仮想化）しなくても、Linux のカーネルの上に、プロセスと、プロセスが必要とする[フレイバー](https://ja.wikipedia.org/wiki/Linux%E3%83%87%E3%82%A3%E3%82%B9%E3%83%88%E3%83%AA%E3%83%93%E3%83%A5%E3%83%BC%E3%82%B7%E3%83%A7%E3%83%B3#%E6%A6%82%E8%A6%81)（Linux 系 OS の各々の特徴）のプロセスをカプセル化したものを乗っければ動くと言う理屈です。

これが VirtualBOX などの[ハイパーバイザ](https://ja.wikipedia.org/wiki/%E3%83%8F%E3%82%A4%E3%83%91%E3%83%BC%E3%83%90%E3%82%A4%E3%82%B6)型と比べて軽量であると言われる理由です。

しかし、macOS や Windows の場合、カーネルがそもそも異なります。そのため、macOS と Windows は Linux の仮想マシン（VM）を動かし、その上で Linux のコンテナを動かします。macOS と Windows ユーザーが VirtualBOX や VMWare と違いがわからなくなる原因がここにあります。

Docker のポテンシャルをフルに活かしたいのであれば、Linux 系のマシンで動かすのがベストでしょう。

まとめると、この必要なプロセスを1つにまとめて保存したものが Docker イメージで、その設定内容が Dockerfile、そして、それをメモリに読み込んだものが Docker コンテナと考えるとわかりやすいと思います。

厳密には、このアイデア自体は Docker の専売特許でもなく、１０年以上前から Linux では存在した概念です。

元々 Linux では、<code>[chroot](https://ja.wikipedia.org/wiki/Chroot)</code> と言う概念があり、ディレクトリごとに異なる環境を構築して切り替えられる機能を持っていました。プログラム言語で言えば、Python の場合は `pyenv`、PHP では `phpenv`、ruby なら `rbenv` のように、各々必要な物を1つのディレクトリにまとめ、切り替えると言う仕組みに似たものです。

この `chroot` の仕組みは、不特定多数のユーザーに1つもしくは複数のサーバーで複数の環境を提供するのに適しているため Linux がサーバーとして好まれる理由の1つにもなりました。当然のように Windows Server なども、この概念を取り入れたりしています。

この概念をカーネルベースにしたものが [KVM](https://ja.wikipedia.org/wiki/Kernel-based_Virtual_Machine)（`Kernel-based Virtual Machine`）と呼ばれるものです。

Docker は、単純に、この概念を利用した成功者に過ぎないのです。おそらく、その成功の理由としては、Golang（[Go 言語](https://ja.wikipedia.org/wiki/Go_(%E3%83%97%E3%83%AD%E3%82%B0%E3%83%A9%E3%83%9F%E3%83%B3%E3%82%B0%E8%A8%80%E8%AA%9E))）で開発されたことにより、マルチ・プラットフォーム対応やインストールが容易だった（扱いやすい）こともあったのだと思います。

コンテナ技術の歴史に興味のある方は[「KVM 歴史」でググる](https://www.google.com/search?q=KVM+%E6%AD%B4%E5%8F%B2)などしてみたり、以下の記事なども参考にすると面白いと思います。

- [コンテナってなんだろう― 「コンテナ」の概要を知る](https://thinkit.co.jp/article/17301) @ ThinkIT

<h5>コンテナの種類</h5>

さて、コンテナの話しに戻りますが、Windows ユーザーの場合、Docker を初めて起動すると「Windows コンテナ」と「Linux コンテナ」の選択肢があるのを不思議に思った人もいると思います。

これは、「対応するカーネルが違う」と言う意味です。

Windows コンテナを選択した場合は [Windows カーネル](https://ja.wikipedia.org/wiki/%E3%82%AB%E3%83%BC%E3%83%8D%E3%83%AB#%E3%83%8F%E3%82%A4%E3%83%96%E3%83%AA%E3%83%83%E3%83%89%E3%82%AB%E3%83%BC%E3%83%8D%E3%83%AB)、Linux コンテナを選んだ場合は Linux カーネル上でコンテナを動かすということです。そのため、Windows コンテナは Linux カーネル・モードでは動きません。カーネルが異なるためです。もちろん、その逆もしかりです。

しかし、ホスト OS が Windows の場合、カーネルは Linux ではありません。そのため、Linux コンテナを選択した場合は、仮想 Linux 環境上でコンテナを動かします。これは[特定の Linux OS](https://ja.wikipedia.org/wiki/Linux%E3%83%87%E3%82%A3%E3%82%B9%E3%83%88%E3%83%AA%E3%83%93%E3%83%A5%E3%83%BC%E3%82%B7%E3%83%A7%E3%83%B3#%E6%A6%82%E8%A6%81) でなく、Linux カーネルを仮想化しているので、ハイパーバイザーより軽量となります。イメージとして、Linux の Wine のようなイメージです。

対して、Windows より先に Docker 環境が整った macOS ですが、「macOS コンテナ」とは余り聞きません。

macOS は UNIX で Linux ではありません。macOS のカーネルも <code>[Darwin](https://ja.wikipedia.org/wiki/Darwin_(%E3%82%AA%E3%83%9A%E3%83%AC%E3%83%BC%E3%83%86%E3%82%A3%E3%83%B3%E3%82%B0%E3%82%B7%E3%82%B9%E3%83%86%E3%83%A0))</code> と呼ばれる BSD 系のものです。しかし、UNIX であるため Linux とは比較的相性が良いこともあり、仮想 Linux 環境が作りやすく、相性も Windows と比べて良いことから、独自のコンテナ仕様（macOS コンテナ）を作るよりは最初から仮想マシン上で動かすことを優先したことが大きいと思われます。

そして、Windows 同様、macOS の場合も仮想的に Linux 環境を作り、その上で Docker コンテナを動かしています。

このように、Windows や macOS などの異なる OS でも Docker を動かせるのは Linux がオープンソースであることの強みです。逆に Linux 上で動く公式の仮想 [Windows カーネル](https://ja.wikipedia.org/wiki/%E3%82%AB%E3%83%BC%E3%83%8D%E3%83%AB#%E3%83%8F%E3%82%A4%E3%83%96%E3%83%AA%E3%83%83%E3%83%89%E3%82%AB%E3%83%BC%E3%83%8D%E3%83%AB)や Darwin カーネルがないのは、Microsoft 社も Apple 社も、その仕様を一般公開していないからです。

このことは、OS が最初から Linux であれば Linux カーネルをシミュレーションする必要がない、と言うことでもあります。そのため、Docker のパフォーマンスが一番良いのは Linux 上で Docker を動かすことです。

Windows は現在は `VM`（仮想マシン）上で動かしていますが、[WSL2](https://ja.wikipedia.org/wiki/Windows_Subsystem_for_Linux) により Linux で言う <code>[Wine](https://ja.wikipedia.org/wiki/Wine)</code> のようにネイティブ対応してきているので、今後 macOS を凌ぐ使い勝手に期待したいところです。

いずれにしても **Docker は Linux カーネルで動くプロセスをイメージに落としたもの**と考えるのが一般的です。

`docker pull hoge/hoge` と打ってダウンロードされるイメージがそれに該当します。そして、繰り返しますが、そのイメージを起動してメモリにマウントしたものがコンテナになります。

<h5>イメージは CPU 依存</h5>

Docker イメージを理解するのに大事なのが「コンパイル済み（CPU に最適化済みの状態）」ということです。逆に言えば、別の CPU の種類の環境で作成（ビルド）された Docker イメージは使えないということです。つまり、同じ Linux カーネルであっても CPU が異なる場合は動かせないのです。

例えば x86_64 互換（Intel/AMD）の CPU でビルドされたイメージは、他の種類の CPU では動きません。そのため `docker pull` でダウンロードしたイメージが、RaspberryPi などの ARM 系の CPU のマシンでは動かないことがよくあります。

その場合は、Dockerfile や必要なスクリプトをダウンロードして、`docker build` で自分の CPU にあったイメージを作成しないといけません。

次に大事なのが「プロセスである」ということです。CPU は PID 順にメモリ上のプロセスを次々に呼び出して実行します。この時、１つのプロセスが時間（処理時間）を食うと他のプロセスに支障を来たすのは想像できると思います。

よく [Docker のベストプラクティス](https://docs.docker.com/develop/develop-images/dockerfile_best-practices/#decouple-applications)で、「Dockerfile は１プロセスに抑える」と言われます。

これは「Dockerfile 内で複数プロセスを実行する処理はよくない」ということなのですが、どういうことかと言うと、先の `sample.sh` を思い出してください。このスクリプトを実行すると１プロセスが実行されます。

```shell-session:ループ処理で常駐し続けるスクリプトを実行する例
./sample.sh &
```

そして、Dockerfile でコンテナ起動時に実行するコマンドは `ENTRYPOINT` ディレクティブ（指示子）で指定するのですが、このスクリプトを実行させてみます。

```dockerfile
FROM alpine
...
ENTRYPOINT [ "./sample.sh &" ]
```

上記の場合は、コンテナは１プロセスになります。それでは、単１コンテナ、複数プロセスにしてみましょう。

- メインの無限ループ・スクリプト

```sh:sample.sh
#!/bin/sh

while true
do
  sleep 60
done

```

- 複数プロセスを起動する `ENTRYPOINT` のスクリプト

```sh:entrypoint.sh
#!/bin/sh

# このスクリプトは３つのプロセスを実行します。

# バックグラウント実行
/root/sample.sh &
/root/sample.sh &

# フォアグラウンド実行
# すべてをバックグランドで実行すると、この entrypoint.sh 自体が正常終了
# してしまい、コンテナも終了してしまう。そのため、最低 1 つはフォアグラウンドで
# 動かしておく必要がある。
/root/sample.sh

```

- コンテナのイメージ用 Dockerfile

```dockerfile:Dockerfile
FROM alpine

COPY sample.sh     /root/sample.sh
COPY entrypoint.sh /root/entrypoint.sh

ENTRYPOINT ["/bin/sh", "/root/entrypoint.sh"]

```

- 実行の仕方

```shell-session
$ # ディレクトリ構成
$ ls
Dockerfile	entrypoint.sh	sample.sh

$ # イメージのビルド
$ docker build --no-cache -t test:local .
...

$ # コンテナの起動（detach でバックグラウンド実行）
$ docker run --rm --detach test:local
a7922249f82e5b2823d2e7662c49ddd670ccf10397f489561e14378c2219d9ab

$ # コンテナ内で実行されているプロセスを確認する

$ # コンテナの起動 ID を確認
$ docker container ls
CONTAINER ID        IMAGE               COMMAND                  CREATED             STATUS              PORTS               NAMES
a7922249f82e        test:local          "/bin/sh /root/entry…"   2 minutes ago       Up 2 minutes                            busy_stonebraker

$ # 起動中のコンテナに接続し、ps コマンドを実行
$ docker container exec a7922249f82e ps
PID   USER     TIME  COMMAND
    1 root      0:00 /bin/sh /root/entrypoint.sh
    8 root      0:00 {sample.sh} /bin/sh /root/sample.sh
    9 root      0:00 {sample.sh} /bin/sh /root/sample.sh
   10 root      0:00 {sample.sh} /bin/sh /root/sample.sh
   20 root      0:00 sleep 60
   21 root      0:00 sleep 60
   22 root      0:00 sleep 60
   23 root      0:00 ps

```

上記で１つのコンテナに３つの `sample.sh` のプロセスが起動していることがわかると思います。そして、各々が呼び出している `sleep` コマンド（プログラム）もプロセスとして実行されているのも確認できます。もちろんプロセスを確認するために `ps` コマンドも実行したので、`ps` もプロセス一覧にいます。

これは極端な例ですが、SQL サーバーや Web サーバーなど、メインスクリプトとは別に起動させないといけないプロセスは多いと思います。

しかし忘れがちなのが、ホスト OS（ここでは Docker を動かしているマシン）から見れば、各コンテナは Docker エンジンのプロセス内で管理されていることです。

つまり **Docker が落ちると、すべてのコンテナが落ちます**。

そのため、１つのコンテナに色々サービスや機能を詰め込み過ぎて、コンテナが落ちた時にサービス全体が一気に落ちてしまうだけでなく、どのサービスが問題を起こしているかの特定にも時間がかかります。

実は、Docker は `docker-compose` と組み合わせると**コンテナのプロセスが落ちると自動で再起動する仕組みを持っています**。

具体的には `docker-compose` の ["restart" 指示子](https://docs.docker.com/compose/compose-file/#restart)を "no" 以外に設定します。

この設定で `docker-compose up` されると、コンテナが落ちても自動で再起動されます。それだけでなく、`docker-compose down` しない限り、OS が（アップデートなどで）再起動しても Docker が起動すると `docker-compose` が呼び出され自動で各のコンテナも起動します。

そのため、Web サーバー用／SQL サーバー用／メインスクリプト用と複数の Dockerfile、つまり複数コンテナにわけて構築するのが良い（ベストプラクティス）と言われる理由の１つです。

<h5>コンテナをわけることのデメリット</h5>

プロセス（サービス）ごとにコンテナをわけるデメリットもあります。（長い目で見るとメリットであったりもするのですが）

先にも少し述べましたが、コンテナ間の通信が面倒であることです。ローカル内のアプリ間通信と言うより、サーバー間通信になるからです。

というのも、コンテナとコンテナの通信は、サーバーとサーバーの通信と同じ方法でないといけません。たとえば HTTP、SSH や FTP といったネットワーク通信が必要です。

また、Python の実行結果を PHP にパイプ渡しする、といったこともコンテナにわけると手軽にできなくなります。（１つのコンテナに Python と PHP をインストールすれば話しは別です）

他にも特定のディレクトリにファイルを吐き出して、他のプログラムで読み込むといったローカルならではの手法も、ネットワーク通信、つまり Web API を実装しないといけないというデメリットがあります。（共通のボリュームをマウントするという力技もありますが、やはり長い目で見るとリスクになります）

同一サーバー内でのプロセス同士の通信、いわるゆ[プロセス間通信](https://ja.wikipedia.org/wiki/%E3%83%97%E3%83%AD%E3%82%BB%E3%82%B9%E9%96%93%E9%80%9A%E4%BF%A1)は Docker でも可能です。しかし、とても高いリスクを伴います。

実はコンテナ起動時に `--link` オプションをつけるとコンテナ間でプロセス間通信はできるのですが、セキュリティの観点から[近い将来のバージョンで排除されることが決まっています](https://docs.docker.com/network/links/)。つまり **`--link` を前提とした環境を構築しても使えなくなることが決まっている**ということです。

他にも `privileged` と呼ばれる特権モードでコンテナを起動すると、`root` などのユーザーであれば Docker エンジンを介して他のコンテナにアクセスすることができます。**問題は、コンテナ内からホスト OS 側にもアクセスできてしまう**ということです。

`--link` オプションと `privileged` モードの違いは、前者はプロセスをつなげることを前提としており、後者はサウンド・カードや Web カムといった、ホスト OS 側のデバイスにつなげることを前提としていることです。いずれもホスト OS を触れるため、セキュリティホールを作ってしまう可能性は大いにあります。

これが「コンテナ起動時のデフォルトユーザーを `root` にしない」と言うプラクティスも、これに起因しています。

Docker を使うメリットの１つにセキュリティがあります。しかし、これは「ホスト OS にコンテナから触らせない」ということが前提です。

<h6>サーバの KISS</h6>

１つのパソコンやサーバーに複数サービスを入れ過ぎてしまうと、どのサービスがどのポートを使っているのかわからなくなるなど、監視対象が多くなり煩雑になる経験をした方は多いと思います。

また、**再起動したくても、他の稼働中のサービスやプログラムがあるためできない**といったヤキモキなどもあると思います。

こういった場合、従来は別のハードを用意して専用機にしたり、VM の場合は別の仮想マシンを立ち上げて専用にしたりします。しかし、どちらも OS が丸ごと入っているので、「（なんか無駄だよなぁ）」と感じることでしょう。

ここで、先の「コンテナをわけることはサーバーをわけるのと同じ」ということが活きて来ます。

1. アプリが必要な OS のライブラリやモジュールなど
2. アプリ本体
3.（非コンパイル型言語の場合は「プログラム言語のランタイム」）

だけをコンテナに 1 つにしたものを Linux カーネル上で動かして 1 サーバとして機能させるのです。

そして「コンテナ間通信＝サーバー間通信」ということになります。

逆に言えば、**別のコンテナの内部で起きていることは他のコンテナからはわからない**ということでもあります。むしろ知る必要もなく、リクエストに対してレスポンスが返ってくるだけでいい、ということです。

そのため、主なプロセスごとにコンテナをわけることは、各コンテナは自分のコンテナが提供しているサービスに注力できるので、不要なポートは安心して閉じれるということです。また、余計なサービスやアプリも入れていないので、意図しない脆弱性に悩まされる率も減ります。

これがセキュリティの面でのメリットです。

また、たとえコンテナの中で `root` 権限を持ったとしても、他のコンテナ（サーバー）にも `root` で入れるというわけではないことも大事です。

これは Web サーバーを外部に公開して、ハッキングで侵入されたとしても、他のコンテナにアクセスしにくいだけでなくホスト OS のローカルにも入れないということにつながります。

しかし、プロセス間通信のために `privileged` モードで構築すると、この前提が崩れてしまうので諸刃の刃といったところでしょうか。

</div></details>