[docker,laravel8,nginx(非Laradoc)] docker環境構築で躓いたところと解決した方法をまとめてみた

## なぜこの記事を書いた？
会社の業務でdocker環境を構築したときに、エラー文がどこにあるかも分からず全く糸口が見えない状況に何度か陥り苦しんだため、同じ状況で苦しんでいる後続の方のお役に立てればと思い書きました。

## 当初の環境とdockerで構築した環境について（前提条件の共有）

当初は１つ目に記載したローカル環境で開発していましたが、ecs運用を見据えて２つ目に記載したdocker環境に構築し直しました。

```:当初のローカル環境
laravel8 (フロントは専用のフレームワークを使用せずlaravelに備え付けられている機能を使用。vue（inertia）とbladeを両方使用）
php8.0
node14.17.5
mysql5.7
```

```:dockerで構築した環境
laravel8 (フロントは専用のフレームワークを使用せずlaravelに備え付けられている機能を使用。vue（inertia）とbladeを両方使用）
php8.1
node14.17.5（諸事情で低くしていますが、後ほどバージョンアップの予定）
mysql8.0
redis(predisを利用)
minio（s3）
nginx
```


## 困った事例１
#### nginxのデフォルトの画面しか出てこず、エラー画面すら出ない
"nginx laravelの画面でない"で検索してみるとfastcgi_passの指定が間違っているという記事がでてきたため、その箇所を色々修正してみるもうまく行きませんでした。
#### 解決方法
結論は、接続するurlを正しいもので確認することでした。
laravelの画面表示は.envのAPP_URLに依存しております。（docker-psでアドレスを確認することでlaravelが映し出されている本当のurlを確認できます。）
筆者はそれに気づかずにdocker-compose.ymlで指定したnginxのコンテナのport番号を頼りにlocalhost:3334に接続し続けましたが、laravelは 0.0.0.0:3334を指定していたので、localhost（ホスト指定なし）のnginxのデフォルト画面が出されていたというオチでした。

## 困った事例2
#### DBの疎通がうまく行かずlaravelのコンテナが`php artisan migrate`のタイミングでthrow exceptionでstopしてしまう（エラー文は下記）

```
gostep-fpm      |    Illuminate\Database\QueryException 
gostep-fpm      | 
gostep-fpm      |   SQLSTATE[HY000] [2002] Connection refused (SQL: select * from information_schema.tables where table_schema = gostep_local and table_name = migrations and table_type = 'BASE TABLE')
gostep-fpm      | 
gostep-fpm      |   at vendor/laravel/framework/src/Illuminate/Database/Connection.php:712
gostep-fpm      |     708▕         // If an exception occurs when attempting to run a query, we'll format the error
gostep-fpm      |     709▕         // message to include the bindings with SQL, which will make this exception a
gostep-fpm      |     710▕         // lot more helpful to the developer instead of just the database's errors.
gostep-fpm      |     711▕         catch (Exception $e) {
gostep-fpm      |   ➜ 712▕             throw new QueryException(
gostep-fpm      |     713▕                 $query, $this->prepareBindings($bindings), $e
gostep-fpm      |     714▕             );
gostep-fpm      |     715▕         }
gostep-fpm      |     716▕     }
gostep-fpm      | 
gostep-fpm      |       +36 vendor frames 
gostep-fpm      |   37  artisan:37
gostep-fpm      |       Illuminate\Foundation\Console\Kernel::handle(Object(Symfony\Component\Console\Input\ArgvInput), Object(Symfony\Component\Console\Output\ConsoleOutput))
gostep-fpm exited with code 1

```
#### 解決方法
結論は.env.localを作成することで解決しました。
詳細ですが、/bootstrap/app.phpに
```
\Dotenv\Dotenv::createMutable(__DIR__ . '/../', '.env.' . env('APP_ENV', 'local'))->load(); 
```
（.envではなく.env.xxxを読み込むという処理）を新たに指定した事を忘れており、その当時は.env.localを作っていなかったため、DB_USERNAMEなどのDB接続に使用する環境変数がlaravelで準備されずエラーを起こしてしまっていました。


## 困った事例3
#### laravelの初期画面が出ず、500エラー画面が出現しているが、エラー文がざっくり過ぎてその先に進めない（下記のエラー）
```
2022/11/21 03:06:59 [error] 30#30: *1 connect() failed (113: Host is unreachable) while connecting to upstream, client: 172.21.0.1, server: , request: "GET / HTTP/1.1", upstream: "fastcgi://172.21.0.7:9000", host: "0.0.0.0:3334"
```
#### 解決方法
まずこのエラーの原因は.env.localでAPP_KEYがないことだったので、それを記載することで解決しました。
今回肝の躓いた理由ですが、APP_KEYについて言及しているエラー文自体を探せなかったことでした。
探せなかった理由ですが、docker構築の際にコンテナ監視のlogを/logs/~に吐き出すように新しく構築しており、そちらに全てlogが吐き出されると思い込んでしまったことからでした。
今回は偶然ほかにもlogがあるのではと思い立ち、実際にlaravelのログを吐く場所（/storage/logs/~）にクリティカルなエラー文が生成されているのを発見し解決できましたが、（未来の自分も含めて）皆さんがすぐ思いつけるとも限らないので、この項目を取り上げてみました。

※参考までに解決に導いてくれた/storage/logs/laravel.logのエラー文を下記に記載しておきます。
```
[2022-11-21 07:50:40] local.ERROR: No application encryption key has been specified. {"exception":"[object] (Illuminate\\Encryption\\MissingAppKeyException(code: 0): No application encryption key has been specified. at /var/www/vendor/laravel/framework/src/Illuminate/Encryption/EncryptionServiceProvider.php:101)
[stacktrace]
#0 /var/www/vendor/laravel/framework/src/Illuminate/Support/helpers.php(263): Illuminate\\Encryption\\EncryptionServiceProvider->Illuminate\\Encryption\\{closure}('')
#1 /var/www/vendor/laravel/framework/src/Illuminate/Encryption/EncryptionServiceProvider.php(103): tap('', Object(Closure))
#2 /var/www/vendor/laravel/framework/src/Illuminate/Encryption/EncryptionServiceProvider.php(82): Illuminate\\Encryption\\EncryptionServiceProvider->key(Array)
#3 /var/www/vendor/laravel/framework/src/Illuminate/Encryption/EncryptionServiceProvider.php(34): Illuminate\\Encryption\\EncryptionServiceProvider->parseKey(Array)
#4 /var/www/vendor/laravel/framework/src/Illuminate/Container/Container.php(873): Illuminate\\Encryption\\EncryptionServiceProvider->Illuminate\\Encryption\\{closure}(Object(Illuminate\\Foundation\\Application), Array)
#5 /var/www/vendor/laravel/fram...
```

## 困った事例4
#### nodeとnpmを使えるようにして`npm install && npm run prod`しているのにcssとjsが当たらない
laravelのコンテナでマルチコンテナビルドを使ったり、立ち上げ時のshellに組み込んでみたり、立ち上がったコンテナに入って`npm run prod`したりしましたが全く上手く行きませんでした。
#### 解決方法
結論は、**nginxのコンテナ**にlaravelのソースコードで`npm install && npm run prod`を行ってできたpublicディレクトリを、rootで指定しているディレクトリ(筆者の場合は/var/www/public)に配置することで解決できました。
~~うまく行かなかった原因は、laravelの外にフロントを分離していないので、laravelのコンテナ内でフロントも準備させる必要があると思いこんでいたところでした。~~
↑
うまくいかなかった原因は、**nginxが読みに行ける場所にフロントのファイル（ディレクトリ）を準備していなかったから**でした。
仮にマイクロサービスと同じ形でフロントのコンテナを用意したとしても、nginxがフロントコンテナの中身を読みにいくわけではないため、別途フロントのpublicディレクトリ（またはhtmlディレクトリ）のコピーをnginxが読める場所に準備しておく必要がありますm(__)m


#### 参考
配置のやり方についてですが、個人的には`git push`時のpublicディレクトリに依存する直のソースコードのCOPYコマンドより、毎度`npm run prod`するマルチコンテナビルドからのCOPYコマンドがオススメです。（ただのコピーだと`npm run prod`されていないものが本番に上る可能性があるからです。）
参考までに筆者が作成した該当箇所のソースコードを添付しますので、うまく利用していただければと思います。（本題と関係ないキャシュの問題などは載せないので、必要に応じてご自身で調整してください）
```nginx.Dockerfile
# マルチコンテナ（nginxのｎimageを作成するためだけに使うコンテナ）の作成部分
FROM node:14.17.5 as node-build
COPY . /application  # applicatioは適当な名前です。仮のコンテナなので、後でコピーする時わからなくならなければディレクトリ名は何でも大丈夫です。
WORKDIR /application
RUN apt-get update \
    && npm install \
    && npm run prod

# 本番で使用するnginxコンテナの準備（この記事では本番システムのセキュリティを考慮して、本筋と関係ない部分を省略して記載をしておりこれだけでは動かない可能性が高いので、マルチコンテナビルド以外は他の記事を参考にしてください。）
FROM nginx:1.19.0-alpine
COPY docker/nginx/nginx.conf /etc/nginx/nginx.conf
COPY --from=node-build /application/public /var/www/public/ # この部分で仮コンテナに準備したcssとjsだけをnginxコンテナにコピーしています。
```

## つまづいた箇所からdocker構築のために特に知っておくといいと思ったこと
- フロントを別フレームワークに切り出さない場合、nginxの役割は画面表示で使う変数を取得して、かつフロントを表示させるところまでとなります。したがって変数以外のviewに関する**cssやjsのデータはnginxのコンテナの中にもたせて作ります**。
この部分を知っておくと"laravelコンテナでフロントを構築してからnginxに渡そうとして躓く"ということを回避できるようになります。
※アクセスのたびに毎度顧客から一番遠い位置にある（通信経路が一番多い）laravelから情報量の多いフロントを受け取らないことで、全体の通信の量を最小限に抑えてレスポンス速度を上げていることを知っておくと良いです。
また技術的な観点では、一般的にfastcgi_passでlaravelの/public/index.phpを呼び出すようになるのですが、そのときに返ってきているのが、/storage/framework/viewsの中にある`変数を埋め込んだhtmlのみのファイルになる（css,jsは受け取る仕組みになっていない）`のを知っておくと良いです。
あと筆者がソースコードで追いきれていない部分の話になるのですが、おそらくlaravelで処理を待っている間にnginxからクライアント側に先に情報量の多いjsとcssを渡し始めてしまうことで、よりクライアント側に無駄な待ち時間を発生させず画面を提供できる様な仕組みになっていると思われ、ゆえにlaravelコンテナからフロントを受け取ってはいけない仕組みになっていると考えられます。


- ~~フロントを別フレームワークに切り出す場合(Nuxtなどを利用する場合)、nginxは画面表示で使う変数をlaravelコンテナからhtmlではなくjsonとして受け取って、そのjsonをnginxの出力用のポート番号（今回は自分の構築では3334）経由でフロントのコンテナに渡して、表示に関する処理はフロントに任せる構築となります。~~
↑こちらはnginxコンテナ内にコピーしたのフロントのファイルか、volumesなどに保存したフロントのファイルを読みに行っていそうです。フロントコンテナを立ち上げるのはnginx内のpublicディレクトリ配下のファイル（またはvolumes）を同期によって最新化するために使われていそうです。間違った記述をしてしまい申し訳ありませんm(__)m
※また、awsのvolumesの取り扱いは[こちら](https://docs.aws.amazon.com/ja_jp/AmazonECS/latest/developerguide/docker-volumes.html)が参考になりそうです。

- docker構築全般的に使える考え方として、ロジック的には動くはずなのに動かず困ったら、まずエラーを見ると上手くいきやすいです。そして、複数のコンテナを立ち上げている場合はエラーログが複数箇所にある場合があるので、今見ているところ以外のエラーログもないか探してみると良いです。


今回の記事は以上になります。

## 余談
筆者は今回企業内で細かい部分の設定（メンテナンス）を自分たちで行えるようにするためにわざわざdocker環境構築をしましたが、作らなくていいものは作らないほうが賢いと考えています。

laravelには開発環境として`laravel-sail`、本番環境構築としては`Laravel Forge`のサポートが準備されています。

今回筆者はこの両方を自作したわけですが、そもそも開発でインフラの細かい設定が不要の場合は、以下の公式リファレンスでも記載されているとおり`Laravel Forge`にまるっと任せてしまったほうが今回のようなエラーに悩むこともなく目的達成に早く近づけるので断然おすすめです。

このようにdocker構築の是非は目的によって変わりますが、１つの知識として作らなくて済む技術があるということを頭の片隅にでもおいていただければ今後皆さんの何かのお役に立てるのではないかと思っています。ご参考までに。

laravel8の公式リファレンス： https://readouble.com/laravel/8.x/ja/deployment.html

## あとがき
今回は会社のアドベントカレンダーに参加ということで、いつもは単発で書くエラー記事を詰め込んで内容マシマシで書いてみました。
面白いところでつまづいてるな（うるる社の技術はレベルが高そう）など興味をもってくれた方は、ぜひ自分以外のうるる社のアドベントカレンダーの記事にも目を通していただけると嬉しいです。
自分は平社員ですが、今回うるる社のアドベントカレンダーでは弊社の重鎮たちがこぞって記事を書いており、参考になる記事が多くなっていると思われるためとてもおすすめです。

明日は [lifegood](https://qiita.com/lifegood) さんによる記事です！お楽しみに！