跳到内容

← 返回知识库WIKI/Linux/ARTICLE

Nginx 开通 HTTPS:从准备到自动续期 ​

这篇教程适合已经有一台运行 Nginx 的 Ubuntu 服务器,想给一个或多个域名启用 HTTPS 的场景。示例使用 Let's Encrypt 的免费证书,证书由 Certbot 申请和自动续期。

先选方案 ​

常见有三种做法:

方案适合场景取舍
Nginx + Certbot webrootNginx 已经在运行,网站不能停配置最透明,推荐大多数网站使用
Certbot standalone还没有 Web 服务,或能暂时停掉 80 端口操作简单,但申请时必须释放 80 端口
云厂商证书托管/CDN网站已经接入云负载均衡或 CDN证书部署省事,但绑定云厂商,配置链路更长

本文采用第一种:Nginx 在 80 端口提供 ACME 校验文件,在 443 端口提供 HTTPS,并把 HTTP 请求重定向到 HTTPS。

前置条件 ​

  1. 有服务器的 root 或 sudo 权限。
  2. 域名的 A/AAAA 记录已指向服务器。多个域名可以一起申请一张证书。
  3. 公网入站 TCP 80 和 443 已放行,云安全组和服务器防火墙都要检查。
  4. 已安装 Nginx,且 nginx -t 能通过。
  5. 安装 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.sh

Docker 环境将最后一行替换成自己的 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 容器是否读到了正确的证书文件。