ローカル開発でも HTTPS が必要になる場面があります。Service Worker、Secure Cookie、HTTP/2 の動作確認など、HTTP では再現できない挙動があります。
この記事では mkcert でローカル CA を構築し、カスタムドメイン(*.local)に証明書を発行し、Docker Compose 上の nginx をリバースプロキシとして立てる手順を説明します。
環境
- macOS Sonoma 14.x
- mkcert v1.4.4
- nginx 1.29.8(Docker:
nginx:alpine) - Docker 28.5.1
mkcert とは
HTTPS の証明書には「信頼された認証局(CA)が署名している」という条件があります。Let's Encrypt などの公的 CA は実在するドメインにしか証明書を発行しないため、app.local のようなローカル専用ドメインには使えません。
openssl で自己署名の証明書を作る方法もあります。ただしその場合、ブラウザーが安全でない接続として警告を出し続けます。毎回例外を追加する必要があるうえ、スクリーンリーダーや自動テストで例外を追加できないため実用になりません。
mkcert はこの問題を解決するツールで、ローカル開発専用の CA を作り、OS の信頼ストアに登録します。一度登録すれば、mkcert が発行した証明書はブラウザーに「正規の CA が署名した証明書」として認識されます。警告は出ません。
インストール
# macOS
brew install mkcert nss # nss は Firefox 対応に必要
# Linux(Debian/Ubuntu)
apt install libnss3-tools
curl -JLO "https://dl.filippo.io/mkcert/latest?for=linux/amd64"
chmod +x mkcert-v*-linux-amd64
mv mkcert-v*-linux-amd64 /usr/local/bin/mkcert
ローカル CA の作成と登録
mkcert -install
このコマンドの動作は次のとおりです。
-
~/.local/share/mkcert/(Linux)または~/Library/Application Support/mkcert/(macOS)に CA 鍵ペアを生成します - OS の証明書ストアに CA を追加します(macOS はキーチェーン、Linux は
~/.pki/nssdb) - Firefox の NSS データベースにも追加します(
nssがインストールされている場合)
CA ファイルの場所は mkcert -CAROOT で確認できます。この CA 鍵を リポジトリへコミットしてはいけません。
証明書の発行
開発で使うドメインを列挙して一度に発行できます。
mkdir -p certs
cd certs
mkcert app.local api.local admin.local
生成されるファイルは次のとおりです。
certs/
├── app.local+2.pem # 証明書
└── app.local+2-key.pem # 秘密鍵
ファイル名がドメイン数に応じて変わると扱いにくいため、固定名にリネームします。
mv app.local+2.pem local.pem
mv app.local+2-key.pem local-key.pem
証明書の有効期限は約 2 年(mkcert のデフォルト)です。
certs/ は .gitignore に追加しておきます。
# .gitignore
certs/*.pem
certs/*-key.pem
/etc/hosts の設定
app.local のようなドメインは DNS サーバーに登録されていないため、そのままでは名前解決できません。/etc/hosts に書くと、OS は DNS を引く前にこのファイルを参照し、指定した IP アドレスを返します。
向き先には 127.0.0.1(ループバックアドレス)を使います。ループバックアドレスとは「自分自身を指す特殊な IP アドレス」で、このアドレス宛のパケットはネットワークに出ず、OS 内部で折り返されます。ローカルで起動しているサーバーに繋ぐための慣習的なアドレスで、localhost もこのアドレスの別名として定義されています。
echo "127.0.0.1 app.local api.local admin.local" | sudo tee -a /etc/hosts
確認します。
ping -c 1 app.local
# PING app.local (127.0.0.1): 56 data bytes
nginx の設定
Docker Compose で nginx をリバースプロキシとして立てます。開発サーバーはホストで動かし、nginx はコンテナーから host.docker.internal 経由でアクセスします。
リクエストの流れは以下のとおりです。nginx がコンテナー内で HTTPS を終端し、ホスト側で動いている開発サーバーに HTTP で転送します。
ディレクトリ構成
project/
├── docker/
│ └── nginx/
│ └── nginx.conf
├── certs/
│ ├── local.pem
│ └── local-key.pem
└── docker-compose.dev.yml
nginx.conf
user nginx;
worker_processes auto;
error_log /var/log/nginx/error.log notice;
pid /var/run/nginx.pid;
events {
worker_connections 1024;
}
http {
# WebSocket の Upgrade ヘッダーを正規化
map $http_upgrade $connection_upgrade {
default upgrade;
'' close;
}
ssl_protocols TLSv1.2 TLSv1.3;
ssl_ciphers HIGH:!aNULL:!MD5;
ssl_prefer_server_ciphers on;
# 証明書は一か所に集約
ssl_certificate /etc/nginx/certs/local.pem;
ssl_certificate_key /etc/nginx/certs/local-key.pem;
proxy_http_version 1.1;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection $connection_upgrade;
# app.local → ホストの :3000 番ポート
server {
listen 8443 ssl;
server_name app.local;
location / {
proxy_pass http://host.docker.internal:3000;
proxy_buffering off;
}
}
# api.local → ホストの :8080 番ポート
server {
listen 8443 ssl;
server_name api.local;
location / {
proxy_pass http://host.docker.internal:8080;
proxy_buffering off;
}
}
}
docker-compose.dev.yml
services:
nginx:
image: nginx:alpine
restart: unless-stopped
ports:
- '127.0.0.1:8443:8443'
volumes:
- ./docker/nginx/nginx.conf:/etc/nginx/nginx.conf:ro
- ./certs:/etc/nginx/certs:ro
extra_hosts:
- 'host.docker.internal:host-gateway' # Linux での host 名解決
healthcheck:
test: ['CMD-SHELL', 'nginx -t 2>/dev/null && echo ok']
interval: 10s
timeout: 5s
retries: 5
ports を 127.0.0.1:8443:8443 と指定することで、外部ネットワークからのアクセスをブロックできます。
起動
docker compose -f docker-compose.dev.yml up -d
アプリを :3000 で起動した状態で https://app.local:8443 にアクセスすると、証明書警告なしに HTTPS で繋がります。
動作確認
curl でも確認できます。--resolve オプションで DNS をオーバーライドします。
# 証明書エラーが出ないことを確認(-sk を使わない)
curl -v --resolve app.local:8443:127.0.0.1 https://app.local:8443/
レスポンスの中に以下が含まれていれば証明書は正常です。
* SSL connection using TLSv1.3 / AEAD-AES256-GCM-SHA384
* Server certificate:
* subject: O=mkcert development certificate; ...
* issuer: O=mkcert development CA; ...
セットアップスクリプトの例
チームで同じ環境を再現するために、スクリプトにまとめておくと便利です。
#!/usr/bin/env bash
# scripts/setup-tls.sh
set -euo pipefail
CERTS_DIR="$(cd "$(dirname "$0")/.." && pwd)/certs"
DOMAINS=(app.local api.local admin.local)
if ! command -v mkcert &>/dev/null; then
echo "Error: mkcert が未インストールです。brew install mkcert nss"
exit 1
fi
# CA が未登録なら登録
CA_ROOT=$(mkcert -CAROOT)
if [[ ! -f "${CA_ROOT}/rootCA.pem" ]]; then
mkcert -install
fi
mkdir -p "${CERTS_DIR}"
cd "${CERTS_DIR}"
mkcert "${DOMAINS[@]}"
COUNT=$(( ${#DOMAINS[@]} - 1 ))
mv "${DOMAINS[0]}+${COUNT}.pem" local.pem
mv "${DOMAINS[0]}+${COUNT}-key.pem" local-key.pem
echo "✓ ${CERTS_DIR}/local.pem"
echo "✓ ${CERTS_DIR}/local-key.pem"
# /etc/hosts への追記
for domain in "${DOMAINS[@]}"; do
if ! grep -qE "^127\.[0-9.]+\s+${domain}" /etc/hosts; then
echo "127.0.0.1 ${domain}" | sudo tee -a /etc/hosts >/dev/null
echo "追加: ${domain}"
fi
done
まとめ
| 役割 | ツール |
|---|---|
| ローカル CA の作成・登録 | mkcert |
| ドメイン証明書の発行 | mkcert |
| DNS 解決 | /etc/hosts |
| HTTPS 終端 + リバースプロキシ | nginx(Docker) |
mkcert -install は開発マシン 1 台につき 1 回だけ実行すれば十分です。証明書は mkcert で発行するたびに新しいものが作られます。複数のプロジェクトで同じ CA を共有できるため、2 つ目以降のプロジェクトでは mkcert -install は不要です。