外观
Nginx 开通 HTTPS:从准备到自动续期
这篇教程适合已经有一台运行 Nginx 的 Ubuntu 服务器,想给一个或多个域名启用 HTTPS 的场景。示例使用 Let's Encrypt 的免费证书,证书由 Certbot 申请和自动续期。
先选方案
常见有三种做法:
| 方案 | 适合场景 | 取舍 |
|---|---|---|
| Nginx + Certbot webroot | Nginx 已经在运行,网站不能停 | 配置最透明,推荐大多数网站使用 |
| Certbot standalone | 还没有 Web 服务,或能暂时停掉 80 端口 | 操作简单,但申请时必须释放 80 端口 |
| 云厂商证书托管/CDN | 网站已经接入云负载均衡或 CDN | 证书部署省事,但绑定云厂商,配置链路更长 |
本文采用第一种:Nginx 在 80 端口提供 ACME 校验文件,在 443 端口提供 HTTPS,并把 HTTP 请求重定向到 HTTPS。
前置条件
- 有服务器的 root 或 sudo 权限。
- 域名的 A/AAAA 记录已指向服务器。多个域名可以一起申请一张证书。
- 公网入站 TCP 80 和 443 已放行,云安全组和服务器防火墙都要检查。
- 已安装 Nginx,且
nginx -t能通过。 - 安装 Certbot:
bash
sudo apt update
sudo apt install -y certbot如果 Nginx 在 Docker 中运行,宿主机上的 Certbot 写入的 webroot 必须映射到 Nginx 容器内同一个目录;证书文件也必须挂载到容器中。不要只在宿主机生成文件,却让容器看不到它们。
第一步:准备 ACME 校验目录
创建一个目录,Let's Encrypt 会通过 HTTP 访问其中的临时文件:
bash
sudo mkdir -p /var/www/acme
sudo chown -R www-data:www-data /var/www/acme先在 Nginx 的 80 端口配置中加入:
nginx
server {
listen 80;
server_name example.com www.example.com;
location ^~ /.well-known/acme-challenge/ {
root /var/www/acme;
default_type text/plain;
}
location / {
return 301 https://$host$request_uri;
}
}把 example.com 换成自己的域名。执行 sudo nginx -t && sudo systemctl reload nginx 后,用一个测试文件确认外网能访问:
bash
sudo mkdir -p /var/www/acme/.well-known/acme-challenge
echo ok | sudo tee /var/www/acme/.well-known/acme-challenge/healthcheck
curl -i http://example.com/.well-known/acme-challenge/healthcheck应当返回 200 和 ok。如果超时,先检查 DNS、云安全组、防火墙和端口映射,不要继续申请证书。
第二步:申请证书
bash
sudo certbot certonly --webroot \
-w /var/www/acme \
-d example.com \
-d www.example.com \
--email admin@example.com \
--agree-tos \
--no-eff-email证书默认位于 /etc/letsencrypt/live/example.com/:
fullchain.pem:证书链,给ssl_certificate使用。privkey.pem:私钥,给ssl_certificate_key使用,权限不能放宽。
证书私钥不要提交 Git、发到聊天工具或复制到前端项目。多人协作时只共享域名和证书路径,不共享私钥内容。
第三步:添加 HTTPS server
在 443 的 server 中加入静态站点或反向代理。静态站点示例:
nginx
server {
listen 443 ssl http2;
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;
ssl_protocols TLSv1.2 TLSv1.3;
root /var/www/example;
index index.html;
}反向代理时保留这些请求头,让后端知道原始请求是 HTTPS:
nginx
location /api/ {
proxy_pass http://127.0.0.1:8080;
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;
}修改后必须先检查再 reload:
bash
sudo nginx -t
sudo systemctl reload nginx一个交互式配置脚本
下面的脚本只生成一个新的 Nginx 配置文件,不会覆盖已有文件。它适用于 Nginx 运行在宿主机上的 Ubuntu;Docker 用户需要把 ACME_DIR 和 CERT_DIR 改成容器内可见的挂载路径。
保存为 enable-https.sh,以 root 执行:
bash
#!/usr/bin/env bash
set -euo pipefail
read -r -p "域名(多个用空格分隔,例如 example.com www.example.com): " -a DOMAINS
read -r -p "Nginx 配置目录 [/etc/nginx/sites-available]: " NGINX_DIR
NGINX_DIR=${NGINX_DIR:-/etc/nginx/sites-available}
read -r -p "网站根目录 [/var/www/html]: " WEB_ROOT
WEB_ROOT=${WEB_ROOT:-/var/www/html}
read -r -p "ACME 校验目录 [/var/www/acme]: " ACME_DIR
ACME_DIR=${ACME_DIR:-/var/www/acme}
read -r -p "证书邮箱: " EMAIL
if [[ ${#DOMAINS[@]} -eq 0 || -z "$EMAIL" ]]; then
echo "域名和邮箱不能为空" >&2
exit 1
fi
PRIMARY_DOMAIN=${DOMAINS[0]}
SERVER_NAMES="${DOMAINS[*]}"
CONFIG="$NGINX_DIR/$PRIMARY_DOMAIN.conf"
sudo mkdir -p "$ACME_DIR" "$NGINX_DIR"
sudo tee "$CONFIG" >/dev/null <<NGINX
server {
listen 80;
server_name $SERVER_NAMES;
location ^~ /.well-known/acme-challenge/ {
root $ACME_DIR;
default_type text/plain;
}
location / { return 301 https://\$host\$request_uri; }
}
NGINX
sudo nginx -t
sudo systemctl reload nginx
CERT_ARGS=()
for domain in "${DOMAINS[@]}"; do CERT_ARGS+=("-d" "$domain"); done
sudo certbot certonly --webroot -w "$ACME_DIR" "${CERT_ARGS[@]}" \
--email "$EMAIL" --agree-tos --no-eff-email
sudo tee -a "$CONFIG" >/dev/null <<NGINX
server {
listen 443 ssl http2;
server_name $SERVER_NAMES;
ssl_certificate /etc/letsencrypt/live/$PRIMARY_DOMAIN/fullchain.pem;
ssl_certificate_key /etc/letsencrypt/live/$PRIMARY_DOMAIN/privkey.pem;
ssl_protocols TLSv1.2 TLSv1.3;
root $WEB_ROOT;
index index.html;
}
NGINX
sudo nginx -t
sudo systemctl reload nginx
echo "HTTPS 已配置:$SERVER_NAMES"脚本故意没有自动改防火墙、DNS 或 Docker compose;这些操作依赖云厂商和现有部署方式,自动修改容易造成不可预期的断网。
自动续期
Certbot 通常会安装 systemd timer。先查看状态,再做一次演练:
bash
systemctl list-timers | grep certbot
sudo certbot renew --dry-run如果 Nginx 需要 reload 才能读取新证书,可添加 deploy hook:
bash
sudo sh -c 'printf "%s\n" "#!/bin/sh" "nginx -t && systemctl reload nginx" > /etc/letsencrypt/renewal-hooks/deploy/reload-nginx.sh'
sudo chmod 700 /etc/letsencrypt/renewal-hooks/deploy/reload-nginx.shDocker 环境将最后一行替换成自己的 reload 命令,例如 docker exec <nginx容器名> nginx -s reload,并确认 hook 执行用户有权限访问 Docker。
上线后的校验
bash
curl -sI http://example.com/
curl -sI https://example.com/
openssl s_client -connect example.com:443 -servername example.com </dev/null 2>/dev/null | openssl x509 -noout -dates -subject预期结果:HTTP 返回 301 并跳转到 HTTPS;HTTPS 返回业务页面的状态码;证书的 notAfter 在未来。浏览器仍提示证书错误时,重点检查域名是否包含在证书中、系统时间是否正确、443 是否放行,以及 Nginx 容器是否读到了正确的证书文件。