SSD Nodes Learn Hosting plans →
ガイド Matt Connor著者 Matt Connor ・更新日 2026-08-26

Ubuntu 24.04でnginx用Certbotを導入する方法

Ubuntu 24.04でsudo apt install certbot python3-certbot-nginxを実行し、certbot --nginxでLet's Encrypt証明書を発行します。aptとsnapの違い、port 80が原因の更新タイムアウトも確認できます。

Certbot のインストール: apt または snap

Ubuntu 24.04 では、sudo apt install certbot python3-certbot-nginx により、実際に使用できる、Let's Encrypt が公開で信頼する証明書を発行する Certbot を導入できます。Certbot の上流ドキュメントでは snap が案内されています。違いは限定的です。snap は上流のリリースを追跡します。アーカイブパッケージは LTS に収録されたバージョンを使用し、セキュリティ修正を受け取ります。

どちらか一方を選択してください。Certbot が 2 つあると、同じ /etc/letsencrypt ツリーを対象とする更新タイマーも 2 つ動作します。忘れていた方が、後で問題を起こします。

apt を使用する場合:

sudo apt update
sudo apt install certbot python3-certbot-nginx

これにより、/usr/bin/certbot、nginx プラグイン、certbot.servicecertbot.timer の組み合わせ、および systemd 環境では何もしない /etc/cron.d/certbot エントリがインストールされます。

snap を使用する場合:

sudo apt remove certbot python3-certbot-nginx
sudo snap install core && sudo snap refresh core
sudo snap install --classic certbot
sudo ln -s /snap/bin/certbot /usr/bin/certbot

snap には独自のタイマー snap.certbot.renew.timer が含まれています。snap をインストールする前に、apt パッケージを削除してください。

その後の動作は、どちらのインストール方法でも同じです。Certbot 2.x はデフォルトで ECDSA(P-256)鍵を使用します。ECDSA に対応していないクライアントの場合のみ --key-type rsa を指定してください。すべての状態は /etc/letsencrypt 以下に保存されます。archive/ には実際の鍵ファイルと証明書ファイルが格納され、live/ は現在のファイルを指します。renewal/ には証明書ごとに 1 つの設定ファイルが保存され、accounts/ には ACME アカウント鍵が保存されます。

HTTP-01 の実際の動作と、port 80 が必須である理由

HTTP-01 チャレンジはコールバックです。example.com を対象とする証明書を Let's Encrypt に要求すると、Let's Encrypt は公開 DNS で名前を解決し、見つかったアドレスの port 80 に接続して http://example.com/.well-known/acme-challenge/<token> を要求します。サーバーは、Certbot がディスクに書き込んだ正確なトークンの内容を返します。仕組みはこれだけです。ここから 3 つの帰結が生じ、多くの発行失敗の原因になります。

  • port 80 は、ノート PC からだけでなく、パブリックインターネットから到達可能でなければなりません。 ufw ルール、クラウドプロバイダーのセキュリティグループ、または VPS コンソールのファイアウォールで 443 だけを開けていると、発行と、その後の更新がすべて失敗します。
  • DNS は事前にこのサーバーを指していなければなりません。 検証サーバーは外部から独自に名前を解決します。ユーザー側の /etc/hosts エントリやブラウザーキャッシュは影響しません。
  • AAAA レコードを公開している場合、IPv6 が先に試行されます。 IPv6 接続が完全に失敗すれば、Let's Encrypt は IPv4 で再試行します。ただし、接続を受け付けて別の内容を返すホストを指す古い AAAA レコードがあると、検証は明確なエラーで失敗します。

リダイレクトは使用できます。検証は HTTP リダイレクトに従って HTTPS へ移動し、移動先の証明書が未設定、期限切れ、または自己署名であることは問題にしません。ただし、port 80 以外から検証を開始することはありません。Certbot には TLS-ALPN-01 の実装がないため、「443 だけを使う」という回避策は利用できません。

認証方式の選択: --nginx、--webroot、--standalone

--nginx は、nginx がすでに稼働し、ドメインを配信している場合の適切なデフォルトです。Certbot は設定を解析し、一時的な challenge 用 location を追加して nginx を reload し、検証後に server block へ TLS ディレクティブを書き込みます。ダウンタイムは発生しません。

sudo certbot --nginx -d example.com -d www.example.com

新規サーバー向けのスクリプト化された方法です。

sudo certbot --nginx \
  -d example.com -d www.example.com \
  --agree-tos -m ops@example.com --no-eff-email \
  --redirect --non-interactive

--webroot は、Certbot を nginx の設定から完全に分離したい場合に適しています。設定をテンプレートから生成する場合、git で管理する場合、または Ansible で配布する場合に使えます。Certbot が書き込むのは、すでに配信しているディレクトリ内の challenge ファイルだけです。

sudo certbot certonly --webroot -w /var/www/example.com \
  -d example.com -d www.example.com \
  --deploy-hook "systemctl reload nginx"

--standalone は、port 80 で待ち受けるプロセスがない場合に適しています。メールサーバー、443 のみで通信する API、nginx が存在する前に実行される初回起動スクリプトなどが該当します。Certbot 自身が数秒間 port 80 を bind します。nginx が稼働中の場合、この処理は失敗するため、実行中だけ nginx を停止します。

sudo certbot certonly --standalone -d mail.example.com \
  --pre-hook "systemctl stop nginx" \
  --post-hook "systemctl start nginx"

これらの hook は証明書の更新設定に記録されるため、更新時も同じ停止・起動処理が unattended で実行されます。

証明書が存在する前後で動作する server ブロック

いわゆる鶏と卵の問題です。存在しないファイルを指す ssl_certificate があると nginx は起動を拒否します。一方、nginx が停止していると Certbot は検証できません。まずポート 80 でサイトを起動します。

server {
    listen 80;
    listen [::]:80;
    server_name example.com www.example.com;

    root /var/www/example.com;
    index index.html;

    location ^~ /.well-known/acme-challenge/ {
        root /var/www/example.com;
        default_type "text/plain";
        try_files $uri =404;
    }

    location / {
        try_files $uri $uri/ =404;
    }
}

sudo nginx -t && sudo systemctl reload nginx を実行し、curl -I http://example.com/ がサーバーの 外部 から応答することを確認してから、証明書を発行します。その後、次を実行します。

server {
    listen 80;
    listen [::]:80;
    server_name example.com www.example.com;

    location ^~ /.well-known/acme-challenge/ {
        root /var/www/example.com;
        default_type "text/plain";
    }

    location / {
        return 301 https://$host$request_uri;
    }
}

server {
    listen 443 ssl;
    listen [::]:443 ssl;
    server_name example.com www.example.com;

    ssl_certificate     /etc/letsencrypt/live/example.com/fullchain.pem;
    ssl_certificate_key /etc/letsencrypt/live/example.com/privkey.pem;
    include /etc/letsencrypt/options-ssl-nginx.conf;
    ssl_dhparam /etc/letsencrypt/ssl-dhparams.pem;

    root /var/www/example.com;
    index index.html;

    location / {
        try_files $uri $uri/ =404;
    }
}

ACME location に ^~ プレフィックスを付けることには意味があります。これにより、return 301 ブロックがチャレンジ要求を取り込むのを防げます。この location をポート 80 に残しておけば、サイト全体を HTTPS 専用にした後も更新が機能します。

上記の両方のブロックはディスク上のファイルを配信します。nginx がアプリケーションの前段にある場合、location /proxy_pass ブロックになります。リバースプロキシの server ブロックを行ごとに確認する説明では、アプリケーションに必要なヘッダーを扱っています。ACME location と TLS ディレクティブはそのままにします。

HTTP/2 の構文は nginx のバージョンによって異なります。2 つの形式を混在させると、起動エラーになります。Ubuntu 24.04 には nginx 1.24 が含まれており、インライン形式の listen 443 ssl http2; を使用します。Debian 13 にはより新しい nginx が含まれており、独立した http2 on; ディレクティブを使用します。まず nginx -v を確認してください。

nginx には live/ を指定し、archive/ は指定しないでください。live/ の symlink は更新のたびに付け替えられます。archive/ 内の固定パスを指定すると、期限切れになる証明書に固定されます。

ワイルドカードには DNS-01 が必要で、DNS-01 にはプラグインが必要です

ワイルドカード証明書(*.example.com)は HTTP-01 では検証できません。ファイルを取得する単一のホスト名が存在しないためです。唯一の方法は DNS-01 です。_acme-challenge.example.com TXT レコードを公開して、ドメインを管理していることを証明します。Certbot がこれを非対話的に実行するには、DNS プロバイダーの API 認証情報が必要です。そのためにプロバイダープラグインを使用します。ワイルドカード証明書の詳しい手順では、TXT レコードの仕組みと manual モードでの更新時の注意点を説明しています。以下では、Cloudflare の短い手順を示します。

sudo snap set certbot trust-plugin-with-root=ok
sudo snap install certbot-dns-cloudflare

apt を使う場合は、代わりに sudo apt install python3-certbot-dns-cloudflare を使用します。認証情報は root 専用のファイルに保存します。

# /root/.secrets/cloudflare.ini
# then: sudo chmod 600 /root/.secrets/cloudflare.ini
dns_cloudflare_api_token = your_scoped_token_here

トークンの権限は、そのゾーンの DNS 編集だけに限定してください。これは DNS の鍵です。鍵と同じように厳重に扱ってください。

sudo certbot certonly \
  --dns-cloudflare \
  --dns-cloudflare-credentials /root/.secrets/cloudflare.ini \
  -d example.com -d '*.example.com'

ワイルドカードを引用符で囲み、shell が glob として展開しないようにします。DNS-01 を使えば、HTTP-01 では対応できない対象にも証明書を発行できます。たとえば、public port 80 を持たないホスト、内部サービス、VPS 上で自己ホストする WireGuard VPN 経由でのみ到達できるサーバー、private interface 上の管理パネルなどです。

更新: 90 日間の有効期間、timer、deploy hook

Let's Encrypt の証明書の有効期間は 90 日間です。Certbot は有効期限まで 30 日未満になると更新するため、更新に失敗しても、障害ではなく修正可能な問題として対処できる 30 日間の余裕があります。Let's Encrypt は有効期限の警告メールを送信しなくなりました。誰かが知らせてくれるわけではないため、監視は自分で行う必要があります。

インストール時に用意された timer を確認します。

systemctl list-timers 'certbot*' 'snap.certbot*'
sudo certbot certificates

certbot renew/etc/letsencrypt/renewal/ 内のすべての設定を確認し、有効期限まで 30 日以上あるものを除外して、残りを初回実行時とまったく同じフラグで更新します。そのため、初回実行が重要です。記録されるのは初回実行時の内容だからです。

ディスク上の証明書を更新しただけでは、何も変わりません。nginx は、reload を指示されるまでメモリ上の古い証明書を提供し続けます。deploy hook を一度設定します。

sudo tee /etc/letsencrypt/renewal-hooks/deploy/reload-nginx.sh >/dev/null <<'EOF'
#!/bin/sh
set -e
nginx -t && systemctl reload nginx
EOF
sudo chmod +x /etc/letsencrypt/renewal-hooks/deploy/reload-nginx.sh

renewal-hooks/deploy/ 内の実行可能ファイルは、更新に成功するたびに実行されます。--deploy-hook フラグを使うと、1 つの証明書に対して同じ処理を行い、renew_hook = ... を更新設定に保存できます。certbot --nginx は自動的に reload しますが、--webroot--standalone の構成では自動的に reload されません。hook がないと、certbot certificates が新しい証明書を正常に報告しているのに、サイトが期限切れの証明書を提供することになります。起動時に証明書を読み込む他のサービスにも同じ hook が必要です。Docker、TLS、バックアップを使用した Nextcloud VPS の構築 のようなコンテナ化されたアプリでは、独自の restart または reload 手順もここに設定する必要があります。

実際の更新をテストする

sudo certbot renew --dry-run

これは Let's Encrypt のステージング環境に対して、完全なチャレンジを実行します。コードパス、ファイアウォール、DNS は本番と同じです。レート制限を消費せず、ディスクにも何も書き込みません。今日これが成功すれば、サーバーの状態が変わらない限り、60 日後の自動更新も成功します。

dry run では、reload hook が実行されることまでは確認できません。この動作は Certbot のバージョンによって異なります。その部分は手動でテストします。hook script を直接実行し、systemctl reload nginx が成功することを確認して、sudo grep renew_hook /etc/letsencrypt/renewal/example.com.conf を確認します。

The errors you will actually hit

Could not bind to IPv4 or IPv6., --standalone while nginx already holds port 80. Use --nginx or --webroot, or stop nginx around the run. Confirm the holder with sudo ss -lntp | grep ':80'.

Timeout during connect (likely firewall problem), Let's Encrypt could not reach port 80. Walk outward: sudo ufw status (open it with sudo ufw allow 'Nginx Full'), then the VPS provider's own firewall, then DNS. Test from somewhere that is not your server: curl -sSv http://example.com/.well-known/acme-challenge/test. A stale AAAA record produces this same message.

unauthorized :: Invalid response from http://example.com/.well-known/acme-challenge/xyz: 404, port 80 is reachable, but the token is not being served. The request landed in a different server block (check which one owns default_server), or the directory passed to -w is not the one nginx serves. Drop a file at /var/www/example.com/.well-known/acme-challenge/test and fetch it externally; if that 404s, the certificate was never the problem.

DNS problem: NXDOMAIN looking up A for example.com, the name does not resolve publicly. New records that have not propagated, or a record in a zone your registrar is not serving.

too many certificates already issued for: example.com, a rate limit, and the one people hit while debugging in a loop. Let's Encrypt caps duplicate certificates, the same exact set of names, at five per week, and separately allows 50 new certificates per registered domain per week; nothing unblocks either except time. Debug against staging with --dry-run.

nginx: [emerg] cannot load certificate "/etc/letsencrypt/live/example.com/fullchain.pem": No such file or directory, nginx is configured for a certificate never issued, or one removed with certbot delete. Comment out the TLS server block, start nginx, issue, restore the block.

open() "/etc/letsencrypt/options-ssl-nginx.conf" failed, that file arrives with the nginx plugin package. On a certonly box without python3-certbot-nginx, either add the plugin or replace the include line with your own ssl_protocols and ssl_ciphers settings.

大規模運用で管理する

1 枚の証明書には最大 100 個の名前を含められます。単一の certbot --nginx -d a.example.com -d b.example.com ... は便利に見えますが、古い DNS レコードが 1 つでも検証に失敗すると、その証明書に含まれる他の名前もすべて利用できなくなります。サイトごとに証明書を分ければ、各サイトが独立して失敗します。数個を超えるサービスを 1 台で運用する場合は、この構成が適しています。サイト数がさらに増えたら、ACME に対応したフロントエンドプロキシを導入する価値があります。Docker Compose で複数のアプリを実行する Traefik リバースプロキシが証明書の取得と更新を自動で行うため、Certbot は完全に不要になります。フロントエンドにどのプロキシを置くかは別の判断です。Nginx、Caddy、Traefik を比較する際は、証明書管理とアプリごとの設定をどこまでプロキシに任せたいかが主な基準になります。

/etc/letsencrypt 全体を、sudo tar -czf letsencrypt-$(date +%F).tar.gz -C /etc letsencrypt とシンボリックリンクを維持したままバックアップします。このツリーには accounts/ が含まれています。これは ACME アカウントキーであり、同一の内容を再生成することはできません。新しい VPS へ移行する場合は、-a を使ってこのツリーを rsync し、Certbot をインストールして DNS の向け先を変更し、切り替え前に certbot renew --dry-run を実行するだけです。

サーバーを再構築したり、新しい LTS に移行したりしても、更新タイマーは引き継がれません。移行、スナップショットのリストア、またはディストリビューションのアップグレード後は、systemctl list-timers 'certbot*' と 1 回の --dry-run を実行します。これを省略すると、更新されると思われていた証明書が 89 日後の午前 3 時に期限切れになり、サイトが停止します。

ここまでの説明は、管理下にあるマシンで、パブリック IP を持ち、インターネット全体から port 80 に接続できることを前提としています。つまり VPS です。上記の手順は、これらの環境で同じように利用できます。

同じ証明書手順は、nginx ではなく Apache を使用する場合にも適用できます。パブリック証明書を使えない場合は、Ubuntu で自己署名証明書を作成することで内部サービスに対応できます。

FAQ

HTTPS のみを提供するサイトでも、port 80 を開く必要がありますか?

はい。HTTP-01 challenge に必要です。Let's Encrypt は必ず port 80 で検証リクエストを開始します。Certbot には TLS-ALPN-01 の実装がないため、443 だけを開くファイアウォールでは、初回発行と、その後の自動更新がすべて失敗します。port 80 から HTTPS へのリダイレクトは問題ありません。検証はリダイレクトに従います。port 80 を完全に閉じる唯一の方法は、provider plugin を使った DNS-01 です。

Ubuntu 24.04 で nginx 用の Certbot をインストールする場合、apt と snap のどちらを使うべきですか?

apt を使ってください。sudo apt install certbot python3-certbot-nginx では Ubuntu 24.04 向けに Certbot 2.9.0 が提供されます。このバージョンは、このガイドの内容に必要な機能を十分に備えており、unattended-upgrades を通じてセキュリティパッチを受け取れます。また、snapd も必要ありません。最新リリースをすぐに使う必要がある場合や、DNS plugin が snap でのみ配布されている場合に限り、snap を選択してください。どちらを使う場合も、必ず一方だけを選んでください。2 つインストールすると、同じ /etc/letsencrypt tree を対象とする更新 timer が 2 つ動作します。忘れた方が問題を起こします。

Certbot で nginx 用の wildcard certificate を発行できますか?

DNS-01 を使う場合に限り可能です。*.example.com のような wildcard には、challenge file を取得する単一の hostname がありません。そのため、--nginx--webroot--standalone はすべて使えません。DNS provider 用の plugin をインストールし、scope を限定した API token を root のみが読める credentials file に保存してから、wildcard が shell の glob として展開されないように引用符で囲んで certbot certonly --dns-cloudflare -d example.com -d '*.example.com' を実行してください。

更新が成功したのに、nginx が古い certificate を提供し続けるのはなぜですか?

nginx は certificate をメモリに保持しているため、ディスク上の新しい file を reload するまで認識しません。certbot --nginx は自動的に reload しますが、--webroot--standalone の実行では reload されません。そのため、更新自体は成功していても、browser には有効期限が近い certificate が表示されることがあります。/etc/letsencrypt/renewal-hooks/deploy/ に実行可能な script を配置し、その script で nginx -t && systemctl reload nginx を実行するようにしてください。これにより、更新が成功するたびに実行されます。

certbot renew --dry-run で更新が機能することを確認できますか?

おおむね確認できます。これは staging environment に対して実際の challenge を実行します。firewall、DNS、code path は本番と同じで、rate limit を消費せず、ディスクにも何も書き込みません。そのため、成功すれば network 側に問題がないことを確認できます。ただし、deploy hook が実行されることまでは確実に確認できません。これは別にテストしてください。hook script を手動で実行し、sudo grep renew_hook /etc/letsencrypt/renewal/example.com.conf を確認します。