Go + MySQL + React のDocker開発環境を作成する

# はじめに
GoとMySQLとReact（&TypeScript）を使ったポートフォリオ作成をしようと思い、最初に勉強も兼ねてDocker開発環境を作成しました。
そこでQiitaやZennなどで書かれている記事を参考にしていましたが、最終的に他の方とは違う構成になったので自分も記事を執筆しようと思いました。

# 作成したもの
ディレクトリ構成
```
.
├── README.md
├── db
│   ├── data
│   └── my.cnf
├── docker-compose.yml
├── go-app
│   ├── Dockerfile
│   └── main.go
├── react-ts-app
│   ├── .dockerignore
│   ├── Dockerfile
│   ├── README.md
│   ├── node_modules
│   ├── package-lock.json
│   ├── package.json
│   ├── public
│   ├── src
│   └── tsconfig.json
└── variables.env

```

Dockerfile（go-app）
```dockerfile
FROM golang:latest
WORKDIR /app/go
COPY ./ ./
ENTRYPOINT [ "go", "run", "main.go" ]
```

Dockerfile（react-ts-app）
```dockerfile
FROM node:17.7.2
WORKDIR /app/react-ts
COPY package*.json ./
RUN yarn install
COPY . .
ENTRYPOINT [ "npm", "start" ]
```
dockerignoreファイル（react-ts-app）
```
node_modules
npm-debug.log
yarn-error.log
```

docker-compose.yml
```yml
version: '3'

services:
  react-ts:
    build: ./react-ts-app
    ports:
      - '3000:3000'
    tty: true
    stdin_open: true
    
  go:
    build: ./go-app
    ports:
      - '8000:8000'
    tty: true
    stdin_open: true

  db:
    image: mysql:8.0
    volumes:
      - './db/data:/var/lib/mysql'
      - './db/my.cnf:/etc/mysql/conf.d/my.cnf'
    # MYSQL_ROOT_PASSWORDのみをenvファイルで設定
    env_file:
      - variables.env
    ports:
      - '3306:3306'
```

my.cnf
```
[mysqld]
character-set-server=utf8mb4
collation-server=utf8mb4_unicode_ci

[client]
default-character-set=utf8mb4
```

# 解説
Dockerfileは、docker-composeを使わなくても単体で動作することが可能だよねっていう状態を意識しました。
また、docker-composeはあくまでコンテナを立ち上げるのを楽にするためのものだと意識しました。

### Dockerfile
ENTRYPOINTを指定していることで、docker-composeをupした瞬間にdb・go・react-tsの3つのコンテナが起動＆サーバーも起動するようになっています。

COPY句によってDockerfile自体もイメージ内にコピーされてしまいますが、イメージサイズへの影響が小さい・dockerignoreファイルを作成してもビルドにかかる時間は変わらないと判断し、許容しています。
dockerignoreファイルによるビルド時間への影響は下記のサイトが参考になります。

https://qiita.com/munisystem/items/b0f08b28e8cc26132212 

### dockerignore（React用）
node_modules、npm-debug.log、yarn-error.log の3つを指定しています。
これによってローカルモジュール・デバッグログ・エラーログがDockerイメージにコピーされないようにします。

node_modulesはパッケージがインストールされるディレクトリです。必要なパッケージはソースコードによって異なるため、node_modulesをイメージ内にコピーする必要はないと言えます。

また、node_modules内に多くのパッケージをインストールしている場合、イメージをビルドするときに多くの時間を要します。そのため、node_modules は dockerignoreファイルに登録することが基本となっています。

node_modules のコピーはしませんがコンテナ内でもパッケージは必要になります。ReactのDockerfileでは package.json と package-lock.json をコピーし、`yarn install` を行うことで、コンテナ使用者が必要なパッケージのみをインストールするようにしています。

### Go
FROM でイメージを取得するときに、golangにするか、ubuntuのようなOSイメージにするか、golang:&lt;version>-alpineにするかで迷いました。
この中で一番設定ミスが起こりづらそうだと感じたgolangにしました。

dockerの知識量的にOSイメージからGoの環境を整えるのはハードルが高そう。また、alpineはGo projectに公式サポートされていないとDockerhubに書いてあるといった理由で、この2つは選択肢から外しました。

https://hub.docker.com/_/golang

ENTRYPOINT によって、コンテナが立ち上がるとともに `go run main.go` でサーバーも立ち上がるようにしています。
以下、main.goのコードです。

```main.go
package main

import (
	"fmt"
	"net/http"
)

func main() {
	http.HandleFunc("/", echoHello)
	http.ListenAndServe(":8000", nil)
}

func echoHello(w http.ResponseWriter, r *http.Request) {
	fmt.Fprintf(w, "<h1>Hello World</h1>")
}
```

### React
nodeイメージのDockerfileを見ると、デフォルトでyarnが入っていることがわかります。
なので、yarnをわざわざインストールする必要はありません。

https://github.com/nodejs/docker-node/blob/6e8f32de3f620833e563e9f2b427d50055783801/17/bullseye/Dockerfile 

また、Dockerfileやcomposeに`creat-react-app`の記述もされている方がいますが、コンテナを起動するたびに新規のプロジェクト作成するのは時間がかかりそうだと感じたので僕は書きませんでした。
あくまでソースコードはホスト上にあり、コンテナへそれらをコピーすることで正常に動くようにしたいと思いました。

次に最も重要な部分ですが、ReactのDockerfileではビルド時間の短縮のためにCOPY句を2つ使っています。
```Dockerfile
COPY package*.json ./
RUN yarn install
COPY . .
```
dockerにはレイヤー構造とキャッシュという概念があり、これを活用することでイメージのビルド時間を短縮できます。
レイヤーとキャッシュについては解説すると長くなるので、ここでは割愛させていただきます。

Reactの開発では頻繁にソースコードを変えると思います。
もし `yarn install` の前に `COPY . .` を行うと、ソースコードが変更されるたびにパッケージをインストールし直すことになります。
パッケージを追加・変更していなくても毎回この処理が行われ、多くの時間が費やされてしまいます。

`COPY package*.json ./` と `yarn install` を先に行なっていればレイヤーキャッシュが使用され、ビルドの時間を大幅に節約できます。
以上の理由から、COPYを2回使うようにしました。

この記述方法は、こちらのサイトを参考にしました。

https://nodejs.org/ja/docs/guides/nodejs-docker-webapp/ 

### MySQL
MYSQL_ROOT_PASSWORDを設定しなければエラーが出ます。
共有をしないのであればcompose上に直接設定してもいいですが、僕はgithubを通じて共有しようと考えていたのでenvファイルを使いました。

MySQLはローカルホスト上では`mysql.server start`を使って起動する必要があるかと思いますが、このコンテナ上ではそれが必要ありません。コンテナが立ち上がるとともにDBサーバーも立ち上がります。

my.cnfファイルを用意していありますが、ここでは文字化けを防ぐための設定しかしていません。
もしかしたら他にももっと設定した方がいいものもあるかもしれませんが、沼にハマりそうなのでやめました。

# 最後に
人によって書き方が違って正しい情報を入手しづらく、結構時間がかかってしまいました。
もちろん僕が書いたものも正しいとは言い切れないので、これから更に知識をアップデートしていきたいなと思います。