SI→Web業界への転職に利用したポートフォリオのDocker環境を振り返る

# 概要
これはWano Group Advent Calendar2022 13日目の記事です。
https://qiita.com/advent-calendar/2022/wano-group

Wanoで[Video Kicks](https://www.tunecore.co.jp/video-kicks?via=115&gclid=Cj0KCQiAnNacBhDvARIsABnDa69Iao9-3UA-EV_kKfrduesrPQv0iY11LOWSoHOxNWMAhM-GxBE64qcaAskqEALw_wcB)という
ミュージックビデオをiTunesやApple Music、レコチョクMUSICストア、dミュージック、GYAO!、LINE MUSICなどのビデオ配信ストアで配信/販売できるサービスを開発しています。

私は2022年の4-5月に転職活動を行い、SI業界からWeb業界に転職しました。
その中でdockerを利用していましたが、今思うと見よう見まねでなんとなく使ってしまっていました。
今後業務として本格的にDockerを使っていくために、転職時に利用したポートフォリオを振り返り、Dockerの基本知識とポートフォリオの直すべき点を復習します。

# 開発したポートフォリオ

アマチュアクリエイター向けの作業環境共有webアプリを開発しました。
~~https://tsukurie-for-creators.com/~~ ※AWSが高いので公開停止

### 主な機能
ログイン、メール認証、お問い合わせ、プロフィール投稿、アカウント削除、画像トリミング、画像アップロード

<img width="600" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2475547/d31dca9f-573c-477b-f31a-1a1d699fe073.png">

<img width="600" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2475547/93afc395-f7da-5a89-f26d-14bda0de641c.png">

<img width="600" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2475547/3e76f552-6fba-2f26-c547-ba79f3d728c8.png">


# ポートフォリオに使用したDocker関連ファイル

ディレクトリ構成は以下のようになっています。
主にLaravel、apache、mysqlを使用しています。

```
project-root
├──src/    <--この下にLaravelのソースを配置
├──docker/
│　　　　　　├──app/   
│   │   ├──Dockerfile         <--php・Apache用Dockerfile
│   │   ├──000-default.conf   <--Apache用設定ファイル（今回は割愛）
│   │   └──php.ini            <--php用設定ファイル（今回は割愛）
│   └──db/ 
│       ├──data/              <--mysql用データディレクトリ（今回は割愛）
│       └──my.cnf             <--mysql用設定ファイル（今回は割愛）
│
docker-compose.yml
```

#### Dockerfile
```Dockerfile
# どんなdockerイメージを利用して構築をするか
FROM php:8.1-apache

# 設定ファイルをdockerコンテナ内のPHP、Apacheに読み込ませる
ADD ./docker/app/php.ini /usr/local/etc/php/
ADD ./docker/app/000-default.conf /etc/apache2/sites-enabled/

# Composerのインストール
RUN cd /usr/bin && curl -s http://getcomposer.org/installer | php && ln -s /usr/bin/composer.phar /usr/bin/composer

# ミドルウェアインストール
RUN apt-get update \
&& apt-get install -y \
git \
zip \
unzip \
vim \
libpng-dev \
libpq-dev \
&& docker-php-ext-install pdo_mysql

# デプロイ用に追加
COPY ./src/laravel /var/www/html
RUN chmod -R 777 /var/www/html/storage

# Laravelで必要になるmodRewriteを有効化する
RUN mv /etc/apache2/mods-available/rewrite.load /etc/apache2/mods-enabled
RUN /bin/sh -c a2enmod rewrite

```

#### docker-compose.yml

```docker-compose.yml
# Compose fileのバージョン指定
version: '3'
# どんなコンテナを立ち上げるか
services:
 # laravelを動かすコンテナ
 app:
   # どのポートを開いて繋ぐか。下記はコンテナの80番ポートを開いて、ホストの8000番に繋ぐ
   ports:
     - "8000:80"
   # 先ほど作ったDockerfileを使って、コンテナをビルドするという指定
   build: 
    context: .
    dockerfile: ./docker/app/Dockerfile
   # コンテナの名前を指定
   container_name: tsukurie_app
   # コンテナとホスト側のディレクトリを同期する場所の指定。laravelのソースが入る予定の場所
   volumes:
     - ./src/laravel:/var/www/html
 # MySQLを動かすコンテナ
 db:
   # Docker HubからMySQL5.7の公式イメージをダウンロードしてくる指定
   image: mysql:5.7
   container_name: tsukurie_db
   # コンテナ内の環境変数を指定。環境変数を渡すとビルド時に設定してくれるDockerイメージがあるので、利用の際はDocker Hubのサイトで確認すると良い
   environment:
     MYSQL_ROOT_PASSWORD: root
     MYSQL_DATABASE: tsukurie_db
     MYSQL_USER: tsukurie_user
     MYSQL_PASSWORD: tsukurie_pass
     TZ: 'Asia/Tokyo'
   # 起動時のコマンド
   command: mysqld --character-set-server=utf8mb4 --collation-server=utf8mb4_unicode_ci
   # ディレクトリ同期。設定ファイルとMySQLのデータが保存される場所を同期している。コンテナは基本的に起動時に変更されてもコンテナ自体が止まるとデータが消えてしまうため、保存しておきたいものはホストマシンと同期しておく必要がある。
   volumes:
     - ./docker/db/data:/var/lib/mysql
     -  type: bind
        source: "./docker/db/my.cnf"
        target: "/etc/my.cnf"
     
   ports:
     - 3306:3306
 # 開発環境用メールサーバー
 mail:     
   image: mailhog/mailhog    
   ports:
     - 8025:8025

```
## 各設定項目について

### Dockerfile
Dockerfileには、イメージを作り上げるために実行するコマンドライン命令を記載します。
`docker build`コマンド実行時に順次コマンドライン命令を自動化した処理を行い、ビルド結果となるイメージが得られます。
> [Dcoekrfileリファレンス](https://docs.docker.jp/engine/reference/builder.html#dockerfile)

また、命令ごとに１つのレイヤを作成します。
> Dockerイメージを構成するのは、Dockerfileの各命令に相当する、読み込み専用のレイヤ群です。それぞれのレイヤは直前のレイヤから変更した差分であり、これらのレイヤが積み重なっています。
> [Dockerfileのベスト・プラクティス](https://docs.docker.jp/develop/develop-images/dockerfile_best-practices.html)



#### FROM

Dockerイメージからレイヤを作成します。
今回はphp:8.1およびapacheの公式イメージを使用しています。

```Dockerfile
FROM php:8.1-apache
```

#### ADD
追加したいファイル、ディレクトリ、リモートファイルのURLを<追加元>で指定すると、これらをイメージのファイルシステム上のパス<追加先>に追加します。

今回は、ホスト側のphp.iniと000-default.confを<追加先>のパスに追加しています。
```Dockerfile
# ADD <追加元> <追加先>
ADD ./docker/app/php.ini /usr/local/etc/php/
ADD ./docker/app/000-default.conf /etc/apache2/sites-enabled/
```

#### COPY

COPY命令では、追加したいファイル、ディレクトリを<コピー元>で指定すると、これらをイメージのファイルシステム上のパス<コピー先>に追加します。

今回は、ホスト側の./src/laravel配下を/var/www/html配下に追加しています。
```Dockerfile
# COPY <コピー元> <コピー先>
COPY ./src/laravel /var/www/html
```


※ADDとCOPYの違いは後述



#### RUN

RUN命令は、現在のイメージよりも上にある新しいレイヤでコマンドを実行し、その結果をコミット（確定）します。
結果が確定されたイメージは、Dockerfileの次のステップで使われます。
また、`\`バックスラッシュ区切りで記述することで、複数行に渡ってコマンドを記述することができます。

```Dockerfile
# 一部抜粋
RUN apt-get update \
&& apt-get install -y \
git \
zip \
unzip \
vim \
libpng-dev \
libpq-dev \
&& docker-php-ext-install pdo_mysql

RUN chmod -R 777 /var/www/html/storage

RUN mv /etc/apache2/mods-available/rewrite.load /etc/apache2/mods-enabled
RUN /bin/sh -c a2enmod rewrite
```


### docker-compose.yml
#### version
Composeファイルのバージョンを指定します。
マイナーバージョンを指定しなければデフォルトで0が使われるので、意図的に0を使用したい場合以外は注意が必要です。

> Compose ファイルのバージョンを指定する場合は、メジャー番号とマイナー番号の両方を指定する必要があります。マイナーバージョンの指定がなければ、最新のマイナーバージョンではなく、デフォルトの 0 が使われます。その結果、新しいバージョンで追加された機能はサポートされません。たとえば
>version: "3"
は、以下の指定と同等です。
version: "3.0"
>
>[Docker-docs-ja](https://docs.docker.jp/compose/compose-file/compose-versioning.html#compose-file-version-3)


#### context

Dockerfile を含むディレクトリのパス、あるいは、git リポジトリへの URL を指定します。

相対パスとして値を指定すると、 Compose ファイルがある場所を基準とした相対パスとして解釈されます。また、そのディレクトリが構築コンテキストとなり、その内容が Docker デーモンに対して送られます。

>docker buildコマンドを実行したときの、カレントなワーキングディレクトリのことを ビルドコンテキスト（build context）と呼びます。 デフォルトで Dockerfile は、カレントなワーキングディレクトリにあるものとみなされます。
>[ビルドコンテキストの理解](https://matsuand.github.io/docs.docker.jp.onthefly/develop/develop-images/dockerfile_best-practices/#%E3%83%93%E3%83%AB%E3%83%89%E3%82%B3%E3%83%B3%E3%83%86%E3%82%AD%E3%82%B9%E3%83%88%E3%81%AE%E7%90%86%E8%A7%A3)

今回は`context: .`と定義しているので、docker-compose.ymlが置かれているproject-rootディレクトリがcontextとなります。
Dockerfileを別ディレクトリに置きたい場合は、後述のdockerfileにcontextからの相対パスを定義します。

#### dockerfile
Composeが利用するDockerfileの、contextからの相対パスを指定します。
今回は、`dockerfile: ./docker/app/Dockerfile`としています。

#### volumes
コンテナとホスト側のディレクトリを同期する場所を指定します。
今回は、ホストの./src/laravel配下のソースと、コンテナの/var/www/html配下、
./docker/db/data配下と/var/lib/mysql配下をバインドマウントという方法で同期しています。
コンテナ内のファイルは、起動時に変更されてもコンテナ自体が止まると消えてしまいます。そのため、保存しておきたいものはホストマシンと同期しておく必要があります。

バインドマウントの他に、ボリュームやtmpfsというマウント手法があります。
- ボリュームは、ホストPC上にコンテナ用のデータファイルを作成し、コンテナ内の特定のディレクトリパスにマッピングします。
- バインドマウントは、ホストPCの特定のディレクトリ（絶対パス指定）を、コンテナ内の特定のディレクトリパスにマッピングします。

> ボリュームとは、Dockerコンテナーにおいて生成され利用されるデータを、永続的に保持する目的で利用される仕組みです。 バインドマウントはホストマシンOSのディレクトリ構造に依存しますが、ボリュームは完全にDockerによって管理されます。
>[ボリュームの使用](https://matsuand.github.io/docs.docker.jp.onthefly/storage/volumes/)

- type ：マウントの型でvolume、bind、tmpfsを指定できる。

```docker-compose.yml
   # 一部抜粋
   volumes:
     - ./docker/db/data:/var/lib/mysql
     -  type: bind
        source: "./docker/db/my.cnf"
        target: "/etc/my.cnf"
```

#### image
DBに関してはDockerfileを作成せず、mysqlのdockerイメージをdockerhubから直接ダウンロードするよう指定しています。

#### environment
コンテナ内の環境変数を指定します。
今回はmysqlのユーザー名、DB名、パスワードを指定しています。

#### command
コンテナ起動時に実行するコマンドを指定します。
今回は以下のようにサーバーの起動時に文字設定を指定しています。
[MySQL 8.0 リファレンスマニュアル(アプリケーションの文字セットおよび照合順序の構成)](https://dev.mysql.com/doc/refman/8.0/ja/charset-applications.html)

```
command: mysqld --character-set-server=utf8mb4 --collation-server=utf8mb4_unicode_ci
```
なお、上記のコマンドの代わりにmy.cnfでも設定できます。
```my.cnf
[mysqld]
character-set-server=utf8mb4
collation-server=utf8mb4_unicode_ci
```


# 直すべきところ

## Dockerfile
#### ADDではなくCOPYを使用すべきだった
Dockerfileのベストプラクティスとして、機能がより明確であるCOPYが推奨されています。
COPYはローカルファイルをコンテナの中にコピーするという、基本的な機能しかサポートしていません。

一方で、ADDは複数の機能（ローカル上でのtarアーカイブ展開や、リモートURLのサポート）を持ち、一見では処理内容が分かりません。
したがって、ADDのベストな使い方は、ローカルのtarファイルをイメージに自動展開することとされています。


[Dockerfileを書くベスト・プラクティス（ADD と COPY）](https://docs.docker.jp/engine/userguide/eng-image/dockerfile_best-practice.html#add-copy)


## docker-compose.yml
#### version指定をマイナーバージョンまで明示的に指定すべきだった
マイナーバージョン未指定の場合に、そのメジャーバージョンの最新版を利用すると勘違いする可能性がある。
例えば、3.0と3.8では利用できるパラメータが追加・削除されているため、composeファイルにパラメータ追加変更時にエラーが発生する。

#### パスワード等は環境変数として読み込むべきだった

以下のように.envに環境変数を定義し、読み込むことができます。
ハードコードだめ、ぜったい。

```
$ cat .env
ROOTPASS=hoge
DATABASE=fuga
USERNAME=piyo
USERPASS=hogera

$ cat docker-compose.yml
  // 一部抜粋
  db:
    image: mysql:5.7
    environment:
      MYSQL_ROOT_PASSWORD: ${ROOTPASS}
      MYSQL_DATABASE: ${DATABASE}
      MYSQL_USER: ${USERNAME}
      MYSQL_PASSWORD: ${USERPASS}
```

ちなみに、環境変数を読み込んだdocker-compose.ymlがどのようになるかは
`docker-compose config`で確認することができます。

注意事項として、.envからの読み込みは`docker-compose up`コマンド実行時のみであり、
`docker stack deploy`では機能しないようです。
開発環境では問題ないですが、本番環境では別の方法が必要となる可能性があります。

[Compose 内の環境変数](https://docs.docker.jp/compose/environment-variables.html)


#### バインドマウントではなく、ボリュームを使用すべきだった
ボリュームを使用することのメリットの例として以下が挙げられます。
- バックアップや移行が容易
ホスト側の多数のファイルとマッピングされてしまうため別環境に移しにくい場合があります。

- 意図しないファイルがホスト上に作られることを防ぐ
バインドマウントの場合はOSのディレクトリ構造に依存するため、コンテナ内で作成した危険なファイルが、そのままホスト上にも作られてしまう危険があります。（下図参照）

- コンテナサイズを増加させない
ボリューム内のデータはコンテナのライフサイクルから離れたところに存在しているためです。
アプリケーションが巨大になり永続化したいデータが増えるほど、ボリューム使用による恩恵を受けられると考えられます。

![dockervlolume.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2475547/32f5ec3c-a1b0-8398-c439-8ceb1de895aa.png)



他にも、ボリュームは複数コンテナー間で安全に共有できたり、MacやWindowsホストからのバインドマウントに比べて、より高い性能を実現するなどの理由から推奨されています。

#### あえてバインドマウントを使用する場合、long syntaxを使用すべきだった

- short syntax

ホストのディレクトリとコンテナ内のマウントパスをコロン区切りで１行で記述
```docker-compose.yml
   volumes:
     - ./docker/db/data:/var/lib/mysql
```

- long syntax

マウントの設定をマッピングの配列として記述
```docker-compose.yml
   volumes:
     -  type: bind
        source: "./docker/db/my.cnf"
        target: "/etc/my.cnf"
```

> long syntaxでの設定では, アプリケーション起動時にホストに対象 (ディレクトリ, ファイル) が存在しない場合はエラーを吐いて終了しますが, short syntax での設定では, 自動でホストにディレクトリを作成します。
>[docker-compose の bind mount を1行で書くな](https://zenn.dev/sarisia/articles/0c1db052d09921)

LinuxOSの場合、ここで生成されたディレクトリのオーナーはrootとなるため、別ユーザーで操作する場合にマウントされたディレクトリへのアクセス権がありません。
アプリケーションはエラーを吐き、解消に時間がかかる可能性があります。

# まとめ

Dockerわからない！という方はサンプルコードをそのまま使ってしまうかもしれません。
試しに動かしてみるだけならそれでもOKですが、動かした後に、自分が今何をやっているのか、この記述はどういう意図があるのか、
当たり前ではありますがドキュメントをしっかり読み込むことが重要です。知りたいことはほぼ書いてあります。

これからDockerを使っていきたいという方の参考になれば幸いです！