VSCodeのdevcontainerでLaravel・Docker環境を構築する

# 概要

VSCodeのdevcontainerを使ってLaravel環境構築を解説する記事です。
使用ツールさえ用意すれば高速で環境構築でき、Dockerを使用しているのでチームでの統一された開発環境としても使用できます。
とりあえず動かしたい人はリポジトリリンクからどうぞ。


# リポジトリ

https://github.com/naoyayamamoto/laravel-docker-sample
テンプレートリポジトリとして用意しているので、雛形としても使用できます。

## 使用ツール

* [Docker](https://docs.docker.com/get-docker/)
* Docker-compose
* [Visual Studio Code](https://code.visualstudio.com/download)
* [Visual Studio Code Remote - Containers](https://marketplace.visualstudio.com/items?itemName=ms-vscode-remote.remote-containers)

以上のツールを使用します。
`Docker`・`Visual Studio Code`についてはインストールの詳細を省きます。
`Visual Studio Code Remote - Containers`についてはMicrosoft謹製のVSCodeプラグインです。リンクよりVSCodeにインストールできます。

## 使用方法

### リポジトリをVSCodeで開く

```bash
$ git clone https://github.com/naoyayamamoto/laravel-docker-sample
$ code laravel-docker-sample
# codeコマンドが入っていなければVSCodeでリポジトリを開いてください
```

### devcontanerを使用してVSCodeを開く

1. VSCodeに`Visual Studio Code Remote - Containers`が入っていれば左下にアイコンが追加されているので、それを押してダイアログを開くか、Show All Commands`cmd + shift + P`でダイアログを開く。
1. `Remote-Containers: Reopen in Container`を実行すると環境の準備が始まります。
1. 初回はDockerの準備を行うため時間がかかりますが、2回目以降は高速で立ち上がります。

### アクセス確認

`http://localhost:8000`へアクセスでLaravel初期ページが表示されるはずです。

![laravel-8-top.jpg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/74691/58e4c8b7-5acf-dca0-9a56-7dc2fd94242e.jpeg)


# 解説

devcontainerを開いた際に`.devcontainer/docker-compose.yml`を元にDocker環境が構築されます。
以下のコンテナを使用しており、それぞれ解説していきます。

* nginx:alpine (web)
* mysql:8 (db)
* VSCode用にカスタマイズしたphp:7-fpm (app)

### nginx:alpine (web)

* デフォルトイメージを使用
* フロントとして常に起動するので、`php artisan serve`なしでアクセス可能
* `.devcontainer/docker/nginx/default.conf`を読み込ませ、php-fpmへ処理を流している

### mysql:8 (db)

* デフォルトイメージを使用
* `.devcontainer/docker/mysql/my.cnf`を読み込ませている（日本語基本設定）
* 環境変数`MYSQL_ALLOW_EMPTY_PASSWORD: 'yes'` `MYSQL_DATABASE: laravel`を設定しているので、rootパスワードなし、初期テーブル`laravel`が作成される

### VSCode用にカスタマイズしたphp:7-fpm (app)

* devcontainerを開いた際の初期接続コンテナ
* `.devcontainer/docker/php/Dockerfile`を使用してビルド
* `composer`・`node`・`npm`・`yarn`が使用可能
* php-fpmをsockで実行するために`.devcontainer/docker/php/php-fpm.d/zzz-www.conf`を読み込ませている
* `DB_HOST: db`を設定しているので、.envでの設定よりこちらが優先されてdbコンテナに接続されます

# おまけ

## 本番環境用のDockerfile

本番を想定してビルドを行うDockerfileがリポジトリに追加しているので、カスタマイズの元として使用してください。
マルチステージビルドを使用して、jsのコンパイルとcomposerのインストールを先行して行い、最終イメージを極力小さくしています。
GithubActionsでビルドテストだけは通しています。

## kubernetesで使用する場合

参考までにkubernetesで使用する場合の設定です。
nginxとphpを1PODとして動かすことを想定しています。

<details><summary>参考deployment.yml</summary><div>  

```yml
---
apiVersion: v1
kind: ConfigMap
metadata:
  name: nginx-conf
data:
  default.conf: |
    access_log /dev/stdout main;
    error_log /dev/stderr warn;
    server {
        server_tokens off;
        sendfile on;
        tcp_nopush on;
        tcp_nodelay on;
        gzip on;
        gzip_http_version 1.0;
        gzip_disable "msie6";
        gzip_proxied any;
        gzip_min_length 1024;
        gzip_comp_level 6;
        gzip_types text/plain text/css application/json application/x-javascript text/xml application/xml application/xml+rss text/javascript application/javascript;
        open_file_cache max=100000 inactive=20s;
        open_file_cache_valid 30s;
        open_file_cache_min_uses 2;
        open_file_cache_errors on;

        listen 80;
        root /workspace/public;

        add_header X-Frame-Options "SAMEORIGIN";
        add_header X-XSS-Protection "1; mode=block";
        add_header X-Content-Type-Options "nosniff";

        index index.html index.htm 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 unix:/var/run/php-fpm/php-fpm.sock;
            fastcgi_index index.php;
            fastcgi_param SCRIPT_FILENAME $realpath_root$fastcgi_script_name;
            include fastcgi_params;
        }

        location ~ /\.(?!well-known).* {
            deny all;
        }
    }
---
kind: Service
apiVersion: v1
metadata:
  name: laravel-service
spec:
  selector:
    app: laravel
  ports:
    - protocol: TCP
      port: 80
      targetPort: 80
      name: laravel-http
---
kind: Deployment
apiVersion: apps/v1
metadata:
  name: laravel
  labels:
    app: laravel
spec:
  replicas: 1
  revisionHistoryLimit: 10
  selector:
    matchLabels:
      app: laravel
  template:
    metadata:
      labels:
        app: laravel
    spec:
      containers:
        - name: app
          image: myimage
          volumeMounts:
            - mountPath: /var/run/php-fpm
              name: php-fpm-socket
            - mountPath: /shared
              name: public-contents
          lifecycle:
            postStart:
              exec:
                command: ["/bin/sh", "-c", "cp -aT /workspace/public /shared"]
        - name: web
          image: nginx:alpine
          ports:
            - containerPort: 80
          volumeMounts:
            - mountPath: /var/run/php-fpm
              name: php-fpm-socket
            - mountPath: /etc/nginx/conf.d
              name: nginx-conf
            - mountPath: /workspace/public
              name: public-contents
      volumes:
        - name: php-fpm-socket
          emptyDir: {}
        - name: public-contents
          emptyDir: {}
        - name: nginx-conf
          configMap:
            name: nginx-conf
            items:
              - key: default.conf
                path: default.conf

```
</div></details>