コンテナ環境でLaravelを動かす


# 概要
自分用の備忘録です。
用語が間違っていたり、理解が微妙に違うこともあるかもしれませんが、あくまで自分用ですのでご了承ください。

# 目標
コンテナ環境を構築して、nginx経由でLaravelにアクセスできるようにします。
LaravelからはMysqlやRedisにもアクセスできることとします。

# 使用バージョン
2023/6/16時点でのLaravel最新バージョンを使用。
各ソフトウェアのバージョンもそれに合わせたものを使用します。

    Laravel:10
    PHP:8.2
    nginx: 1.25.1
    mysql: 8.1.0
    redis: 7.0

# 手順
## ディレクトリを用意する
今回の作業ディレクトリは`study`という名前で用意しました。
この作業ディレクトリに`docker`ディレクトリを作成します。

```
$ mkdir docker
$ ls
docker    
```

コンテナに関するファイルは、こちらのdockerディレクトリに追加していきます。

## Dockerfileを用意する
### nginxのDockerfileを作成
dockerディレクトリにnginxディレクトリを作成し、そのディレクトリ内に`Dockerfile`を作成します。

```
$ pwd
/Users/username/study/docker
$ mkdir nginx
$ ls
nginx
$ cd nginx
$ vim Dockerfile
```

今回はDockerHubの公式イメージを元に少し調整したものを使用します。
[Docker Hub nginx Official Image](https://hub.docker.com/_/nginx)
※既存イメージをそのまま使用する場合はDockerfileは不要となります。
    
```txt:docker/nginx/Dockerfile
FROM nginx:1.25.1

ENV LANG ja_JP.UTF-8
```

#### nginxコンテナを動かしてみる
正しく`Dockerfile`が用意できたことを確認するために、試しにnginxコンテナを動かしてローカルアクセスしてみます。
この手順はスキップ可能です。

```
$ docker build -t nginx:latest .
$ docker run -it -d -p 80:80 --name nginx_container nginx
$ docker exec -it nginx_container bash
$ nginx -v ←コンテナ内で実行
nginx version: nginx/1.25.1
```

バージョンが表示されれば成功です。
Webブラウザで`localhost:80`にアクセスするとnginxのデフォルトページが表示されます。
起動するときはポートの指定を忘れないように…！

### PHPのDockerfileを作成
dockerディレクトリにphpディレクトリを作成し、このディレクトリ内に`Dockerfile`を作成します。

```
$ pwd
/Users/username/study/docker
$ mkdir php
$ ls
php
$ cd php
$ vim Dockerfile
```

今回はDocker Hubの公式イメージを元に少し調整したものを使用します。
[Docker Hub php Official Image](https://hub.docker.com/_/php)

```txt:docker/php/Dockerfile
FROM php:8.2-fpm

ENV LANG ja_JP.UTF-8
```

後でたくさん書き足すのですが、順を追って書いていきたいのでひとまずこれだけ記載しておきます。

### MysqlのDockerfileを作成
dockerディレクトにmysqlディレクトリを作成し、mysqlディレクトリ内にDockerfileを作成します。

```
$ pwd
/Users/username/study/docker
$ mkdir mysql
$ ls
mysql
$ cd mysql
$ vim Dockerfile
```

今回はDockerHubの公式イメージを少し調整したものを使用します。
[Docker Hub mysql Official Image](https://hub.docker.com/_/mysql)

```txt:docker/mysql/Dockerfile
FROM mysql:8.1.0

ENV LANG ja_JP.UTF-8
```

### reidsのDockerfileを作成
dockerディレクトリにredisディレクトリを作成し、redisディレクトリ内にDockerfileを作成します。

```
$ pwd
/Users/username/study/docker
$ mkdir redis
$ ls
redis
$ cd redis
$ vim Dockerfile
```

今回はDocker Hubの公式イメージを元に少し調整したものを使用します。
[Docker Hub redis Official Image](https://hub.docker.com/_/redis)

```txt:docker/redis/Dockerfile
FROM redis:7.0

ENV LANG ja_JP.UTF-8
```


## docker-composeを作成
docker-composeは複数のコンテナを一括管理するためのツールです。
docker-compose.ymlにあらかじめ各コンテナをオプション等を起動することで、毎回オプションを入力することなく、自動でコンテナの起動などを行うことができます。

dockerディレクトリと同じ階層に`docker-compose.yml`を作成します。

```
$ pwd
/Users/username/study
vim docker-compose.yml
```

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

services:
  nginx:
    build: docker/nginx
    ports:
      - "80:80"
    depends_on:
      - php-fpm

  php-fpm:
    build: docker/php
    depends_on:
      - mysql
      - redis

  mysql:
    build: docker/mysql
    volumes:
      - docker/mysql/var/lib/mysql:/var/lib/mysql
    environment:
      MYSQL_ROOT_PASSWORD: root
      MYSQL_DATABASE: test
      MYSQL_USER: test
      MYSQL_PASSWORD: test

  redis:
    build: docker/redis
```

##### nginx
nginxコンテナにはコンテナの外からアクセスがいくので、ポートを指定しておく必要があります。
今回はホストコンピュータ/nginxコンテナの両ポート共に80に設定してあります。
また、nginxコンテナはphp-fpmコンテナ起動後に起動してほしいため、起動順序の設定も記載しておきます。

##### php-fpm
また、php-fpmコンテナはmysqlコンテナとredisコンテナの起動後に起動してほしいため、起動順序の設定を記載しておきます。

##### mysql
mysqlコンテナは環境変数をいくつか設定する必要があります。
公式ドキュメントに沿って設定していきます。

`MYSQL_ROOT_PASSWORD`
ルートユーザーのパスワードを設定します。この変数は必須です。
本来はファイルに機密事項を記載しない方がよく、別ファイルから変数を読み込む方法がありますが、ひとまず割愛します。

`MYSQL_DATABASE`
コンテナ起動時に作成するデータベースを指定します。
ユーザー/パスワードを指定した場合、そのユーザーにはGRANT ALLのアクセス権が付与されます。

`MYSQL_USER`, `MYSQL_PASSWORD`
MYSQL_DATABASEで作成したデーターベースにアクセスできるユーザーを作成します。
この変数はオプションであり必須ではありませんが、ユーザー作成をする場合は両オプションを指定する必要があります。

また、今回使用するデータベースでは、コンテナを停止した際にもデータが消えないようにしたいので、追加で設定をしていきます。
公式ドキュメントに記載されている方法は2通りありますが、今回はホストコンピュータにデータ保存用の`var`ディレクトリを作成し、`/var/lib_mysql`ディレクトリにマウントします。
マウントの設定は上記の`docker-compose.yml`に記載した通りで、マウント用のディレクトリは下記手順で作成します。

```
$ pwd
/Users/username/study/docker/mysql
$ mkdir var
$ ls
Dockerfile var
```

コンテナを起動し、データが作成されると、varディレクトリにも同じファイルが保存されます。

##### redis
特に追加で必要な設定はないため、`build`のみ指定しておきます。

## docker-composeでコンテナを起動

```
docker-compose up -d
```

上記コマンドを実行することで、イメージのビルドからコンテナ起動までを一括で行ってくれます。
イメージが存在する場合はビルドがスキップされるので、`Dockerfile`を更新した場合は`--build`オプションをつけて実行します。
また、`作業ディレクトリ名_default`という名前でネットワーク作成し、各コンテナを`service`項目で指定したサービス名でこのネットワークに接続してくれます。

## Laravelアプリケーションを作成
コンテナの作成が完了したので、Laravelアプリケーションを作成し、各コンテナをLaravel用に調整していきます。

### 必要なコマンドをインストール
LaravelのインストールにはGitやunzipなどのコマンドが必要になるため、php-fpmコンテナのDockerfileにインストールのコマンドを追記します。
今回使用しているPHPのイメージは`debian:12-slim`を元に作成されており、パッケージのインストールには`apt`/`apt-get`コマンドが使用できるようになっています。

```txt:docker/php/Dockerfile
RUN apt-get update \
    && apt-get install -y git zlib1g-dev libzip-dev unzip \
    && pecl install zip \
    && docker-php-ext-enable zip
```

RUNコマンドは元となるdockerイメージに対して実行する処理を記述します。
バックスラッシュを用いることで、複数行に分けて記載可能です（見やすいように改行して記載できる）。
&&をつけることで1つのRUNコマンドに対して複数の処理を記載でき、これらを1つのレイヤとしてみなされるため、軽量になります。

コンテナに入って、試しにGitがインストールされていることを確認してみます。
php-fpmコンテナのイメージを更新したので、コンテナに反映するためには再ビルドする必要があります。
現在コンテナが起動状態のため、一度コンテナを停止・削除してから再ビルド・コンテナ作成・起動を行います。

```
$ docker-compose down
$ docker-compose up -d
$ docker exec -it <CONTAINER ID> bash
$ git -v
git version 2.39.2
```

Gitのバージョンが表示されることが確認できました。
確認が済んだので、一旦コンテナを落としておきます。

```
$ exit
$ docker-compose down
```

### Composerをインストール
php-fpmコンテナにComposerをインストールします。
今回はComposer公式ドキュメントに記載されている方法でインストールするので、インストールコマンドを実行するように、php-fpmコンテナの`Dockerfile`に実行コマンドを追記します。

```txt:docker/php/Dockerfile
RUN php -r "copy('https://getcomposer.org/installer', 'composer-setup.php');" \
   && php -r "if (hash_file('sha384', 'composer-setup.php') === 'e21205b207c3ff031906575712edab6f13eb0b361f2085f1f1237b7126d785e826a450292b6cfd1d64d92e6563bbde02') { echo 'Installer verified'; } else { echo 'Installer corrupt'; unlink('composer-setup.php'); } echo PHP_EOL;" \
   && php composer-setup.php \
   && php -r "unlink('composer-setup.php');" \
   && mv composer.phar /usr/local/bin/composer
```

php-fpmコンテナのイメージを更新したので、コンテナに反映するためには再ビルドが必要です。

```
$ docker-compose up -d --build
```

これで再起動が完了しました。
php-fpmコンテナの中に入って、Composerがインストールされていることを確認してみます。

```
$ docker exec -it <CONTAINER ID> bash
$ composer -v
```

上記コマンドでComposerのバージョンが表示されればOK。
確認ができたので、一旦コンテナを落としておきます。

```
$ exit
$ docker-compose down
```

### Laravelをインストール
次に、Composerを使用してLaravelをインストールします。
Composerはphp-fpmコンテナにインストールされているため、php-fpmコンテナ内でLaravelのインストールを行います。
また、コンテナ内のLaravelプロジェクト用ディレクトリとローカルのディレクトリをマウントさせておくことで、ローカルでの差分をすぐにコンテナに反映させることができるため、マウントの設定も追加していきます。

まず、ローカルにマウント用のprojectディレクトリを作成します。

```
$ pwd
/Users/username/study
$ mkdir project
$ ls
docker docker-compose.yml project
```

docker-compose.ymlにマウントの設定を足します。
projectディレクトリには書き込み/読み取りの権限を付与しておきます。

```
  php-fpm:
    build: ./docker/php
    volumes:　　　　　　　　　　　　　　　　　　　　　　　　　　　　　　　　　　　　　
      - ./project:/project:rw　　← NEW!
    depends_on:
      - mysql
      - redis
```

これでもう一度起動すると、projectディレクトリがマウントされます。
もう一度コンテナを起動し直して、php-fpmコンテナ内でLaravelプロジェクトをインストールします。

```txt:php-fpmコンテナ内
$ cd /
$ composer create-project laravel/laravel project
```

インストール完了後にローカルのprojectディレクトリを確認すると、Laravelがインストールされています。

### localhostでLaravelにアクセスする
locahostにアクセスしたときに、Laravelのページが表示されるようにします。
そのためにはnginxの設定を修正する必要があります。

#### nginxのconfファイルを修正
localhostにアクセスしたときに、Laravelの`public/index.php`が表示されるように修正する必要があります。
nginxコンテナの`default.conf`をローカルにコピーして、必要に応じて修正したものを、コンテナ起動時にnginxコンテナの`default.conf`に上書きします。
ローカルのファイルは`docker/nginx/conf/default.conf`にコピーします。

```txt:ローカル
$ pwd
/Users/username/study/docker/nginx
$ mkdir conf
$ cd conf
$ vim default.conf
```

```txt:nginxコンテナ
$ cat /etc/nginx/conf.d/default.conf
```

nginxコンテナの`default.conf`の内容は以下の通りです。

```/etc/nginx/conf.d/default.conf
server {
    listen       80;
    listen  [::]:80;
    server_name  localhost;

    #access_log  /var/log/nginx/host.access.log  main;

    location / {
        root   /usr/share/nginx/html;
        index  index.html index.htm;
    }

    #error_page  404              /404.html;

    # redirect server error pages to the static page /50x.html
    #
    error_page   500 502 503 504  /50x.html;
    location = /50x.html {
        root   /usr/share/nginx/html;
    }

    # proxy the PHP scripts to Apache listening on 127.0.0.1:80
    #
    #location ~ \.php$ {
    #    proxy_pass   http://127.0.0.1;
    #}

    # pass the PHP scripts to FastCGI server listening on 127.0.0.1:9000
    #
    #location ~ \.php$ {
    #    root           html;
    #    fastcgi_pass   127.0.0.1:9000;
    #    fastcgi_index  index.php;
    #    fastcgi_param  SCRIPT_FILENAME  /scripts$fastcgi_script_name;
    #    include        fastcgi_params;
    #}

    # deny access to .htaccess files, if Apache's document root
    # concurs with nginx's one
    #
    #location ~ /\.ht {
    #    deny  all;
    #}
}

```


catコマンドで表示・コピーしてきたものを、ローカルのdefault.confにコピーします。
ここから[Laravel公式ドキュメント](https://laravel.com/docs/10.x/deployment#nginx)を参考に設定を修正していきます。

```text:docker/nginx/conf/default.conf
server {
    :
    :
    root /project/public; 
    index index.php

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

Laravelでは全ての処理を`public/index.phpから始めます。
そのため、locationのrootをproject/public、indexをindex.phpに変更し、locationの外に記述します。
`try_files`は左から順にファイルまたはディレクトリを探してくれる設定です。
さらに下記の設定も修正します。

```text:docker/nginx/conf/default.conf
server {
    :
    :

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

`fastcgi_pass`にはphp-fpmコンテナのサービス名とポート番号を指定します。
ポート番号は`docker ps`または`docker-compose ps`コマンドを実行することで確認できます。
`fastcgi_param`と`include`の値はは公式ドキュメントのままです。

次に、この`default.conf`をコンテナ起動時にphp-fpmコンテナの`default.conf`に上書きするための設定を追加します。

```txt:docker/nginx/Dockerfile
COPY conf/default.conf /etc/nginx/conf.d/default.conf
```

`COPY`コマンドで上書きが可能です。
ローカルに置いてあるファイルは`Dockerfile`から見たパスを記載します。

また、nginxコンテナにもLaravelプロジェクトを配置するため、`docker-compose.yml`にマウントの設定を追加します。

```txt:docker-compose.yml
  nginx:
    build: ./docker/nginx
    volumes:
      - ./project:/project:ro　← NEW!
    ports:
      - "80:80"
    depends_on:
      - php-fpm
```

設定が完了したのでイメージのビルド/コンテナの立ち上げを行います。
localhostにアクセスすると、Laravelのデフォルトのページが表示されます。

### LaravelからDBにアクセス
LaravelからDBにアクセスするには設定の追加が必要になります。
LaravelのDBアクセス時に使用するドライバーの設定は、`config/database.php`に記載されており、さらにそこではenvファイルからの読み込みが行われています。
なので、`.env`ファイルを修正する必要があります。

```txt:.env
DB_CONNECTION=mysql
DB_HOST=mysql
DB_PORT=3306
DB_DATABASE=test
DB_USERNAME=test
DB_PASSWORD=test
```

`DB_HOST`には`docker-compose.yml`で設定したMysqlコンテナのサービス名、`DB_DATABASE`/`DB_USERNAME`/`DB_PASSWORD`にはそれぞれ`MYSQL_DATABASE`/`MYSQL_USER`/`MYSQL_PASSWORD`を記述します。

また、この設定を追加しただけではうまくドライバーを読み込んでくれないので、拡張モジュールをインストールする必要があります。
`config/database.php`のmysqlのoptionsに`pdo_mysql`をロードする旨が記載されているため、`pdo_mysql`をインストールするようphp-fpmコンテナ用のDockerfileに下記を追記します。

```txt:docker/php/Dockerfile
RUN docker-php-ext-install pdo_mysql
```

ここでは新たにRUNコマンドを記載していますが、実際のファイルにはgit等をインストールしたときに記載したRUNコマンドに追記する形で記載しています。

これでDBにアクセスできるはず。
確認のために、適当なテーブルとデータを作成し、そのデータをLaravel側から読み込みできるようにしてみます。
まずmysqlコンテナからmysqlにログインし、testDBに`example_table`テーブルを作成します。
また、2件の適当なデータも作成しておきます。

```
$ docker exec -it <CONTAINER ID> bash
$ mysql -utest -p --port 3306
mysql > use test
mysql > create table example_table (id int(11) auto_increment not null, name varchar(30) not null, age int(3) not null, primary key (id));
mysql> desc example_table;
+-------+-------------+------+-----+---------+----------------+
| Field | Type        | Null | Key | Default | Extra          |
+-------+-------------+------+-----+---------+----------------+
| id    | int         | NO   | PRI | NULL    | auto_increment |
| name  | varchar(30) | NO   |     | NULL    |                |
| age   | int         | NO   |     | NULL    |                |
+-------+-------------+------+-----+---------+----------------+
mysql> insert into example_table (name, age) values ('Sato', 25), ('Tanaka', 30);
mysql> select * from example_table;
+----+--------+-----+
| id | name   | age |
+----+--------+-----+
|  1 | Sato   |  25 |
|  2 | Tanaka |  30 |
+----+--------+-----+
```

データの作成が完了したので、artisanコマンドを使って実際にDBアクセスしてみます。

```txt:php-fpmコンテナ内
$ pwd
/project
php artisan db:table example_table
  example_table ...........................................  
  Columns ............................................... 3  
  Size ............................................ 0.02MiB  

  Column ............................................. Type  
  id autoincrement, integer ....................... integer  
  name string ...................................... string  
  age integer ..................................... integer  

  Index ...................................................  
  PRIMARY id .............................. unique, primary  

```

テーブル情報が正しく取得できました。
次にtinkerを実行し、テーブルデータを取得できるかどうかも確認してみます。

```txt:php-fpmコンテナ内
$ pwd
/project
$ php artisan tinker
> use Illuminate\Support\Facades\DB;
> DB::table('example_table')->get()
= Illuminate\Support\Collection {#6225
    all: [
      {#6228
        +"id": 1,
        +"name": "Sato",
        +"age": 25,
      },
      {#6230
        +"id": 2,
        +"name": "Tanaka",
        +"age": 30,
      },
    ],
  }

```

データを取得できることが確認できました。

さらに、php-fpmコンテナからartisanコマンド経由でmysqlにログインできるようにします。
php-fpmコンテナの`Dockerfile`に`mysql-cli`のインストールを追記します。

```txt:docker/php/Dockerfile
RUN apt-get install -y git default-mysql-client
```

再度ビルドし、立ち上げ直すとmysq-cliがインストールされます。
実際にartisanコマンドを叩いてみます。

```txt:php-fpmコンテナ内
$ pwd
/project
$ php artisan db
Welcome to the MariaDB monitor.  Commands end with ; or \g.
Your MySQL connection id is 9
Server version: 8.1.0 MySQL Community Server - GPL

Copyright (c) 2000, 2018, Oracle, MariaDB Corporation Ab and others.

MySQL [test]> 
```

artisanコマンド経由でmysqlにログインすることができました。

### LaravelからRedisにアクセス
LaravelからRedisにアクセスするには設定の追加が必要になります。
LaravelのRedisアクセス時に使用するドライバーの設定は、`config/database.php`に記載されており、さらにそこではenvファイルからの読み込みが行われています。
なので、`.env`ファイルを修正する必要があります。

```txt:.env
REDIS_HOST=redis
REDIS_PASSWORD=null
REDIS_PORT=6379
```

`REDIS_HOST`には`docker-compose.yml`で設定したRedisコンテナのサービス名を記述します。

また、この設定を追加しただけではうまくドライバーを読み込んでくれないので、php-fpmコンテナに拡張モジュールを追加します。

```txt:docker/php/Dockerfile
RUN pecl install redis \
    && docker-php-ext-enable redis
```

これで動くようになったか実際に確認してみます。
artisanコマンド経由でRedisに適当な値を保存してみます。

```txt:php-fpmコンテナ内
$ pwd
/project
$ php artisan tinker
> use Illuminate\Support\Facades\Redis;
> Redis::set(“example”, "test");
= true
```

Redisファサードを使用して保存したため、`config/database.php`の`default`の設定を使用しています。
今回はホスト以外の値はデフォルトのままなので、redisコンテナのDB0に保存されているはずです。
また、`REDIS_PREFIX`の設定をしていないため、デフォルトの設定により`laravel_database_example`というkeyで保存されます。

```txt:redisコンテナ内
$ redis-cli
127.0.0.1:6379> get laravel_database_example
"test"
```

redisに値が保存されていることが確認できました。
redisで保存した値を取得することも可能です。

```txt:redisコンテナ内
$ redis-cli
127.0.0.1:6379> set laravel_database_example2 aaaaaaaaaaaaaa
OK
```

```txt:php-fpmコンテナ内
$ pwd
/project
$ php artisan tinker
> use Illuminate\Support\Facades\Redis;
> Redis::get("example2");
= "aaaaaaaaaaaaaa"
```

また、php-fpmコンテナ内からredisにログインできるように`Dockerfile`を調整します。

```txt:docker/php/Dockerfile
RUN apt-get install -y redis-tools
```

再ビルドして反映すると、php-fpmコンテナでredis-cliが使用できるようになります。
ホストを`redis`にすることで、redisコンテナのredisサーバーに接続することができます。

```txt:php-fpmコンテナ内
$ redis-cli -h redis
redis:6379> 
```