Docker × Laravel × Nginx

## 背景

Laravel × VueのSPAのTODOアプリか何かを作成したくなったので備忘録として。

## 概要

Dockerを利用してapiコンテナとnginxコンテナを立ち上げ初期画面でlaravelプロジェクトを表示させる。

## 環境

- Windows10
- Ubuntu (WSL2)
- Docker (20.10.12)
- Docker-compose(1.29.2)

- PHP(8.0.26)
- Laravel(8.6.12)
- nginx(1.23.3)
## ディレクトリ構造

```
sample-prj
├ api(laravel用)
├ devcontainer
│ ├ nginx
│ │ └ default.conf
│ └ php
│ 　 └ Dockerfile
└ docker-compose.yml
```

## docker-compose.yml（nginx）

- nginxはDcokerfileを使用せず公式のイメージを使用するためわざわざDockerfileに切り出して作成する必要がないので直接docker-compose.ymlに記述していく

※ 今回作成するものは簡易的なものなので、Dockerfileは使用しないが、カスタムでいろいろ指定したい場合はDockerfileに記述していく必要があるかも

```jsx
version: '3'
services:
	nginx:
		image: nginx
	  container_name: nginx
	  ports:
	    - 8080:80
		volumes:
      - ./api:/var/www/localhost/htdocs
      - ./docker/nginx/default.conf:/etc/nginx/conf.d/default.conf
```

- `image` ：ベースとなるイメージを指定している
    - [Nginx - Official Image | Docker Hub](https://hub.docker.com/_/nginx) 

- `ports`：ポートのマッピングを指定している
    - クラインアントが8080でアクセスすると、nginxの80番ポートにリクエストが流されることで通信が可能になる
※8080という数字は環境に合わせて適宜なんでも

- `volumes`：ホスト側とコンテナ側のファイル群をマウントする
    - ホスト - コンテナ間のマウント
    - nginxのサーバー設定を行うdefault.confをコンテナ内に配置する

## default.confを作成する

```
server {
    listen 80;
    listen [::]:80;
    root /var/www/localhost/htdocs/public;

    add_header X-Frame-Options "SAMEORIGIN";
    add_header X-Content-Type-Options "nosniff";

    index index.php;

    charset utf-8;

    location / {
        try_files $uri $uri/ /index.php?$query_string;
    }

    location = /favicon.ico { access_log off; log_not_found off; }
    location = /robots.txt  { access_log off; log_not_found off; }

    error_page 404 /index.php;

    location ~ \.php$ {
        fastcgi_pass api:9000;
        fastcgi_param SCRIPT_FILENAME $realpath_root$fastcgi_script_name;
        include fastcgi_params;
    }

    location ~ /\.(?!well-known).* {
        deny all;
    }
}
```
##### ここでは、Nginxがphp-fpm（アプリケーションサーバ）と通信するための設定を記述

- `server`：ディレクティブ。この内にサーバの設定を記述
- `listen`：待ち受けるポート番号を指定
    - nginx側で80番ポートにアクセスを流す設定
- `root`：ドキュメントルート（Webサーバで基準になるディレクトリ）の指定
    - public/test.phpにアクセスした際に、/var/www/localhost/htdocs/public/test.phpが呼び出される
- `add_header X-Frame-Options "SAMEORIGIN"`：クリックジャッキング攻撃の防止。
    - 他ドメインからiframeでサイトを表示させるのを防ぐ

- `add_header X-Content-Type-Options "nosniff"`：MIMEタイプのスニッフィングを防止
- `index`：インデックスページを指定
- `charset utf-8`：Content_Typeに付与する文字コード
- `error_page`：エラーコードとエラーページのURI
- `location`：指定したURL毎のディレクティブ指定
    - locationディレクティブは、デフォルトでは「前方一致」なため、この場合（location /）だと/配下の全てのURLに対してこの設定が反映される

- `fastcgi_pass`：FastCGIサーバーのアドレス
    - apiコンテナとそのポート番号を指定（php-fpmコンテナのポートはデフォルトで9000）
    └これによりPHPファイルの処理がphp-fpmで実行される

- `include`：	他の設定ファイルを読み込む
    - この場合、`fastdgi_param` を読み込んでいる

- ` location ~ /\.(?!well-known).* {
        deny all;
    }`：
   - 「.」で始まるファイルや「.」で始まるディレクトリ下のファイルへのアクセスを禁止する
   - 「.well-known」ディレクトリ下のファイルはアクセスを禁止しない

参考：
・[DockerとNginxを使ったLaravelの開発環境構築【最小限のコード】](https://www.engilaboo.com/laravel-nginx-docker/)
・[nginx.confの歩き方。NginxでLaravelを動かすための設定を読み解く](https://www.cyberowl.co.jp/blog/technology/1230)
・[Nginx について纏めてみる](https://qiita.com/leomaro7/items/98d6af85a4e155449aed)


## コンテナを起動（nginx）

- 下記コマンド実行してコンテナを立ち上げる
`docker-compose up -d`

- 下記コマンドでコンテナが立ち上がっているか確認
`docker-compose ps`
コンテナ名 `nginx` が `status` `up`（起動）していることを確認


## Dockerfileを作成する（PHP）
##### アプリケーションを動かすサーバーとして動作するコンテナをDockerfileを使用して作成

```docker
FROM php:8.0-fpm

# Install packages
RUN apt update && \
		apt install -y \
          wget \
          git \
          unzip\
          libpq-dev \
          zlib1g-dev \
          mariadb-client \
          libzip-dev \
          libonig-dev \

# Install PHP Extensions
RUN docker-php-ext-install \
        zip \
        pdo_mysql

# Install Composer
RUN curl -sS https://getcomposer.org/installer | php \
    && mv composer.phar /usr/local/bin/composer \
    && composer self-update

# Application Settings
RUN curl -L https://raw.githubusercontent.com/php/php-src/master/php.ini-production \
    | sed 's/;opcache.enable=1/opcache.enable=${OPCACHE_ENABLE}/' \
    | sed 's/max_execution_time = 30/max_execution_time = 300/' \
    | sed 's/expose_php = On/expose_php = Off/' \
    | sed 's/post_max_size = 8M/post_max_size = 300000M/' \
    | sed 's/upload_max_filesize = 2M/upload_max_filesize = 300000M/' \
    | sed 's/memory_limit = 128M/memory_limit = 1024M/' > /usr/local/etc/php/php.ini

# Working Directory
WORKDIR /var/www/localhost/htdocs
ADD ./ /var/www/localhost/htdocs
```

##### Dockerfileの中身の解説

```docker
From php:fpm-8.0 
```

##### DockerHubが公式で提供してくれているコンテナを作成するためのベースとなるイメージを指定している

- 公式パッケージ以外にもalpine linux, debian, windows等ベースとなるイメージが存在する
    - 選択については、各自の作成するアプリケーションの内容や環境によって異なるが、alpine系は軽量使用することができる。
    - デメリットもあり、軽量化されているため一部の言語などでは速度が遅くなったり、パッケージがそろっていないなど懸念点もある。

参考記事：
[Docker image(イメージ)の違い:alpine,bullseye, buster, slim, stretch, jessie, slim-buster, windowsservercore, latestどれを選ぶべきか？](https://prograshi.com/platform/docker/docker-image-tags-difference/)

##### ちなみにphp-fpmってあるけどこれ何？
> FPM (FastCGI Process Manager) は、PHP における FastCGI 実装です。(主に)高負荷のサイトで有用な機能が含まれています。

[FastCGI Process Manager (FPM) ¶](https://www.php.net/manual/ja/install.fpm.php)

FastCGIはWebサーバのプログラムを一度起動させてしばらくメモリ上へ保持し待機させるという特徴があり、このことから、簡単に言ってしまえば、PHPをFastCGIで実行することで「負荷軽減」できるということ


```docker
# Install packages
RUN apt update && \
    apt install -y \
        wget \ → HTTP/S、FTP/S通信でコマンドライン上でファイルを取得するためのもの
        git \　→ git操作
        unzip\ → zipファイルを扱う
        libpq-dev → PostgreSQLデータベースバックエンドと通信するためのもの
```
##### OSで扱うパッケージのインストールを行う

- `RUN` ：Dockerイメージ作成時に実行するコマンド
- `&&` ：複数のコマンドをつなぎ、 `\` で改行
- `&&` と `\` をワンライナーで書くこともできる

[Intro Guide to Dockerfile Best Practices - Docker Blog](https://www.docker.com/blog/intro-guide-to-dockerfile-best-practices/#:~:text=Tip%20%233%3A%20Identify%20cacheable%20units%20such%20as%20apt%2Dget%20update%20%26%20install)

- `apt-get update` ：インストール可能なパッケージを更新
- `apt-get -y install`：パッケージのインストール

```docker
# Install PHP Extensions
RUN apt install -y \
        docker-php-ext-install \
        zip \　→ PHPでzip関連を扱う
        pdo_mysql → PHPからMySQLデータベースへのアクセスを可能にするためのドライバ
```
##### PHPモジュールのインストールを行う

- `docker-php-ext-install`：PHPに標準で備わっている拡張パッケージのインストール＆有効化ができる （[PHPモジュールを詳しく見る](https://www.php.net/manual/ja/extensions.alphabetical.php)）
- ※`pecl install`：PHPに標準で備わっていないパッケージをインストールできる

```docker
# Install Composer
RUN curl -sS https://getcomposer.org/installer | php \
    && mv composer.phar /usr/local/bin/composer \　→どのディレクトリにいてもcomposerが使えるようにシンボリックを貼る
    && composer self-update
```
##### Composerのインストール

- `curl -sS`：ファイル出力時の進捗状況を非表示＆エラー表示

※参照：[よく使うcurlコマンドのオプション](https://qiita.com/ryuichi1208/items/e4e1b27ff7d54a66dcd9)


```docker
# Application Settings
RUN curl -L https://raw.githubusercontent.com/php/php-src/master/php.ini-production \
    | sed 's/;opcache.enable=1/opcache.enable=${OPCACHE_ENABLE}/' \
    | sed 's/max_execution_time = 30/max_execution_time = 300/' \
    | sed 's/expose_php = On/expose_php = Off/' \
    | sed 's/post_max_size = 8M/post_max_size = 300000M/' \
    | sed 's/upload_max_filesize = 2M/upload_max_filesize = 300000M/' \
    | sed 's/memory_limit = 128M/memory_limit = 1024M/' > /usr/local/etc/php/php.ini
```
##### PHP.ini設定ファイルを編集

- https://github.com/php/php-src/blob/master/php.ini-production
ここからphp.ini（phpの設定ファイル）をダウンロードしてくる

- `sed` ：文字列置換で利用するコマンド (sed [オプション]/スクリプトコマンド/入力ファイル/)
※[sedを使用して設定ファイルの変更](https://www.wakuwakubank.com/posts/338-linux-sed/#:~:text=5555%0A6666%0A7777-,%E8%A8%AD%E5%AE%9A%E3%83%95%E3%82%A1%E3%82%A4%E3%83%AB%E3%81%AE%E8%A8%AD%E5%AE%9A%E5%80%A4%E5%A4%89%E6%9B%B4,-sed%E3%82%92%E6%B4%BB%E7%94%A8)

- php.iniの設定項目は適宜環境に合わせて変える
    - 今回はわかりやすく大きく変更していますが、例えば簡易的なアプリケーションでは`memory_limit`は256Mぐらいで事足りるでしょう。

参考：
・[PHP7.4 ぼくのかんがえたさいきょうのphp.ini](https://qiita.com/ucan-lab/items/0d74378e1b9ba81699a9)
・[設定ファイル ¶](https://www.php.net/manual/ja/configuration.file.php)

##### php.ini-development　VS　php.ini-production
- php.iniのもととなるファイルとして、php.ini-development（開発環境）とphp.ini-production（本番環境）の２つがある

> ; php.ini-production contains settings which hold security, performance and
; best practices at its core. But please be aware, these settings may break
; compatibility with older or less security-conscious applications. We
; recommending using the production ini in production and testing environments.
```
php.ini-production には、セキュリティ、パフォーマンス、およびその核となるベストプラクティスが含まれている。
ただし、これらの設定が、古いアプリケーションやセキュリティ意識の低いアプリケーションの間で壊れる可能性があることに注意してください。
本番環境およびテスト環境では、php.ini-production を使用することをお勧めします。
```
> ; php.ini-development is very similar to its production variant, except it is
; much more verbose when it comes to errors. We recommend using the
; development version only in development environments, as errors shown to
; application users can inadvertently leak otherwise secure information.

```
php.ini-development は、エラーに関して、本番用と比べてはるかに詳細だという点を除いては、本番環境用と非常によく似ています。開発環境用のものは開発環境でのみ使用することをお勧めします。
アプリケーションのユーザーに表示されるエラーは、セキュリティで保護されている情報を誤って漏洩する可能性があります。
```
- この２つの違いは、主に開発時にはデバッグがしやすいように、本番では無駄なリソースの消費がないように設定されている
- また本番環境で開発環境用のものを使わないように注意する

参考：[PHP 8.0 の設定ファイル php.ini-development と php.ini-production の違い](https://www.ritolab.com/posts/219#phpini-development-%E3%81%A8-phpini-production-%E3%81%AE%E9%81%95%E3%81%84)


```docker
# Working Directory
WORKDIR /var/www/localhost/htdocs
ADD ./ /var/www/localhost/htdocs
```
##### コンテナログイン時に作業ディレクトリとなるディレクトリの指定とソースコードの追加

参考：[Dockerfile の ADD と COPY の違いを結論から書く](https://qiita.com/YumaInaura/items/1647e509f83462a37494)

## docker-compose.yml（php）

```
version: '3'
services:
  api:
    build: ./devcontainer/php
    container_name: api
    volumes:
      - ./api:/var/www/localhost/htdocs
```

- `services`：アプリケーションを動かす各サービスの要素
    - api(laravelアプリケーション)のサービスとして、いろいろな設定をネストさせて書いていく

- `build`：実行するComposeFileのパスを指定する

- `container_name`：作成するコンテナに名前を付ける

- `volumes`：ホスト側とコンテナ側でファイルを共有ファイルとしてマウントしている
    - ここではホスト側の `./api`  以下のファイルと `/var/www/localhost/htdocs` 以下マウントしている

## コンテナを起動

- 下記コマンド実行してコンテナを立ち上げる
`docker-compose up -d —build`

※Dockerfileの記述コードが長いと数分かかるときもある

- 下記コマンドでコンテナが立ち上がっているか確認
`docker-compose ps`

コンテナ名 `api` が `status` `up`（起動）していることを確認

- 下記コマンドでコンテナの中に入ってみる
`docker-compose exec api bash`
コンテナにログインした後、作業ディレクトリとして指定した `/var/www/localhost/htdocs` が作業ディレクトリとなっていることが確認できる

- 試しにvolumesで指定したファイルマウントができるか確認してみる
`touch test.html`
    - コンテナから抜けてホスト側のディレクトリを確認してみる
    - ホスト側でapiディレクトリ直下に先ほどのファイルが作成されていることが確認できる


一旦ここまでで、アプリケーションサーバー側のコンテナは作成完了
※この後laravelのインストールなどを行っていく

## サーバーの動作確認
api/public 以下に test.php を作成する
```
mkdir api/public
touch api/public/test.php
```

test.phpの編集
```php
<?php
echo 'PHP表示テスト';
phpinfo();
```

`http://localhost:8080/test.php`
 にアクセスすると以下のように表示され、webサーバーが正しく動作していることが確認できる
![phpinfo().png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2520232/ab267d27-c6d1-32d8-f6d3-2e9bda6e8e90.png)
※確認後public/は削除する

## Laravelのインストール
##### laravelの初期ページを表示するために、まずはlaravelのインストールを行う

- apiコンテナに入り以下のコマンドを実行
        `composer create-project laravel/laravel . "8.*"`

- Laravelがインストールされていることの確認
```
/var/www/localhost/htdocs# php artisan -V
Laravel Framework 8.83.27
```

- docker-compose.ymlのvolumesによってホスト側とコンテナ側にlaravelプロジェクトがマウントされていることの確認

- 最後に [localhost:8080](http://localhost:8080) にアクセスして画像のような、laravelのウェルカムページが表示されていることの確認
※8080はdocker-compose.ymlで定義したポート番号によって異なる

![laravel top.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2520232/1a16cff7-c3e3-437f-f35c-3bb49d780f75.png)
無事laravelの初期画面が表示されました！！！

##### 注意
下記のファイルに対しての書き込みが権限がなく、エラーが表示されることがあります。
その際はそれぞれのコマンドを実行することで、権限を付与するとエラーが出なくなります。

`storage/logs/laravel.log`　→　`chmod 777 storage/logs/`
`storage/framework/sessions/~~~~~~~`　→　`chmod 777 storage/frameworks/sessions/`
`storage/framework/views/~~~~~~~~` →　`chmod 777 storage/frameworks/views/`