でも、そんなに強くアピールしてなくて、8月になってから急に連絡メールが強めなメッセージできていた。
それで解約申請したけど遅かった。
プライベートや仕事で気づいたことやノウハウなどを書き留めるブログです
Dockerでnginxを運用しているVPSで、Let's Encryptの証明書が複数作成され、 どの証明書をnginxが利用しているのか分かりにくくなっていました。 また、Certbotによる証明書更新も自動化されておらず、証明書の期限切れが発生していました。
そこで今回は、複数のドメイン・サブドメインを 1つのLet's Encrypt証明書に統合し、 Docker版Certbotとsystemd timerを使って自動更新する構成に整理します。
この記事では例として、以下の4ドメインを利用します。
example.com
www1.example.com
www2.example.com
www3.example.com
最終的な構成は次のようになります。
Let's Encrypt
↑
│ HTTP-01認証
│
Docker nginx
│
├─ /.well-known/acme-challenge/
│
↓
/srv/web/certbot/www
Docker Certbot
│
├─ 証明書管理名:example.com
│
└─ 4ドメインを1証明書で管理
│
↓
/srv/web/certbot/conf
systemd timer
│
↓
docker run certbot renew
│
↓
更新成功時のみ nginx reload
ポイントは、Ubuntu本体にCertbotを追加インストールせず、 公式のDocker版Certbotに統一することです。 Certbotを二重管理しないため、後から見ても構成が分かりやすくなります。
nginxとCertbotはDockerで動かし、証明書データだけをVPS上の永続ディレクトリに保存します。
/srv/web/certbot/
├── conf/
│ ├── live/
│ ├── archive/
│ └── renewal/
│
└── www/
└── .well-known/
└── acme-challenge/
役割は次の通りです。
/srv/web/certbot/conf:証明書、秘密鍵、Certbotの更新設定/srv/web/certbot/www:Let's EncryptのHTTP-01認証で使用nginxコンテナとCertbotコンテナから同じディレクトリを見ることで、 nginxを停止せずに証明書を更新できます。
Let's Encryptでは、1枚の証明書に複数のDNS名を含めることができます。
今回は次の4ドメインを1枚にします。
example.com
www1.example.com
www2.example.com
www3.example.com
Certbot上の管理名も、分かりやすく
example.com に統一します。
最終的には次のようになります。
Certificate Name: example.com
Identifiers:
example.com
www1.example.com
www2.example.com
www3.example.com
サブドメインごとに証明書を作る必要がなければ、 このようにまとめた方が管理対象を減らせます。
nginxには、例えば次のようにマウントします。
- /srv/web/certbot/conf:/etc/letsencrypt
- /srv/web/certbot/www:/var/www/certbot
実際のマウント状態は次のコマンドで確認できます。
docker inspect nginx \
--format '{{range .Mounts}}{{println .Source " -> " .Destination}}{{end}}'
例えば次のようになっていればOKです。
/srv/web/certbot/www -> /var/www/certbot
/srv/web/certbot/conf -> /etc/letsencrypt
Let's EncryptのHTTP-01認証では、 HTTPの80番ポートから特定のURLへアクセスできる必要があります。
server {
listen 80;
listen [::]:80;
server_name
example.com
www1.example.com
www2.example.com
www3.example.com;
location ^~ /.well-known/acme-challenge/ {
root /var/www/certbot;
try_files $uri =404;
}
location / {
return 301 https://$host$request_uri;
}
}
通常のHTTPアクセスはHTTPSへリダイレクトしますが、
/.well-known/acme-challenge/ だけはCertbot用にHTTPでアクセス可能にします。
証明書発行前に、テストファイルを作成します。
sudo mkdir -p /srv/web/certbot/www/.well-known/acme-challenge
echo 'certbot-test' | sudo tee \
/srv/web/certbot/www/.well-known/acme-challenge/test.txt
各ドメインから確認します。
curl http://example.com/.well-known/acme-challenge/test.txt
curl http://www1.example.com/.well-known/acme-challenge/test.txt
curl http://www2.example.com/.well-known/acme-challenge/test.txt
curl http://www3.example.com/.well-known/acme-challenge/test.txt
すべてで次の文字列が返れば正常です。
certbot-test
この確認を先に行っておくと、Certbot実行時の認証失敗をかなり減らせます。
公式Certbot Dockerイメージを使います。
docker run --rm \
-v /srv/web/certbot/conf:/etc/letsencrypt \
-v /srv/web/certbot/www:/var/www/certbot \
certbot/certbot certonly \
--webroot \
-w /var/www/certbot \
--cert-name example.com \
-d example.com \
-d www1.example.com \
-d www2.example.com \
-d www3.example.com
成功すると、証明書は次の場所に作成されます。
/etc/letsencrypt/live/example.com/fullchain.pem
/etc/letsencrypt/live/example.com/privkey.pem
実ファイルはVPS側の
/srv/web/certbot/conf
に保存されているため、Certbotコンテナを削除しても証明書は残ります。
証明書一覧は次のコマンドで確認できます。
docker run --rm \
-v /srv/web/certbot/conf:/etc/letsencrypt \
certbot/certbot certificates
4つのHTTPS serverで同じ証明書を指定します。
ssl_certificate /etc/letsencrypt/live/example.com/fullchain.pem;
ssl_certificate_key /etc/letsencrypt/live/example.com/privkey.pem;
nginx設定を確認します。
docker exec nginx nginx -t
正常ならreloadします。
docker exec nginx nginx -s reload
実際に読み込まれている証明書設定も確認できます。
docker exec nginx nginx -T 2>/dev/null \
| grep -E 'server_name|ssl_certificate'
HTTPS通信も確認します。
curl -I https://example.com
curl -I https://www1.example.com
curl -I https://www2.example.com
curl -I https://www3.example.com
HTTPステータスが200以外でも、アプリケーション側が400や302を返している場合があります。 重要なのはSSL証明書エラーが発生していないことです。
過去に複数の証明書を作っている場合、
live、
archive、
renewal
に古い設定が残っていることがあります。
まず一覧を確認します。
docker run --rm \
-v /srv/web/certbot/conf:/etc/letsencrypt \
certbot/certbot certificates
nginxが古い証明書を参照していないことを確認してから、 Certbot管理下の不要な証明書を削除します。
docker run --rm \
-v /srv/web/certbot/conf:/etc/letsencrypt \
certbot/certbot delete \
--cert-name 古い証明書名 \
--non-interactive
/etc/letsencrypt/live配下を単純にrmするのは避けます。
Certbotは
live、
archive、
renewal
を連動して管理しているためです。
ただし、過去の作業で
live が消えているのに
archive と
renewal/*.conf
だけが残った壊れた設定では、手動整理が必要になる場合があります。
作業前にはバックアップを取っておくと安全です。
sudo tar -czf /root/letsencrypt-backup.tar.gz \
/srv/web/certbot/conf
自動化する前に、実際には証明書を更新しない
--dry-run を実行します。
docker run --rm \
-v /srv/web/certbot/conf:/etc/letsencrypt \
-v /srv/web/certbot/www:/var/www/certbot \
certbot/certbot renew \
--dry-run
正常なら次のようなメッセージが表示されます。
Congratulations, all simulated renewals succeeded
ここまで成功してから自動実行を設定するのが安全です。
Docker版Certbotは、コンテナ自身がcronのように常駐して定期実行する構成ではありません。
そこでUbuntu側のsystemd timerを使って、 定期的にDocker版Certbotを起動します。
まずserviceを作ります。
sudo tee /etc/systemd/system/certbot-docker-renew.service > /dev/null <<'EOF'
[Unit]
Description=Renew Let's Encrypt certificates with Docker Certbot
Requires=docker.service
After=docker.service network-online.target
Wants=network-online.target
[Service]
Type=oneshot
RuntimeDirectory=certbot-renew
ExecStartPre=/usr/bin/rm -f /run/certbot-renew/renewed
ExecStart=/usr/bin/docker run --rm \
-v /srv/web/certbot/conf:/etc/letsencrypt \
-v /srv/web/certbot/www:/var/www/certbot \
-v /run/certbot-renew:/run/certbot-renew \
certbot/certbot renew \
--deploy-hook "touch /run/certbot-renew/renewed"
ExecStartPost=/bin/sh -c 'if [ -f /run/certbot-renew/renewed ]; then echo "Certificate renewed - reloading nginx"; /usr/bin/docker exec nginx nginx -s reload; else echo "No certificate renewal - nginx reload not required"; fi'
EOF
このserviceでは、証明書が実際に更新された場合だけ
nginx -s reload
を実行します。
次にtimerを作ります。
sudo tee /etc/systemd/system/certbot-docker-renew.timer > /dev/null <<'EOF'
[Unit]
Description=Run Docker Certbot renewal twice daily
[Timer]
OnCalendar=*-*-* 03,15:00:00
RandomizedDelaySec=30m
Persistent=true
Unit=certbot-docker-renew.service
[Install]
WantedBy=timers.target
EOF
systemdへ設定を読み込ませます。
sudo systemctl daemon-reload
timerを有効化して、その場で開始します。
sudo systemctl enable --now certbot-docker-renew.timer
この例では1日2回、3時台と15時台にCertbotが更新チェックを行います。
毎回証明書を発行するわけではありません。
certbot renew
が更新期限を判断し、更新が必要な場合だけ証明書を更新します。
timerの状態を確認します。
systemctl status certbot-docker-renew.timer --no-pager
次回実行時刻はこちらです。
systemctl list-timers --all | grep certbot
有効化状態も確認できます。
systemctl is-enabled certbot-docker-renew.timer
systemctl is-active certbot-docker-renew.timer
正常なら次のようになります。
enabled
active
timerを待たずにserviceだけ手動実行することもできます。
sudo systemctl start certbot-docker-renew.service
実行ログを確認します。
sudo journalctl \
-u certbot-docker-renew.service \
-n 100 \
--no-pager
証明書を取得したばかりの場合は更新されないため、 次のような内容になるのが正常です。
Certificate not yet due for renewal
No certificate renewal - nginx reload not required
今回の構成で重要なのは、Certbotや証明書の管理方法をできるだけ増やさないことです。
最終的には次の状態を目指します。
証明書
└─ example.com
├─ example.com
├─ www1.example.com
├─ www2.example.com
└─ www3.example.com
Certbot
└─ Docker版のみ
認証方式
└─ webroot
定期実行
└─ systemd timer
Webサーバー
└─ Docker nginx
Ubuntuへ別のCertbotをインストールすると、 「OS版Certbot」と「Docker版Certbot」の2系統が存在することになります。
特別な理由がなければ、今回のようにDocker版Certbotだけに統一した方が、 数か月後に設定を見直したときにも理解しやすくなります。
また、証明書をサブドメインごとに分割すると、 renewal設定、証明書ファイル、期限確認対象も増えます。
同じサーバーで利用していて、同じタイミングで更新して問題ないドメインなら、 1枚の証明書へまとめることで保守対象を減らせます。
# 証明書一覧
docker run --rm \
-v /srv/web/certbot/conf:/etc/letsencrypt \
certbot/certbot certificates
# 更新シミュレーション
docker run --rm \
-v /srv/web/certbot/conf:/etc/letsencrypt \
-v /srv/web/certbot/www:/var/www/certbot \
certbot/certbot renew --dry-run
# nginxが使用している証明書
docker exec nginx nginx -T 2>/dev/null \
| grep -E 'server_name|ssl_certificate'
# timer確認
systemctl list-timers --all | grep certbot
# 更新ログ
sudo journalctl \
-u certbot-docker-renew.service \
-n 100 \
--no-pager
この構成にしておけば、通常の運用ではCertbotを手動で操作する必要はほとんどなく、 systemd timerが定期チェックし、必要なタイミングで証明書を更新できます。
Ubuntu VPS上でLaravelのWebアプリ、データベース、Docker管理画面、更新確認ツールを動かす構成を初心者向けに整理します。Laravelは通常のブラウザから利用でき、PortainerとWUDは管理者だけがブラウザで開ける構成です。
インターネット上のブラウザ
│ HTTP / HTTPS
▼
Ubuntu VPS
└─ Docker Engine
├─ nginxコンテナ ─────── 公開窓口
│ │ FastCGI
│ ▼
├─ PHP-FPMコンテナ ──── Laravelを実行
│ │
│ ▼
├─ PostgreSQLコンテナ ─ データを保存
├─ Portainerコンテナ ── Docker管理画面
└─ WUDコンテナ ─────── イメージ更新を確認
Docker Engineは1つだけです。nginx、PHP-FPM、PostgreSQL、Portainer、WUDは、同じDocker Engine上で動く別々のコンテナです。LaravelはPHP-FPMコンテナ内で実行されるアプリケーションです。
| 要素 | 役割 | 外部公開 |
|---|---|---|
| nginx | ブラウザからのアクセスを受け、PHP処理をPHP-FPMへ渡す | 80・443番 |
| PHP-FPM | PHPを実行し、Laravelの処理結果をnginxへ返す | 公開しない |
| Laravel | 画面表示、ログイン、検索、データ処理などWebアプリ本体 | nginx経由 |
| PostgreSQL | Laravelが使うデータベース | 公開しない |
| Portainer CE | DockerをGUIで確認・管理する | localhostのみ |
| WUD | Dockerイメージの更新を検出する | localhostのみ |
https://example.com
↓
VPSの443番
↓
nginx
↓
php:9000
↓
Laravel
↓
postgres:5432
同じDockerネットワークに接続したコンテナは、Composeのサービス名で通信できます。
php:9000postgres:5432LaravelのDB_HOSTにはpostgresを指定します。PHPコンテナ内のlocalhostはPHPコンテナ自身を意味するためです。
/srv/
├─ laravel-app/
│ ├─ compose.yml
│ ├─ .env
│ ├─ docker/
│ │ ├─ nginx/default.conf
│ │ └─ php/Dockerfile
│ └─ src/
│ ├─ artisan
│ ├─ app/
│ ├─ public/
│ └─ storage/
└─ docker-management/
└─ compose.yml
Laravel側と管理ツール側を別のComposeプロジェクトに分けます。それぞれでdocker compose up -dを実行すると、同じDocker Engine上へ追加されます。
/srv/laravel-app/compose.ymlの基本例です。
name: laravel-app
services:
nginx:
image: nginx:alpine
container_name: laravel-nginx
restart: unless-stopped
ports:
- "80:80"
volumes:
- ./src:/var/www/html:ro
- ./docker/nginx/default.conf:/etc/nginx/conf.d/default.conf:ro
depends_on:
- php
networks:
- app-network
php:
build:
context: ./docker/php
container_name: laravel-php
restart: unless-stopped
working_dir: /var/www/html
volumes:
- ./src:/var/www/html
environment:
APP_ENV: production
DB_CONNECTION: pgsql
DB_HOST: postgres
DB_PORT: 5432
DB_DATABASE: laravel
DB_USERNAME: laravel
DB_PASSWORD: ${DB_PASSWORD}
depends_on:
postgres:
condition: service_healthy
networks:
- app-network
postgres:
image: postgres:16-alpine
container_name: laravel-postgres
restart: unless-stopped
environment:
POSTGRES_DB: laravel
POSTGRES_USER: laravel
POSTGRES_PASSWORD: ${DB_PASSWORD}
volumes:
- postgres_data:/var/lib/postgresql/data
healthcheck:
test: ["CMD-SHELL", "pg_isready -U laravel -d laravel"]
interval: 10s
timeout: 5s
retries: 5
networks:
- app-network
volumes:
postgres_data:
networks:
app-network:
PostgreSQLにはportsを書いていないため、VPS外部から5432番へ直接接続できません。Composeと同じディレクトリの.envには、DB_PASSWORD=長いランダムなパスワードを設定します。
/srv/docker-management/compose.ymlの例です。
name: docker-management
services:
portainer:
image: portainer/portainer-ce:2.39.6
container_name: portainer
restart: unless-stopped
ports:
- "127.0.0.1:9443:9443"
volumes:
- /var/run/docker.sock:/var/run/docker.sock
- portainer_data:/data
wud:
image: getwud/wud:8.3.1
container_name: wud
restart: unless-stopped
ports:
- "127.0.0.1:3000:3000"
environment:
TZ: Asia/Tokyo
WUD_WATCHER_LOCAL_CRON: "0 6 * * *"
WUD_WATCHER_LOCAL_WATCHBYDEFAULT: "true"
WUD_WATCHER_LOCAL_WATCHEVENTS: "false"
volumes:
- /var/run/docker.sock:/var/run/docker.sock:ro
- wud_data:/store
volumes:
portainer_data:
wud_data:
PortainerとWUDはVPSの127.0.0.1だけに公開します。インターネットから直接は開けません。
/srv/laravel-app/docker/nginx/default.confの基本例です。
server {
listen 80;
server_name example.com;
root /var/www/html/public;
index index.php index.html;
location / {
try_files $uri $uri/ /index.php?$query_string;
}
location ~ \.php$ {
include fastcgi_params;
fastcgi_pass php:9000;
fastcgi_param SCRIPT_FILENAME $document_root$fastcgi_script_name;
}
location ~ /\.ht {
deny all;
}
}
fastcgi_pass php:9000;により、PHP処理をphpサービスへ渡します。
cd /srv/laravel-app
docker compose config
docker compose build
docker compose up -d
docker compose ps
cd /srv/docker-management
docker compose config
docker compose pull
docker compose up -d
docker compose ps
全体は次で確認できます。
docker ps
docker compose ls
WindowsのPowerShellからSSHトンネルを開きます。
ssh -L 9443:127.0.0.1:9443 -L 3000:127.0.0.1:3000 ユーザー名@VPSのIPアドレス
SSH接続を開いたまま、ブラウザで次を開きます。
https://取得したドメインhttps://localhost:9443http://localhost:3000Laravelは一般利用者向けに公開し、PortainerとWUDはSSH接続できる管理者だけが利用します。
| 対象 | 管理方法 | 更新方法 |
|---|---|---|
| Ubuntu | APT | セキュリティ更新は自動、通常更新は手動 |
| Docker Engine | APT | メンテナンス時間に手動 |
| nginx・PHP・PostgreSQL | WUDで検出 | Composeから手動 |
| Laravel | Gitなど | コード配備、Composer、マイグレーション |
| コンテナの状態 | Portainer | ログと稼働状態を確認 |
cd /srv/laravel-app
docker compose pull
docker compose build --pull php
docker compose up -d
docker compose ps
docker compose logs --tail 100
PostgreSQLデータはpostgres_dataボリューム、Portainer設定はportainer_data、WUD履歴はwud_dataに保存されます。
docker compose down -vはボリュームを削除するため、通常は使いません。docker ps
docker compose ls
docker system df
df -h /
cd /srv/laravel-app
docker compose logs --tail 100 nginx
docker compose logs --tail 100 php
docker compose logs --tail 100 postgres
docker exec laravel-nginx nginx -t
docker compose exec php php artisan about
Laravelはドメインから通常のブラウザで利用できます。PortainerとWUDはlocalhostだけに公開し、SSHトンネル経由でブラウザ表示します。これにより、Webアプリの公開とDocker管理画面の保護を両立できます。
Dockerでは、稼働中のコンテナを直接作り込むより、Composeファイル、Dockerfile、nginx設定、Laravelソース、環境変数を設計図として保ち、同じ構成を再現できる状態にすることが重要です。