Dockerなんもわからん部～新入部員のあがき～

# Dockerなんもわからん
Dockerがなんもわかりません。理解するためにおててを動かしていきます。

## Dockerって何？
コンテナ・・・　コンテナ・・・？
実行環境を整える・・・？？？
まずはgpt4o君に聞きました。今回のきは4o君におんぶにだっこです。


>__まずはイメージで理解してみよう！__
>普通のアプリ開発だと：
>「このアプリ、Pythonのバージョンが違って動かないよ！」
>「ライブラリが入ってない！」
>ってことがよくある。
>
>__Dockerを使うと：__
>必要な環境（OS、ライブラリ、アプリ本体）をひとまとめにして「コンテナ」にする。
>どこでもそのコンテナを動かせばOK。
>「えっ、Pythonのバージョン？もう入ってるよ（ドヤ）」って感じ。

・・・・？？？？？
まあ共通的な隔離環境を簡単に提供できるって感じ・・・？

>__🧱 3つのキーワードだけ覚えて！__
>| 用語 | 簡単解説 |
>|:-:|:-:|
>| Dockerfile  | コンテナのレシピ（何を入れるかを書く）  |
>| Image（イメージ)  | レシピから作った完成品の箱の設計図  |
>| Container（コンテナ）  |  イメージから動かしてる実物（箱そのもの) |

らしい。ほーん。
次は実例を見るべ。

## dockerfileを見よう！

まずは、gpt4oに生成させたDockerfileの実例を見ます。
```dockerfile
FROM python:3.10
COPY . /app
WORKDIR /app
RUN pip install -r requirements.txt
CMD ["python", "main.py"]
```
コードだけ見ると、pythonをインストールして、そこにrequirements.txtでパッケージinstall、というように見える。ふむふむ。
じゃあfromって何？　聞いてみました！

### dockerfileのfromとは？

>🔧 FROMって何をしてるの？
> ズバリ！
>
>「このDockerイメージは〇〇を土台（ベース）にして作りますよ〜」と指定している。
>
>ベースイメージの中には、
>
> * OS（例：Ubuntu、Alpine）
>
> * ランタイム（例：Python、Node.js）
>
> * その他ツール（例：Anaconda、Android SDK）
>
> が すでに入っていることが多いの！

なるほどね。つまり本当に一番最初の方向性決定なわけだ。
つまりここでOSとランタイム、その他ツールの大枠をセットで決めていると。
ここのfromの引数は、Dockerhubにいろいろあるっぽい。
例に出されたのは、こんな感じ
```dockerfile
# UbuntuベースのDockerfile
# これでubuntu環境のimageを引っ張ってこれる
# ubuntu環境を作れる
FROM ubuntu:22.04

# Node.jsをインストール
# これでOS(ubuntu系？)を入れた環境と
# そこにNode.jsが入った環境を作れる
FROM node:20

# anaconda3をインストール
# これでpathを通したうえでanacondaを
# インストールした環境を整備
FROM continuumio/anaconda3
```

### COPYとWORKDIRとは
どういう意味ですか？gptに聞いてみました！

>```dockerfile
># コンテナの作業ディレクトリを /app に設定(cd　/app)
>WORKDIR /app
>
># ホストPCのカレントディレクトリの内容を /app にコピー
>COPY . /app
>
># → 以降のRUNやCMDは、/appで実行される
>```
だそうです。つまり、作業ディレクトリを変えて、そこにファイルをコピーしているそうです。なるほど
ついでに、.dockerignoreについても教えてくれました。
これは、copyの際にtarファイルに圧縮してコピーしているそうですが、その際にコピーしないファイルを一覧で記録しているそうです。
例えば、環境変数をまとめたファイル(.env)などを入れておくことで情報漏洩を防いだりできるらしい。なる

### RUNとCMD、Entrypointについて
最後にRUNとCMD、ついでにEntrypointについて聞いてみました。
RUNとCMDについては、コンテナのビルド中に使うか、コンテナ起動時に使うかという違いがあるそうです。
また、Entrypointは、コンテナ起動時に使うコマンドということは同じですが、CMDと違って上書きが出来ないという特性があるそうです。
実際に、RUNのコマンドはパッケージのインストールになっていることから、ビルドする際には必要だが、コンテナを起動するたびに必要な作業ではないことが分かると思います。
また、CMDとEntrypointの例を見てみます。
まずはCMDについて
```dockerfile:CMD_test
FROM ubuntu
CMD ["echo", "Hello from CMD"]
```
```bash:CMD_test run
# 通常実行時
docker container run CMD_test
# echo Hello from CMD

# CMDのコマンド変更
docker container run CMD_test echo "Override!"
# echo Override
# container起動時のコマンド故、引数を設定するとそのたびに変更できる
# コンテナのビルド時の変数ではないからこそいじれる
```
次にEntrypointについて
```dockerfile:Entrypoint_test
FROM ubuntu
ENTRYPOINT ["echo"]
CMD ["Hello from ENTRYPOINT"]
```

```bash:Entrypoint_test run
# 通常実行時
docker container run Entrypoint_test
# echo Hello from ENTRYPOINT

# Entrypointのコマンド変更
docker container run Entrypoint_test "Override!"
# echo Override
# echo自体は変更できないが、CMDの方は変更できた。
```

使い分けとしてはこんなかんじらしい
| ケース | おすすめ |
|:-:|:-:|
|  デフォルトはあるけど、柔軟に変更したい | CMD  |
|  絶対にこのコマンドで起動してほしい（引数だけ変えたい） | Entrypoint  |
| 両方使って「コマンド + 引数」構成にしたい  | 両方  |

ちなみに、書き方としてはshellっぽく
```
ENTRYPOINT python main.py
```
でもいけるけど、安定性とかの問題？から
```
ENTRYPOINT ["python", "main.py"]
```
のexec形式のほうがいいらしい。へー

### 補足：anacondaのdocker imageを見てみよう
fromの部分で特に気になったのが、anacondaの環境整備だった。実際にimageファイルを見てみたところ、環境整備をしていることが分かった。
まず、以前WSL2上で環境構築したときの記録はこんな感じ

https://qiita.com/hotate_2235/items/87654a1348e51774c224#2-wsl%E3%81%ABminiconda%E3%82%92%E5%B0%8E%E5%85%A5

手順としては、install用のシェルスクリプトをinstallして実行していた。
(ちなみに、conda install用のシェルスクリプトそのものは、500行くらいまで実行手順やライセンス情報が書かれてたけど、それ以降は文字化けしてた。50000行くらい)

ここで、実際にanacondaを作るdocker imageを見てみました。
(fromで引っ張ってこられてるimageの中身)

https://hub.docker.com/layers/continuumio/anaconda3/latest/images/sha256-3e8f002ea70b6fe2682609b19c799dcb12b9a0009f0400f1a47bd61b733d7478

<details><summary>dockerfileの中身を見ようの会</summary>

>```dockerfile
># debian(OS?)のインストール
>ADD file:702193928cded0bcec5edbf4a5660961e7caef8c9d9cafea3337b7f6720c4464 in / 
># bashシェルの起動
> CMD ["bash"]
># コンテナ内部の言語設定
>ENV LANG=C.UTF-8 LC_ALL=C.UTF-8
># パスの設定(anaconda実行に必須！)
>ENV PATH=/opt/conda/bin:/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin
># ビルド時にのみ使われる変数を設定している(ARGコマンド)
># ENVはコンテナ実行中にもビルド中にも使える変数設定
># conda install用のシェルスクリプトDLリンク設定
># x86_64 CPU用のlinux向け変数設定
>ARG INSTALLER_URL_LINUX64=https://repo.anaconda.com/archive/Anaconda3-2024.10-1-Linux-x86_64.sh
># インストールOSごとにファイル破損検知のためのハッシュ値の設定
>ARG SHA256SUM_LINUX64=3ba0a298155c32fbfd80cbc238298560bf69a2df511783054adfc151b76d80d8
># S390X用(IBMのメインフレームらしい)
>ARG INSTALLER_URL_S390X=https://repo.anaconda.com/archive/Anaconda3-2024.10-1-Linux-s390x.sh
>ARG SHA256SUM_S390X=e00bd5e6c275695e8050a45aa85790315f504c95243dfe3632f505284310f3c4
># AArch(ArmV8以降のArm CPUでのLinux用)
>ARG INSTALLER_URL_AARCH64=https://repo.anaconda.com/archive/Anaconda3-2024.10-1-Linux-aarch64.sh
>ARG SHA256SUM_AARCH64=489c608e8bddd2cf29dfbdd811cf99087cd6b6a0615d41c6f9058ce340594b65
># 変数の設定
>RUN INSTALLER_URL_LINUX64=https://repo.anaconda.com/archive/Anaconda3-2024.10-1-Linux-x86_64.sh \
>     SHA256SUM_LINUX64=3ba0a298155c32fbfd80cbc238298560bf69a2df511783054adfc151b76d80d8 \
>     INSTALLER_URL_S390X=https://repo.anaconda.com/archive/Anaconda3-2024.10-1-Linux-s390x.sh \
>     SHA256SUM_S390X=e00bd5e6c275695e8050a45aa85790315f504c95243dfe3632f505284310f3c4 \
>     INSTALLER_URL_AARCH64=https://repo.anaconda.com/archive/Anaconda3-2024.10-1-Linux-aarch64.sh \
>     SHA256SUM_AARCH64=489c608e8bddd2cf29dfbdd811cf99087cd6b6a0615d41c6f9058ce340594b65 \
>     /bin/sh -c "set -x && \
> # OSパッケージのインストール
>     apt-get update --fix-missing && \
>     apt-get install -y --no-install-recommends \
>         bzip2 \
>         ca-certificates \
>         git \
>         libglib2.0-0 \
>         libsm6 \
>         libxcomposite1 \
>         libxcursor1 \
>         libxdamage1 \
>         libxext6 \
>         libxfixes3 \
>         libxi6 \
>         libxinerama1 \
>         libxrandr2 \
>         libxrender1 \
>         mercurial \
>         openssh-client \
>         procps \
>         subversion \
>         wget && \
> # キャッシュ削除
>     apt-get clean && \
>     rm -rf /var/lib/apt/lists/* && \
> # アーキテクチャ名の取得($(uname -m))
>     UNAME_M=\"\$(uname -m)\" && \
> # アーキテクチャに応じたインストーラーリンクとハッシュ値
>     if [ \"\$UNAME_M\" = \"x86_64\" ]; then \
>         INSTALLER_URL=\$INSTALLER_URL_LINUX64; \
>         SHA256SUM=\$SHA256SUM_LINUX64; \
>     elif [ \"\$UNAME_M\" = \"s390x\" ]; then \
>         INSTALLER_URL=\$INSTALLER_URL_S390X; \
>         SHA256SUM=\$SHA256SUM_S390X; \
>     elif [ \"\$UNAME_M\" = \"aarch64\" ]; then \
>         INSTALLER_URL=\$INSTALLER_URL_AARCH64; \
>         SHA256SUM=\$SHA256SUM_AARCH64; \
>     fi && \
> # install用のシェルスクリプトをダウンロード
>     wget \"\$INSTALLER_URL\" -O anaconda.sh -q && \
> # ハッシュ値の一致確認
>     echo \"\$SHA256SUM anaconda.sh\" > shasum && \
>     sha256sum --check --status shasum && \
> # opt/condaにinstall
>     /bin/bash anaconda.sh -b -p /opt/conda && \
> # いらないファイルの削除
>     rm anaconda.sh shasum && \
>     ln -s /opt/conda/etc/profile.d/conda.sh /etc/profile.d/conda.sh && \
> # 環境変数への書き出し(pathを通す)
>     echo \". /opt/conda/etc/profile.d/conda.sh\" >> ~/.bashrc && \
>     echo \"conda activate base\" >> ~/.bashrc && \
>     find /opt/conda/ -follow -type f -name '*.a' -delete && \
>     find /opt/conda/ -follow -type f -name '*.js.map' -delete && \
> # キャッシュファイルの削除
>     /opt/conda/bin/conda clean -afy"
> # bashの起動
>CMD ["/bin/bash"]
>
>```

このdockerfileが実行されることで、OSに依存しない形でanaconda環境を整備できているようでした。勉強になるなー
(というかSX390とかいうメインフレーム用のinstallリンクが存在することが驚き)
</details>

## docker imageってなんだろな
バイナリ
基本的な流れとしては、dockerfileがあるディレクトリにターミナル上で移動したうえで、
```
docker image build .
```
でビルドできるらしい。これによって、dockerfileで指定した方法にのっとった環境(docker image)が完成したことになるらしい。このimageはバイナリそのものなので、dockerfileとは違う。(あっちはテキスト)
へー

## docker containerってなんだろな
実際のコンテナ
つまり実際に作った環境を起動させる感じらしい。
へー　なんか難しいね！

# おててを使って遊ぶ(格好よくいうとハンズオン)

## 前提環境
今回は、WSL2を仕込んだwindows PCを使っていきます。
OSはHomeです。
WSLの仕込み方は、前回の記事を適当に見てください。

https://qiita.com/hotate_2235/items/87654a1348e51774c224

まずは、Rancher Desktopをinstallしました。

https://rancherdesktop.io/

よく分かってませんが、商用利用可能なDocker Desktopの代替となりうるものらしい・・？？です。ふわふわ理解

必要要件は以下の通りらしいです。

> - One of
>   * Windows 10 with latest updates. The Home edition is supported.
>   * Windows 11 with latest updates. The Home edition is supported.
>   * Windows 2022 Server with latest updates.
> - Running on a machine with virtualization capabilities.
> - Persistent internet connection.
>
> Rancher Desktop requires Windows Subsystem for Linux on Windows. This must be installed prior to running the Rancher Desktop installer.

ということで、どうやらWSL2は必須らしいです。セットアップしててよかったね。
installの手順自体は、インストーラーをダウンロードしてポチポチすればOKです。最終的にinstallしたRancher Desktopを起動して、画面右下インジケーターから、kubernetes is runnigってなればOK

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3806752/796759cd-ea6e-4fb3-8b41-7bf512fd335f.png)

インジケーター自体は隠れてるかもしれないので、そこらへんは探索してください。
これによって、Powershell(or コマンドプロンプト)上でdockerコマンドを認識するようになりました。やったね。
ここから、docker自体をいろいろと遊んでいきます。

## 色々やってみて手作業をする会
やってみる

### 単純なdockerfileからコンテナを起動する。
まずは適当なフォルダに入って、そこでdockerfileを書く。
今回はこれを書いた。

```dockerfile:cowimage
FROM ubuntu:22.04
# ベース環境はubuntu

RUN apt-get update && \
    apt-get install -y cowsay && \
    ln -s /usr/games/cowsay /usr/local/bin/cowsay
# updateとcowsayのダウンロード、そしてパスの指定
# ln -s path1 path2にすることで、path1を、path2でも呼び出せるようにしている

CMD ["cowsay", "Moo!"]
# コンテナ起動時に実行されるコマンド
```

次に、このdockerファイルのあるディレクトリに移動してビルドした。
```bash
docker build -t cowimage .
# コンテナ名前をcowimageにしてビルド
```
最後に、実際に起動した。
```
docker run cowimage

 ______
< Moo! >
 ------
        \   ^__^
         \  (oo)\_______
            (__)\       )\/\
                ||----w |
                ||     ||
```
これで実行完了！
実際にdockerfileから環境を作ってimageをビルド、最終的に起動した。
ちなみに、内部環境に入って対話的に動かす場合は
```
docker run -it cowimage bash
#root@ユーザー？：
```
という感じでbashを起動できる。いいね

おさらい
1\. dockerfileを書く
2\. dockerfileのあるディレクトリに移動してbuildする
3\. 実際にcontainerをrunして起動する
という感じ

### miniconda環境を実際に作ろう！
実際にminiconda環境を作ってみました。ありがとうgpt4oくん

```dockerfile:miniconda_env
FROM ubuntu:latest
RUN echo "install ubuntu latest"

# 必要なパッケージインストール
RUN apt-get update && \
    apt-get install -y wget bzip2 ca-certificates curl git && \
    apt-get clean && \
    rm -rf /var/lib/apt/lists/*
RUN echo "complete required packages"

# Miniforge のインストール
RUN wget https://github.com/conda-forge/miniforge/releases/latest/download/Miniforge3-Linux-x86_64.sh && \
    bash Miniforge3-Linux-x86_64.sh -b -p /opt/conda && \
    rm Miniforge3-Linux-x86_64.sh && \
    /opt/conda/bin/conda clean -afy
RUN echo "complete Miniforge installation"

# PATH の設定
ENV PATH="/opt/conda/bin:$PATH"
# SHELL の設定
SHELL ["/bin/bash", "-c"]
# conda の初期化
RUN /opt/conda/bin/conda init bash && \
    echo "complete conda initialization"

# condaの仮想環境を作成
RUN conda create -n myenv python=3.12.9 && \
    conda clean -afy && \
    echo "complete conda environment creation"

RUN mkdir /work

# ホストPCのカレントディレクトリの内容を /work にコピー
COPY . /work

# コンテナの作業ディレクトリを /work に設定(cd　/work)
WORKDIR /work

# requirements.txtを使ってpipで必要なパッケージをインストール
RUN conda run -n myenv pip install -r requirements.txt && \
    echo "activate conda environment"
# RUN conda activateがなぜかうまくいかなかった
# どうやらbashを再起動しているっぽい？
# なので、この形式でinstallしている。
```

このdockerfileのあるディレクトリに行き、以下のコマンドでビルド、起動した。

```bash
# 実際のビルド
docker build -t miniconda .
# コンテナの起動
docker container run -it miniconda bash
# 起動後
>> (base) root@user:
```

成功！やったね

### バインドマウントを頑張る
先ほどのdockerfileでは、仮想環境下で作成したファイルが、コンテナ起動終了後に自動で消滅する形になっている。
これを修正し、ホスト側のマシン内部のファイルと連動づけるようにする。
そのために、バインドマウントを設定する。
コンテナ起動時にコマンドラインで指定することもできるみたいだけど、いちいち書くのはだるいので、ここは設定ファイルを作っておくことにする。

ファイル構成はこんな感じ
```plaintext
myproject/
├── work/
│   └── hello_yml.py
│   └── hello_dockerfile.py
├── requirements.txt
├── Dockerfile
└── docker-compose.yml
```
んでdockerfileがこんな感じ

```dockerfile
# Dockerfile
FROM python:3.12-slim

WORKDIR /src # コンテナ内部で/srcに移動

COPY requirements.txt .

RUN pip install -r requirements.txt

CMD ["python", "hello_dockerfile.py"]
```
肝心のymlファイルがこんな感じ
```yml:docker-compose
version: '3'
services:
  myapp: #コンテナ名？
    build: .
    volumes:
      - ./src:/src  # ← ホストの ./src をコンテナの /src にバインドマウント！
    working_dir: 
      /src # ← コンテナ内の作業ディレクトリを /src に設定！
    command: python hello_yml.py  # ← コンテナ起動時に実行するコマンドを指定！
```
んでビルド、実行はこんなコード
```bash
docker compose build
docker-compose run myapp bash
>>#root@user : 
```
となる。
ちなみに、コンテナ起動時に実行されるファイルについては、ymlファイルで指定したものが実行されるようで、dockerfileの方の指定したものは実行されなかった
GPT4o君曰く、優先度としては上から
```bash
yml command
docker run image CMD
dockerfile CMD
ENTRYPOINT
```
らしい。
ので、docker-composeで設定をするなら、dockerfile自体は簡素でいいかも。(どうせ優先度で勝てないし)

### 最後に、コンテナ二つ作ってつなげてみる
最後に、SQLサーバーコンテナと、それを解析するpython環境を合わせて作ってみることにする。

ファイル構成はこんな感じ
```plaintext
myproject/
├── src/
│   └── app.py
│   └── dockerfile
├── db
│   └── init.sql
├── .env
└── docker-compose.yml
```
んでdockerfileがこんな感じ

```dockerfile
# Dockerfile
FROM python:3.11-slim

WORKDIR /work

COPY . .

RUN pip install --no-cache-dir mysql-connector-python python-dotenv
```
肝心のymlファイルがこんな感じ
```yml:docker-compose
version: "3.8"

services:
  db:
    image: mysql:8.0
    container_name: mysql_server
    restart: always
    environment:# .envファイルから環境変数取得
      MYSQL_ROOT_PASSWORD: ${MYSQL_ROOT_PASSWORD}
      MYSQL_USER: ${MYSQL_USER}
      MYSQL_PASSWORD: ${MYSQL_PASSWORD}
      MYSQL_DATABASE: ${MYSQL_DATABASE}
    ports:
      - "3306:3306"
    healthcheck:
      test: ["CMD", "mysqladmin", "ping", "-h", "localhost", "-u", "root", "-p${MYSQL_ROOT_PASSWORD}"]
      interval: 2s
      timeout: 2s
      retries: 10
    volumes: # バインドマウント
      - ./db/init.sql:/docker-entrypoint-initdb.d/init.sql

  python:
    build: ./src
    container_name: python_client
    environment:
      MYSQL_ROOT_PASSWORD: ${MYSQL_ROOT_PASSWORD}
      MYSQL_USER: ${MYSQL_USER}
      MYSQL_PASSWORD: ${MYSQL_PASSWORD}
      MYSQL_DATABASE: ${MYSQL_DATABASE}
      MYSQL_HOST: ${MYSQL_HOST}
    depends_on:
      db:
        condition: service_healthy
    volumes: # バインドマウント
      - ./src:/work
    working_dir: /work
    command: ["bash", "-c", "tail -f /dev/null"]
    # コンテナを起動しっぱなしにするコマンド
    # command: ["python", "app.py"]
    # sqlサーバーへの接続確認用の初期動作
```
pythonのファイルがこんな感じ
```python:app.py
import mysql.connector
from dotenv import load_dotenv
import os

# .env 読み込み
load_dotenv('../.env')

# 環境変数から値を取得
mysql_database = os.getenv("MYSQL_DATABASE")
mysql_user = os.getenv("MYSQL_USER")
mysql_password = os.getenv("MYSQL_PASSWORD")
db_host = os.getenv("MYSQL_HOST")
print(f"DB_HOST: {db_host}")
print(f"MYSQL_DATABASE: {mysql_database}")
print(f"MYSQL_USER: {mysql_user}")
print(f"MYSQL_PASSWORD: {mysql_password}")

conn = mysql.connector.connect(
    host=db_host,# servicesの名前
    user=mysql_user,
    password=mysql_password,
    database=mysql_database
)

cursor = conn.cursor()
cursor.execute("SELECT * FROM users")
for row in cursor.fetchall():
    print(row)

print(f"接続先DB: {db_host}, ユーザー: {mysql_user}")

cursor.close()
conn.close()
```

んでビルド、実行はこんなコード
```bash
docker-compose up -d --build # バックグラウンドで起動し続ける
docker-compose run docker_test3-python bash 
# 起動し続けているコンテナへのbashでのアクセス
# コンテナ名は、フォルダ名前-service名
>>#root@user #/work: 
```
これでコンテナの起動と(.envファイルの階層の問題でうまくいかないけど)pythonでSQLサーバーへのアクセスができるようになった。
終了時はctrl+cでOK

:::note warn
ymlファイルで指定している場合、バインドマウントがうまくいかない場合があります
これは、dockerfileとのタイミングが悪かったりすると起きたりするそうです
dockerfileでのwordkirは、ディレクトリがなくても作ってくれるそうですが、ymlの方であらかじめ存在しないフォルダを指定すると、ディレクトリがなかった場合何もしない、ということになるそうです。
(今回の例だと、sqlサーバーのバインドマウントがうまくいっていません。フォルダが出来てないので)
バインドマウントがうまくいっていない場合はそのあたりに注意しましょう。
:::


### お片付け
止まったコンテナ、使ってないimageを全部消す

https://qiita.com/boiyama/items/9972601ffc240553e1f3

```
# 使ってないコンテナの一括削除
docker container prune

#使ってないimageの一括削除
docker image prune
```

# まとめ
色々手作業で動かした結果、dockerfile自体を読めるようにはなりましたが、docker composeあたりがあまり判然としないままでした。
今後よく使うと思うので、そこでまた学んでいきたいです