Docker + Traefik + mkcert を使ったローカル開発環境構築

安定して動くローカル開発環境構築は開発者の永遠のテーマですが、今回、Docker + Traefik + mkcert を使うことで、ブラウザで開いたときに証明書エラーが出ないローカル開発環境を構築したので、まとめてみます。

全体の設定

mkcert で証明書を作成する

mkcert はローカル開発環境で使える SSL 証明書を作成するツールで、Windows、macOS、Linux で利用することができます。今回は Windows 版を前提に説明します。管理者でターミナルを開いて、以下のように入力します。

winget install mkcert # mkcert のインストール
mkcert -install # ローカル CA の登録と信頼されたルート証明機関への登録
mkcert "*.dev.test" # dev.test のワイルドカード証明書の作成

ワイルドカード証明書のドメインを *.dev.test にしたのには理由があって、*.test だと、Edge や Chrome で証明書エラー(NET::ERR_CERT_COMMON_NAME_INVALID)が出るためです。これは mkcert の Github issue でも報告されていて、ブラウザ側の実装なのでどうにもならないです。

もう一つ個人的にハマったポイントとして、*.dev.test のワイルドカード証明書が有効になるのは、example.dev.test や app.dev.test のように1レベルのサブドメインのみということです。なので、後で出てくる Vite 用の別ドメインも vite.example.dev.test としてしまうとワイルドカード証明書が使えないので、vite-examle.dev.test のようにする必要があります。これはワイルドカード証明書作成時に

Created a new certificate valid for the following names 📜
 - "*.dev.test"

Reminder: X.509 wildcards only go one level deep, so this won't match a.b.dev.test

のように書かれていたのですが、うっかり見落としていました。

上記のコマンドを実行すると、_wildcard.dev.test.pem_wildcard.dev.test-key.pem の2つのファイルができあがるので、これを Traefik 用のフォルダに移動します。

Traefik の設定

Traefik はオープンソースのリバースプロキシおよびロードバランサで、Docker と連携させることで複数のサイトを SSL 化させることができます。

Traefik 自体も Docker で提供されているので、以下のような compose.yaml を作成します。

services:
  traefik:
    image: "traefik:3"
    container_name: "traefik"
    command:
      - "--providers.docker=true"
      - "--providers.docker.exposedByDefault=false" # `traefik.enable=true`を指定したコンテナのみ対象にする。
      - "--providers.docker.network=webdev" # 対象とするネットワークを明示的に指定する。
      - "--providers.file.filename=/etc/traefik/tls.yaml" # SSL 証明書の設定
      # `web`エントリポイントの設定をする。
      - "--entryPoints.web.address=:80"
      - "--entryPoints.web.http.redirections.entryPoint.permanent=true"
      - "--entryPoints.web.http.redirections.entryPoint.scheme=https"
      - "--entryPoints.web.http.redirections.entryPoint.to=websecure"
      # `websecure`エントリポイントの設定をする。
      - "--entryPoints.websecure.address=:443"
      - "--entryPoints.websecure.http.tls=true"
    networks: ["webdev"]
    ports: ["80:80", "443:443"]
    restart: "always"
    volumes:
      - "/var/run/docker.sock:/var/run/docker.sock"
      - ".:/etc/traefik:ro"

networks:
  webdev:
    name: webdev
    driver: bridge

また、SSL 証明書の設定のための tls.yaml も作成します。

tls:
  certificates:
    - certFile: /etc/traefik/_wildcard.dev.test.pem
      keyFile: /etc/traefik/_wildcard.dev.test-key.pem

これで全体の設定は完了です。docker compose up -d --build を実行してコンテナを起動します。

個別サイトの設定

次に個別のサイトの設定を行います。PHP + MariaDB + Node.js が動く環境であれば、こんな感じです。こちらも設定が終わったら、docker compose up -d --build を実行してコンテナを起動します。

name: example

services:
  app:
    image: thecodingmachine/php:8.3-v5-apache-node24
    ports:
      - '80'
    environment:
      PHP_EXTENSIONS: bcmath ctype curl dom fileinfo gd imagick mbstring mysqlnd pdo_mysql tokenizer xdebug xml zip
      PHP_INI_MEMORY_LIMIT: 512M
      PHP_INI_DATE__TIMEZONE: Asia/Tokyo
      PHP_INI_MBSTRING__LANGUAGE: Japanese
      APACHE_DOCUMENT_ROOT: /var/www/html/public
      VIRTUAL_HOST: 'example.dev.test'
      TZ: Asia/Tokyo
      LANG: C.UTF-8
      STARTUP_COMMAND_1: sudo apt update; sudo apt install -y jpegoptim optipng pngquant gifsicle mariadb-client
    volumes:
      - '..:/var/www/html'
    labels:
      traefik.enable: true
      traefik.docker.network: webdev

      # Apache
      traefik.http.routers.example.entrypoints: websecure
      traefik.http.routers.example.rule: Host(`example.dev.test`)
      traefik.http.routers.example.service: example
      traefik.http.services.example.loadbalancer.server.port: 80
    networks: ["webdev"]

  mariadb:
    image: 'mariadb:10'
    networks: ["webdev"]
    ports:
      - '${FORWARD_DB_PORT:-3306}:3306'
    environment:
      TZ: Asia/Tokyo
      MARIADB_ROOT_PASSWORD: '${DB_PASSWORD:-password}'
      MARIADB_DATABASE: '${DB_DATABASE:-mariadb}'
      MARIADB_USER: '${DB_USERNAME:-mariadb}'
      MARIADB_PASSWORD: '${DB_PASSWORD:-password}'
      MARIADB_ALLOW_EMPTY_PASSWORD: 'yes'
    volumes:
      - 'mariadb-data:/var/lib/mysql'
    healthcheck:
        test: ["CMD", "healthcheck.sh", "--connect", "--innodb_initialized"]
        retries: 3
        timeout: 5s

networks:
  webdev:
    external: true
    name: "webdev"

volumes:
  mariadb-data:
    driver: local

ドメインの設定は C:\windows\System32\drivers\etc\hosts ファイルを直接編集するか、PowerToys のホストファイルエディターで編集します。

  127.0.0.1    example.dev.test

これで、https://example.dev.test/ をブラウザで開くと、証明書の警告が出ないページが表示されると思います。

Vite の HMR 用の設定を追加する

Laravel で開発する場合、コードを修正するたびに npm run build でアセットをビルドするのは面倒なので、npm run dev で Vite の HMR(Hot Module Replacement) を有効にして開発したいところ。ただ、そのままだと、https://example.dev.test/ は HTTPS で通信しているのに、HMR で生成されるコンテンツは HTTP で配信されるため、混在コンテンツとして扱われ、CSS などが読み込まれません。

そこで追加の設定を行います。まず、compose.yaml を以下のように書き換えます。

name: example

services:
  app:
    image: thecodingmachine/php:8.3-v5-apache-node24
    ports:
      - '80'
    environment:
      PHP_EXTENSIONS: bcmath ctype curl dom fileinfo gd imagick mbstring mysqlnd pdo_mysql tokenizer xdebug xml zip
      PHP_INI_MEMORY_LIMIT: 512M
      PHP_INI_DATE__TIMEZONE: Asia/Tokyo
      PHP_INI_MBSTRING__LANGUAGE: Japanese
      APACHE_DOCUMENT_ROOT: /var/www/html/public
      VIRTUAL_HOST: 'example.dev.test'
      TZ: Asia/Tokyo
      LANG: C.UTF-8
      STARTUP_COMMAND_1: sudo apt update; sudo apt install -y jpegoptim optipng pngquant gifsicle mariadb-client
    volumes:
      - '..:/var/www/html'
    labels:
      traefik.enable: true
      traefik.docker.network: webdev

      # Apache
      traefik.http.routers.example.entrypoints: websecure
      traefik.http.routers.example.rule: Host(`example.dev.test`)
      traefik.http.routers.example.service: example
      traefik.http.services.example.loadbalancer.server.port: 80

      # Vite
      traefik.http.routers.vite-example.entrypoints: websecure
      traefik.http.routers.vite-example.rule: Host(`vite-example.dev.test`)
      traefik.http.routers.vite-example.service: vite-example
      traefik.http.services.vite-example.loadbalancer.server.port: 5173
    networks: ["webdev"]

  mariadb:
    image: 'mariadb:10'
    networks: ["webdev"]
    ports:
      - '${FORWARD_DB_PORT:-3306}:3306'
    environment:
      TZ: Asia/Tokyo
      MARIADB_ROOT_PASSWORD: '${DB_PASSWORD:-password}'
      MARIADB_DATABASE: '${DB_DATABASE:-mariadb}'
      MARIADB_USER: '${DB_USERNAME:-mariadb}'
      MARIADB_PASSWORD: '${DB_PASSWORD:-password}'
      MARIADB_ALLOW_EMPTY_PASSWORD: 'yes'
    volumes:
      - 'mariadb-data:/var/lib/mysql'
    healthcheck:
        test: ["CMD", "healthcheck.sh", "--connect", "--innodb_initialized"]
        retries: 3
        timeout: 5s

networks:
  webdev:
    external: true
    name: "webdev"

volumes:
  mariadb-data:
    driver: local

さらに、Laravel の vite.config.js を以下のように書き換えます。

import { defineConfig, loadEnv } from 'vite';
import laravel from 'laravel-vite-plugin';
import { bunny } from 'laravel-vite-plugin/fonts';
import tailwindcss from '@tailwindcss/vite';

// ============================================================
// ✏️ プロジェクトごとにここだけ変更する
// ============================================================
const PROJECT = {
    input: ['resources/css/app.css', 'resources/js/app.js'],
    port: 5173,
    strictPort: true, // ポートが塞がっていたら起動失敗させる
    fonts: [
        bunny('Inter', { weights: [400, 500, 600] }),
    ],
};
// ============================================================

function buildHmrConfig(hmrHost, port) {
    return {
        protocol: hmrHost ? 'wss' : 'ws',
        host: hmrHost || 'localhost',
        clientPort: hmrHost ? 443 : port,
    };
}

function buildServerConfig(env, { port, strictPort }) {
    return {
        host: '0.0.0.0',
        port,
        cors: true,
        strictPort,
        hmr: buildHmrConfig(env.VITE_HMR_HOST, port),
        watch: {
            ignored: ['**/storage/framework/views/**'],
        },
    };
}

export default defineConfig(({ mode }) => {
    const env = loadEnv(mode, process.cwd(), '');

    return {
        plugins: [
            laravel({
                input: PROJECT.input,
                refresh: true,
                fonts: PROJECT.fonts,
            }),
            tailwindcss(),
        ],
        server: buildServerConfig(env, PROJECT),
    };
});

この vite.config.js では、環境変数 VITE_HMR_HOST の有無で設定を切り替えているので、.env に

VITE_HMR_HOST=vite-example.dev.test

を追加します。最後に、hosts ファイルに

  127.0.0.1    vite-example.dev.test

を追加すれば準備完了です。

あとは、サイトを追加するたびに、hosts にドメインの設定を追加し、それに合わせて compose.yaml(と vite.config.js)を書き換えていけば OK です。準備は多少面倒ですが、HTTPS が有効な開発環境を好きなだけ増やせるのでおすすめです。

この記事を書いた人
グッドネイバー

“ Webに悩むお客さまの「よき隣人」でありたい ” をモットーに、Web システム開発(主に Laravel)、IT 統制構築支援の仕事をしています。お仕事のご依頼・ご相談はこちらからお気軽にどうぞ。