CloudflareとConoHa VPSでドメインを繋いでHTTPSを有効にするまで【後編】
はじめに
前編では、Cloudflareを「フレキシブル」にしてHTTPSを一旦有効化しました。
後編は Let’s Encryptで本物の証明書を入れて「フル(厳密)」へ移行する 手順です。
この記事は、僕とAIのやり取りをまとめたものです!主は、Claude Codeと Codexを使用しています。
※ちゃんとソースを見るようにしてはいますが、一部誤った情報を記載してしまっている場合があります。その場合は、優しくコメントで教えてください♪
環境
- ドメイン管理: Cloudflare
- サーバー: ConoHa VPS
- Webサーバー: nginx
- Docker Compose運用
ゴール
- VPS側にLet’s Encrypt証明書を発行
- nginxに証明書を適用
- Cloudflareを フル(厳密) に変更
- 自動更新(cron)まで設定
先にざっくり流れ
- HTTPで証明書チェック用のURL(ACMEチャレンジ)を通す
- certbotで証明書を発行
- nginxに証明書を読み込ませる
- Cloudflareをフル(厳密)に切り替える
- 自動更新を仕込む
Step 1: HTTPでACMEチャレンジを通す
Let’s Encryptは「このドメイン、本当にあなたのサーバーのもの?」を確認します。
その確認に使うのが /.well-known/acme-challenge/ です。
この設定では以下を行っています。
-
listen 80→ HTTPで待ち受け(certbotの所有権確認はHTTPで行われる) -
location /.well-known/acme-challenge/→ certbotが確認用ファイルを置くパスを公開 -
root /var/www/certbot→ certbotコンテナのwebrootと同じパスを指定 -
return 301 https://...→ それ以外のリクエストはHTTPSにリダイレクト
server {
listen 80;
server_name example.com;
location /.well-known/acme-challenge/ {
root /var/www/certbot;
}
location / {
return 301 https://$host$request_uri;
}
}
root /var/www/certbot は、certbotコンテナで使うwebrootと同じパスにします。ここがズレると所有権確認に失敗します。
Step 2: certbotで証明書を発行する
Docker Composeで certbot サービスを使って発行します。
オプションの意味は以下のとおりです。
-
certonly→ 証明書の発行だけ行う(nginxの設定は変更しない) -
--webroot -w /var/www/certbot→ Step 1で設定したパスで所有権確認 -
--email→ 証明書の期限切れ通知を受け取るメールアドレス -
--agree-tos→ Let's Encryptの利用規約に同意 -
--no-eff-email→ EFFからのメール購読をスキップ -
-d example.com→ 証明書を発行するドメイン
cd ~/repos/your-infra-repo
docker compose run --rm certbot certonly \
--webroot -w /var/www/certbot \
--email you@example.com \
--agree-tos --no-eff-email \
-d example.com
成功すると以下が作成されます。
/etc/letsencrypt/live/example.com/fullchain.pem
/etc/letsencrypt/live/example.com/privkey.pem
このふたつが証明書本体です。次のステップでnginxに読み込ませます。
Step 3: nginxで証明書を読む
HTTPSで待ち受ける443側の設定に、Step 2で発行した証明書パスを入れます。
-
ssl_certificate/ssl_certificate_key→ Step 2で作成された証明書ファイルのパス -
ssl_protocols TLSv1.2 TLSv1.3→ 古いTLS(1.0, 1.1)を無効化 -
proxy_pass http://app:3000→ アプリコンテナ(Next.js等)に転送 -
proxy_set_header群 → アプリ側にクライアントのIP・プロトコル・WebSocket情報を渡す
server {
listen 443 ssl;
server_name example.com;
ssl_certificate /etc/letsencrypt/live/example.com/fullchain.pem;
ssl_certificate_key /etc/letsencrypt/live/example.com/privkey.pem;
ssl_protocols TLSv1.2 TLSv1.3;
ssl_prefer_server_ciphers off;
location / {
proxy_pass http://app:3000;
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 "upgrade";
}
}
反映:
docker compose up -d nginx
Step 4: CloudflareのSSLをフル(厳密)へ
前編ではフレキシブル(Cloudflare→VPS間がHTTP)にしていましたが、Step 2で証明書を取得したのでフル(厳密)に切り替えます。
フレキシブル:ブラウザ ←HTTPS→ Cloudflare ←HTTP→ VPS
フル(厳密):ブラウザ ←HTTPS→ Cloudflare ←HTTPS→ VPS(Step 2の証明書を使用)
Cloudflareダッシュボードで切り替えます。
SSL/TLS → 概要 → フル(厳密)
これで ブラウザ ↔ Cloudflare ↔ VPS の両区間がHTTPSになります。
Step 5: 動作確認
curl -I https://example.com
HTTP 200が返ればOKです。
実際に確認したログ(抜粋)
1つ目はCloudflareプロキシ経由のアクセスです。server: cloudflare が返ります。
$ curl -I https://example.com
HTTP/2 200
server: cloudflare
2つ目はお名前ドットコムのネームサーバー経由でVPSのIPが直接解決されるため、nginxが直接応答します。
$ curl -I https://example.com
HTTP/1.1 200 OK
Server: nginx
どちらも 200 が返っていれば、証明書が正しく機能しています。
Step 6: 証明書の自動更新(cron)
Let’s Encryptの証明書は現在90日有効です。ただし公式の発表で、今後 90日 → 64日 → 45日 と段階的に短縮される予定があります。
certbotの更新判定は「有効期限の残り1/3になったら更新する」という動きです(古いドキュメントに「30日前」と書いてあるものもありますが、Certbot 4.0.0以降はバージョン依存に変わっています)。
つまり cron は定期的に回しておくだけでOKです。「更新すべきか」の判断はcertbotが自動でやってくれます。
1. まずドライラン
cd ~/repos/your-infra-repo
docker compose run --rm certbot renew --dry-run --webroot -w /var/www/certbot
2. 本番スクリプト
scripts/renew-and-reload.sh を作成します。
#!/usr/bin/env bash
set -euo pipefail
# Run renewal once and reload nginx if renewal succeeded.
docker compose run --rm certbot renew --webroot -w /var/www/certbot --quiet
docker compose exec nginx nginx -s reload
echo "Renew check completed and nginx reloaded."
実行権限を付与します。
chmod +x ./scripts/renew-and-reload.sh
3. cron登録
0 */12 * * * は12時間ごとに実行するという意味です(1日2回)。
(crontab -l 2>/dev/null; echo '0 */12 * * * cd ~/repos/your-infra-repo && ./scripts/renew-and-reload.sh >> /var/log/cert-renew.log 2>&1') | crontab -
crontab -l
ログ確認:
tail -n 100 /var/log/cert-renew.log
遭遇したトラブル
1. ERR_SSL_VERSION_OR_CIPHER_MISMATCH
CloudflareがHTTPSでVPSに到達できていない時に出ます。ネームサーバー切り替え直後に、VPS側にまだ証明書が入っていない状態でも発生します。
原因の確認順序:
- nginxが443で待ち受けていない
- 証明書パスが間違っている
- 古いTLS設定
一時的に回避したい場合は、CloudflareのSSL/TLSを フレキシブル に変更してサイトを表示させてから、証明書の設定を進めるのが順番として安全です。
2. no peer certificate
openssl s_client で no peer certificate が出る場合、nginx -t が通っていてもコンテナが古い設定のまま動いていることが原因です。再起動ではなく、コンテナの再作成が必要です。
docker compose stop nginx
docker compose rm -f nginx
docker compose up -d nginx
その後、再度確認します。
openssl s_client -connect 127.0.0.1:443 -servername example.com </dev/null | head -n 20
3. Recv failure: Connection reset by peer
certbot実行中にACMEチャレンジへのアクセスが Connection reset by peer で失敗する場合、nginxが正常に起動できていません。
docker compose logs nginx --tail=100
以下のようなエラーが出ていたら、証明書ファイルがまだ存在しないのにSSL設定を読もうとして起動に失敗している状態です。
[emerg] cannot load certificate "/etc/letsencrypt/live/example.com/fullchain.pem":
No such file or directory
certbot実行前のnginx設定はSSLを参照しないHTTPのみの設定にしておく必要があります。Step 1のACMEチャレンジ用設定(listen 80 のみ)になっているか確認してください。
まとめ
- Let’s Encryptで証明書を発行
- nginxに証明書を適用
- Cloudflareを フル(厳密) に変更
- cronで自動更新
AIとの対話で詰まったポイントを一つずつ整理した結果、ここまで到達できました。
番外編:次にやること
今回の構成はこうなっています。
- ConoHa VPS 上で nginx + Next.js(サイトA)+ Next.js(サイトB) の3コンテナ運用
- サイトA:お名前ドットコムでドメイン取得、ネームサーバーもお名前ドットコムのまま
- サイトB:Cloudflareでドメイン取得、ネームサーバーもCloudflare
サイトAのドメインをCloudflareに移行してネームサーバーを切り替えれば、両サイトともCloudflareで統一管理できます。
次の記事では、お名前ドットコムで取得したドメインのネームサーバーをCloudflareに変えるまでを書きます!