🧰

VS Code Remote Containers用PHP開発環境を構築する

2021/10/30に公開

みなさん、VS Code Remote Containersは使っていますか?
プロジェクト毎に開発環境を構築・共有できて、とても便利ですよね。
コンテナの起動・停止がVS Codeと連動するのでとても良い開発体験かと思います。

また、GitHub Codespacesというクラウドの開発環境でも同じ設定で動かすことができるので、さまざまなシーンでの活用が期待できます。

今回、PHPの開発環境を構築してみたので共有したいと思います。

設定

ディレクトリ構成

.devcontainer 配下に設定をまとめてあります。

.
└── .devcontainer
    ├── .env
    ├── devcontainer.json
    ├── docker
    │   ├── mysql
    │   │   └── initdb.d
    │   │       └── create_database.sh
    │   ├── nginx
    │   │   ├── Dockerfile
    │   │   └── config
    │   │       └── default.conf
    │   ├── php
    │   │   ├── Dockerfile
    │   │   └── config
    │   │       ├── php.ini
    │   │       └── xdebug.ini
    │   ├── postgres
    │   │   └── initdb.d
    │   │       └── create_database.sh
    │   └── workspace
    │       ├── Dockerfile
    │       └── config
    │           └── php.ini
    └── docker-compose.yml

devcontainer.json

VS Codeのコンテナ開発の設定を行います。

.devcontainer/devcontainer.json
{
    "name": "PHP Development",
    "dockerComposeFile": [
        "docker-compose.yml"
    ],
    "service": "workspace",
    "workspaceFolder": "/var/www",
    "remoteUser": "vscode",
    "settings": {
        "php.validate.enable": false,
        "php.suggest.basic": false,
        "[php]": {
            "editor.formatOnSave": true,
            "editor.defaultFormatter": "bmewburn.vscode-intelephense-client"
        },
        "search.exclude": {
            "**/node_modules": true,
            "**/bower_components": true,
            "**/*.code-search": true,
            "**/vendor/*/**": true
        }
    },
    "extensions": [
        "mikestead.dotenv",
        "EditorConfig.EditorConfig",
        "mhutchie.git-graph",
        "eamodio.gitlens",
        "xdebug.php-debug",
        "neilbrayfield.php-docblocker",
        "bmewburn.vscode-intelephense-client",
        "recca0120.vscode-phpunit"
    ]
}

serviceに、接続するサービス名を指定します。
settingsに、接続コンテナの VS Code の設定を行います。
extensionsに、接続コンテナにインストールする拡張機能を指定します。

docker-compose.yml

.devcontainer/docker-compose.yml
services:
    workspace:
        build:
            context: ./docker/workspace
            args:
                USERNAME: ${USERNAME-vscode}
                USER_UID: ${USER_UID-1000}
                USER_GID: ${USER_GID-1000}
                TIME_ZONE: ${TIME_ZONE-UTC}
                LOCALE: ${LOCALE-C}
        tty: true
        volumes:
            - ../:/var/www
            - ./docker/workspace/config/php.ini:/usr/local/etc/php/conf.d/99-php.ini
        working_dir: /var/www

    nginx:
        build: ./docker/nginx
        ports:
            - "${IP_ADDRESS_SETTING}80:80"
            - "${IP_ADDRESS_SETTING}443:443"
        volumes:
            - ./docker/nginx/config:/etc/nginx/conf.d
            - ../.docker/nginx/log:/var/log/nginx
            - ../:/var/www
        environment:
            TZ: ${TIME_ZONE-UTC}

    php:
        build:
            context: ./docker/php
            args:
                USER_UID: ${USER_UID-1000}
                USER_GID: ${USER_GID-1000}
        volumes:
            - ./docker/php/config/php.ini:/usr/local/etc/php/php.ini
            - ./docker/php/config/xdebug.ini:/usr/local/etc/php/conf.d/xdebug.ini
            - ../:/var/www
        working_dir: /var/www

    mysql:
        image: mysql/mysql-server:8.0
        ports:
            - "${IP_ADDRESS_SETTING}3306:3306"
        volumes:
            - mysql:/var/lib/mysql
            - ./docker/mysql/initdb.d:/docker-entrypoint-initdb.d
        environment:
            MYSQL_ROOT_PASSWORD: ${DB_PASSWORD-docker}
            MYSQL_USER: ${DB_USERNAME-docker}
            MYSQL_PASSWORD: ${DB_PASSWORD-docker}
            MYSQL_DATABASE: ${DB_DATABASE-docker}
            TZ: ${TIME_ZONE-UTC}

    postgres:
        image: postgres:15.0-bullseye
        ports:
            - "${IP_ADDRESS_SETTING}5432:5432"
        volumes:
            - postgres:/var/lib/postgresql/data
            - ./docker/postgres/initdb.d:/docker-entrypoint-initdb.d
        environment:
            POSTGRES_USER: ${DB_USERNAME-docker}
            POSTGRES_PASSWORD: ${DB_PASSWORD-docker}
            POSTGRES_DB: ${DB_DATABASE-docker}
            TZ: ${TIME_ZONE-UTC}

    redis:
        image: redis:7.0
        ports:
            - "${IP_ADDRESS_SETTING}6379:6379"
        volumes:
            - redis:/data

    redisinsight:
        image: redislabs/redisinsight:latest
        ports:
            - "${IP_ADDRESS_SETTING}8001:8001"

    mailhog:
        image: mailhog/mailhog
        ports:
            - ${IP_ADDRESS_SETTING}1025:1025
            - ${IP_ADDRESS_SETTING}8025:8025

    selenium:
        image: 'selenium/standalone-chrome'
        volumes:
            - '/dev/shm:/dev/shm'

volumes:
    mysql:
        driver: local
    postgres:
        driver: local
    redis:
        driver: local

設定しているコンテナの種類です。

  • PHP(CLI)
  • nginx
  • PHP(FPM)
  • MySQL
  • PostgreSQL
  • Redis
  • Redisinsight
  • Mailhog
  • Selenium

workspaceがPHP(CLI)になり、このコンテナ内で作業を行います。
純粋にPHPだけの開発であれば、workspaceのみあれば大丈夫です。

.env

.envにはdocker-compose.ymlの環境変数を設定します。

.devcontainer/.env
TIME_ZONE=Asia/Tokyo
LOCALE=ja_JP.UTF-8

DB_DATABASE=docker
DB_USERNAME=docker
DB_PASSWORD=docker

# Local Loopback Address(127.0.0.0/8):
IP_ADDRESS_SETTING=127.127.127.127:

IP_ADDRESS_SETTING

Dockerではポート毎に公開するIPアドレスを設定できます。
IPアドレスを設定することにより、複数のプロジェクトでのポートの重複を気にしなくて済むようになります。
また、設定したIPアドレスでhostsを設定すると、より便利に開発できます。

127.127.127.127 php-develop.test

IP_ADDRESS_SETTINGに値を設定しない場合は、localhostでの接続になります。
IPアドレスはローカルループバックアドレスを使います。
他のIPアドレスはホストOSから到達できないと思いますが、ローカルループバックアドレスであれば自分自身(Dockerホスト)をさすので接続可能です。

docker

dockerディレクトリにDockerfileや各種コンテナアプリケーションの設定ファイルを設置しています。

workspace

.devcontainer/docker/workspace/Dockerfile
FROM php:8.1-cli-bullseye

ARG USERNAME=vscode
ARG USER_UID=1000
ARG USER_GID=${USER_UID}

ARG LOCALE=en_US.UTF-8
ARG TIME_ZONE=UTC

ENV PKG="bash-completion curl dnsutils git imagemagick jq locales mariadb-client postgresql-client rsync sqlite3 tree unzip vim wget zip"
ENV PKG_LIB="libc-client-dev libfreetype6-dev libjpeg62-turbo-dev libkrb5-dev libmagickwand-dev libonig-dev libpng-dev libpq-dev libsqlite3-dev libxslt-dev libzip-dev"
ENV COMPOSER_ALLOW_SUPERUSER 1
ENV DEBIAN_FRONTEND noninteractive
ENV LANG=${LOCALE}
ENV TZ=${TIME_ZONE}

COPY --from=composer:2.4.3 /usr/bin/composer /usr/bin/composer

RUN apt-get update \
    && apt-get install -y $PKG $PKG_LIB \
    && pecl install imagick redis-5.3.4 xdebug-3.1.1 \
    && docker-php-ext-configure imap --with-kerberos --with-imap-ssl \
    && docker-php-ext-configure gd --with-freetype --with-jpeg \
    && docker-php-ext-enable imagick redis xdebug \
    && docker-php-ext-install bcmath exif gd imap intl pdo_mysql pdo_pgsql pdo_sqlite xml zip \
    #
    # nodejs
    && curl -sL https://deb.nodesource.com/setup_18.x | bash - \
    && apt-get install -y nodejs \
    && npm install -g yarn \
    #
    # locale
    && sed -i -E "s/# (${LOCALE})/\1/" /etc/locale.gen \
    && locale-gen ${LOCALE} \
    && dpkg-reconfigure locales \
    && update-locale LANG=${LOCALE} \
    #
    # timezone
    && ln -snf /usr/share/zoneinfo/${TIME_ZONE} /etc/localtime && echo ${TIME_ZONE} > /etc/timezone \
    #
    # user
    && groupadd --gid ${USER_GID} ${USERNAME} \
    && useradd -s /bin/bash --uid ${USER_UID} --gid ${USER_GID} -m ${USERNAME} \
    && apt-get install -y sudo \
    && echo ${USERNAME} ALL=\(root\) NOPASSWD:ALL > /etc/sudoers.d/${USERNAME} \
    && chmod 0440 /etc/sudoers.d/${USERNAME}

作業を行うコンテナです。
PHP(CLI)のphp:8.1-cli-bullseyeをベースに設定しています。

COPY --fromで別のイメージからコピーしています。

作業はrootで行わず、別ユーザで動かすのでユーザの設定を行っています。

.devcontainer/docker/workspace/config/php.ini
[xdebug]
xdebug.mode=debug,develop,coverage
xdebug.start_with_request=yes
xdebug.client_host=localhost
xdebug.log_level=0

PHP(CLI)の設定を行います。

php

.devcontainer/docker/php/Dockerfile
FROM php:8.1-fpm-bullseye

ARG USER_UID=1000
ARG USER_GID=${USER_UID}

RUN apt-get update \
    && apt-get install -y libc-client-dev libfreetype6-dev libjpeg62-turbo-dev libkrb5-dev libmagickwand-dev libonig-dev libpng-dev libpq-dev libsqlite3-dev libxslt-dev libzip-dev sqlite3 zip \
    && pecl install imagick redis-5.3.4 xdebug-3.1.1 \
    && docker-php-ext-configure imap --with-kerberos --with-imap-ssl \
    && docker-php-ext-configure gd --with-freetype --with-jpeg \
    && docker-php-ext-enable imagick redis xdebug \
    && docker-php-ext-install bcmath exif gd imap intl pdo_mysql pdo_pgsql pdo_sqlite xml zip \
    #
    # user
    && groupmod -o -g ${USER_GID} www-data \
    && usermod -o -u ${USER_UID} -g www-data www-data

PHP(FPM)のphp:8.1-fpm-bullseyeをベースに設定しています。

PHP(CLI)とPHP(FPM)のユーザが異なるとパーミッションの設定が面倒なので、UID/GIDを合わせます。

.devcontainer/docker/php/config/php.ini
post_max_size = 520M
upload_max_filesize = 512M
.devcontainer/docker/php/config/xdebug.ini
[xdebug]
xdebug.mode=debug,develop,coverage
xdebug.start_with_request=yes
xdebug.client_host=workspace
xdebug.log_level=0

PHP(FPM)の設定を行います。

nginx

.devcontainer/docker/nginx/Dockerfile
FROM nginx:1.23.2

RUN apt-get update && apt-get -y install openssl \
    && openssl req -newkey rsa:2048 -x509 -nodes -set_serial 1 -days 3650 \
       -subj "/C=JP/ST=Tokyo/L=Chiyoda-ku" \
       -keyout "/etc/ssl/private/server.key" -out "/etc/ssl/private/server.crt" \
    && chmod 400 /etc/ssl/private/server.*

httpsで接続できるようにしたいので、自己SSL証明書を作成します。

.devcontainer/docker/nginx/config/default.conf
server {
    listen 80;
    listen 443 ssl;

    client_max_body_size 520M;

    root /var/www/public;
    index index.php index.html;

    location / {
        try_files $uri $uri/ /index.php$is_args$args;
    }

    error_page 404 /index.php;

    location ~ \.php$ {
        fastcgi_pass php:9000;
        fastcgi_index index.php;
        fastcgi_split_path_info ^(.+\.php)(/.+)$;

        fastcgi_param SCRIPT_FILENAME $document_root$fastcgi_script_name;
        fastcgi_param PATH_INFO $fastcgi_path_info;
        include fastcgi_params;

        fastcgi_buffers 16 16k;
        fastcgi_buffer_size 32k;
        fastcgi_read_timeout 600;
    }

    location ~ /\.(?!well-known).* {
        deny all;
    }

    access_log /var/log/nginx/access.log;
    error_log /var/log/nginx/error.log;

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

    location ~ \.(css|gif|jpeg|jpg|js|png|svg) {
        access_log off;
        log_not_found off;
    }

    ssl_certificate /etc/ssl/private/server.crt;
    ssl_certificate_key /etc/ssl/private/server.key;
}

mysql

.devcontainer/docker/mysql/initdb.d/create_database.sh
#!/bin/sh

DATABASE="${MYSQL_DATABASE}_test"

CMD_MYSQL="mysql -u root -p${MYSQL_ROOT_PASSWORD}"

echo " Creating database '$DATABASE'"
$CMD_MYSQL -e "CREATE DATABASE ${DATABASE};"
$CMD_MYSQL -e "GRANT ALL PRIVILEGES ON ${DATABASE}.* to '${MYSQL_USER}'@'%';"

テスト用データベースを作成しています。

postgres

.devcontainer/docker/postgres/initdb.d/create_database.sh
#!/bin/bash
set -eu

DATABASE="${POSTGRES_DB}_test"

echo " Creating database '$DATABASE'"
psql -v ON_ERROR_STOP=1 --username "$POSTGRES_USER" <<-EOSQL
  CREATE DATABASE $DATABASE;
  GRANT ALL PRIVILEGES ON DATABASE $DATABASE TO $POSTGRES_USER;
EOSQL

テスト用データベースを作成しています。

使用例

プロジェクトのディレクトリを作成し、VS Codeを起動します。

mkdir php-develop
cd php-develop
code .

プロジェクトのディレクトリに.devcontainerを配置します。

VS Codeのコマンドパレット(F1)から [Remote-Containers: Reopen in Container] を実行します。

コンテナの開発環境の構築が始まります。

構築完了後、コンテナ内での開発が可能になります。

Xdebug

VS CodeにXdebugを受け入れる設定を行います。

.vscode/launch.json
{
    "version": "0.2.0",
    "configurations": [
        {
            "name": "Listen for Xdebug",
            "type": "php",
            "request": "launch",
            "port": 9003,
        }
   ]
}

Xdebugはバージョン3系をインストールしています。
デフォルトのport9003になります。

Xdebugは、Xdebug側からIDE(VS Code)に接続するのでホスト名を正しく設定する必要があります。

  • PHP(CLI)
    xdebug.client_host=localhost
    PHP(CLI)とVS Codeは同じコンテナになるのでlocalhostになります。

  • PHP(FPM)
    xdebug.client_host=workspace
    VS Codeはworkspaceで動いているので、workspaceを設定します。

Laravel

インストール

composer でインストールします。
composer create-project は空のディレクトリでないとインストールできないので、いったんtmpに退避させてから移動します。

composer create-project --prefer-dist "laravel/laravel:9.*" /tmp/laravel
mv -n /tmp/laravel/* /tmp/laravel/.[^\.]* .

正しくインストールされていればページが表示されると思います。

設定

config/app.php
  'timezone' => env('TZ', 'UTC'),

コンテナの環境変数が設定されている場合、このような設定も可能です。

MySQL

.env
DB_CONNECTION=mysql
DB_HOST=mysql
DB_PORT=3306
DB_DATABASE=docker
DB_USERNAME=docker
DB_PASSWORD=docker

PostgreSQL

.env
DB_CONNECTION=pgsql
DB_HOST=postgres
DB_PORT=5432
DB_DATABASE=docker
DB_USERNAME=docker
DB_PASSWORD=docker

Redis

.env
REDIS_HOST=redis
REDIS_PORT=6379
Redisinsight

Mailhog

.env
MAIL_DRIVER=smtp
MAIL_HOST=mailhog
MAIL_PORT=1025
MAIL_USERNAME=null
MAIL_PASSWORD=null
MAIL_ENCRYPTION=null

Laravel Dusk

Laravelを使ったブラウザテストです。
Seleniumを使った設定を行います。

パッケージをインストール

composer require --dev laravel/dusk

Laraveのインストール設定

php artisan dusk:install

WEBサーバ(nginx)のURL

.env
APP_URL=http://nginx

WebDriverのURL

.env
DUSK_DRIVER_URL=http://selenium:4444/wd/hub

テスト実行

php artisan dusk

その他

maxOSの場合、Dockerの動作が遅いという記事をよく見ます。
色々対策があるようなのでこちらを参考にしてみてください。
https://qiita.com/JO95/items/15d01956ebf0b83fbdc4
https://www.publickey1.jp/blog/22/virtiofsmacdocker_desktop_415webassemblycontainerd.html

また、WindowsのWSL2も同様の問題があるのですが、こちらはWSL2側をマウントさせれば問題ありません。
/mnt/c などWindows側をマウントさせると、すごく遅いです。


リポジトリ

https://github.com/horatjp/devcontainer-php

参考

https://code.visualstudio.com/docs/remote/containers
https://wand-ta.hatenablog.com/entry/2020/05/23/011001

GitHubで編集を提案

Discussion