16
0
0

Nginx 多站点代理、Certbot SSL 与证书错乱排查实战

文章摘要
|

Docker 迁移 WordPress 到新服务器:Nginx 多站点代理、Certbot SSL 与证书错乱排查实战

最近将一个 WordPress 网站从旧服务器迁移到新的 Ubuntu 服务器。

新服务器本身已经部署了其他系统,并且宿主机已经安装 Nginx,因此这次没有继续使用 Docker 中的 Nginx,而是采用:

互联网
   ↓
宿主机 Nginx
   ↓
Docker WordPress
   ↓
Docker MySQL

同时,这台服务器还需要运行多个系统、多个域名,并分别配置 HTTPS。

迁移过程中遇到了几个比较典型的问题:

  • WordPress 如何通过 Docker Compose 部署

  • 旧数据库如何导入

  • wp-config.php 如何修改

  • 宿主机 Nginx 如何代理 Docker WordPress

  • 一台服务器多个域名如何配置 Certbot

  • Certbot 为什么验证失败

  • 为什么访问 WordPress 域名却显示另一个网站的 SSL 证书

  • 如何判断到底是 DNS、Nginx 还是证书的问题

本文完整记录这次迁移过程。


一、部署结构

项目目录如下:

/opt/a.example/
├── docker-compose.yml
├── html/
├── mysql/
├── update.sql
└── a.example.com.zip

其中:

  • html/:保存 WordPress 网站文件

  • mysql/:用于持久化 MySQL 数据

  • update.sql:从旧服务器导出的 WordPress 数据库

Docker Compose 配置如下:

services:
​
  mysql:
    image: mysql:8.0
    container_name: wordpress-mysql
    restart: unless-stopped
​
    environment:
      MYSQL_ROOT_PASSWORD: root123456
      MYSQL_DATABASE: wordpress
      MYSQL_USER: wordpress
      MYSQL_PASSWORD: wordpress123456
​
    volumes:
      - ./mysql:/var/lib/mysql
​
  wordpress:
    image: wordpress:php8.2-apache
    container_name: wordpress
    restart: unless-stopped
​
    depends_on:
      - mysql
​
    ports:
      - "127.0.0.1:8080:80"
​
    volumes:
      - ./html:/var/www/html

这里有一个值得注意的地方:

ports:
  - "127.0.0.1:8080:80"

而不是:

ports:
  - "8080:80"

原因是 WordPress 不需要直接暴露到公网。

公网请求统一经过宿主机 Nginx:

互联网
   ↓
Nginx :443
   ↓
127.0.0.1:8080
   ↓
WordPress Docker :80

这样安全性更高,后续维护也更方便。


二、启动 WordPress 和 MySQL

进入项目目录:

cd /opt/a.example

启动:

docker compose up -d

查看容器状态:

sudo docker ps

正常应该看到:

wordpress
wordpress-mysql

并且两个容器都处于 Up 状态。


三、导入旧 WordPress 数据库

数据库文件位于:

/opt/a.example/update.sql

不需要把 SQL 文件复制进 MySQL 容器,可以直接通过标准输入导入:

docker exec -i wordpress-mysql mysql \
  -uwordpress \
  -pwordpress123456 \
  wordpress < /opt/a.example/update.sql

参数含义:

  • -uwordpress:数据库用户名

  • -pwordpress123456:数据库密码

  • wordpress:数据库名

这里几个参数和符号需要理解一下。

-i 是什么意思

-idocker exec 的参数,表示:

保持标准输入 STDIN 打开

也就是允许宿主机把内容继续传递给容器里的命令。

这里之所以需要 -i,是因为后面的 SQL 文件:

/opt/a.example/update.sql

需要通过标准输入传给容器里的 mysql 命令。

-t 是什么意思

平时进入容器时经常会看到:

docker exec -it wordpress bash

其中:

-i:保持标准输入打开,可以继续输入内容
-t:分配一个伪终端 TTY

-t 可以理解为给命令提供一个类似真实终端的交互环境。

所以:

docker exec -it wordpress bash

适合进入容器后继续手动执行命令。

而导入 SQL 时:

docker exec -i wordpress-mysql mysql ...

只需要把 SQL 文件内容传进去,并不需要交互式终端,所以只使用 -i,不需要 -t

< 是什么意思

< 是 Linux Shell 的输入重定向符。

例如:

mysql wordpress < update.sql

表示:

update.sql
   ↓
mysql
   ↓
wordpress 数据库

也就是:

update.sql 文件中的内容作为 mysql 命令的标准输入执行。

> 是什么意思

>< 方向相反。

> 表示把命令的输出写入文件。

例如导出数据库:

mysqldump -uroot -p123456 wordpress > backup.sql

表示:

MySQL 数据
   ↓
mysqldump
   ↓
   >
   ↓
backup.sql

可以简单记:

<  文件 → 命令,一般用于导入
>  命令 → 文件,一般用于导出

或者说符号的方向指向那里就代表数据流向哪里


四、修改 wp-config.php

由于本次迁移使用的是旧服务器上的 WordPress 数据,因此在启动新环境之前,先将原 WordPress 网站文件解压并放到:

/opt/a.example/html/

目录中。

例如最终目录类似:

/opt/a.example/
├── docker-compose.yml
├── html/
│   ├── wp-admin/
│   ├── wp-content/
│   ├── wp-includes/
│   ├── wp-config.php
│   └── ...
├── mysql/
└── update.sql

这里的 html/ 会通过 Docker Compose 挂载到 WordPress 容器:

volumes:
  - ./html:/var/www/html

也就是说:

宿主机:
/opt/a.example/html/

        ↓ 挂载

WordPress 容器:
/var/www/html/

由于 wp-config.php 也是从旧服务器迁移过来的,所以里面原来的数据库地址、数据库名、用户名和密码,很可能还是旧服务器的配置。

因此需要修改:

/opt/a.example/html/wp-config.php

使其与当前 docker-compose.yml 中的 MySQL 配置保持一致。

WordPress 配置文件位于:

/opt/a.example/html/wp-config.php

编辑:

vim /opt/a.example/html/wp-config.php

数据库连接配置:

define( 'DB_NAME', 'wordpress' );
define( 'DB_USER', 'wordpress' );
define( 'DB_PASSWORD', 'wordpress123456' );
define( 'DB_HOST', 'mysql' );

这里最容易写错的是 DB_HOST

必须写:

define( 'DB_HOST', 'mysql' );

不能写:

define( 'DB_HOST', 'localhost' );

也不能写:

define( 'DB_HOST', '127.0.0.1' );

因为 WordPress 和 MySQL 是两个不同的 Docker 容器。

Docker Compose 会自动根据服务名:

services:
  mysql:

提供内部 DNS,因此 WordPress 可以直接通过 mysql 访问数据库。


五、指定 WordPress 正确域名

站点域名为:

https://www.a.example.com

可以在 wp-config.php 中明确指定:

define( 'WP_HOME', 'https://www.a.example.com' );
define( 'WP_SITEURL', 'https://www.a.example.com' );

推荐放在:

/* That's all, stop editing! Happy publishing. */

之前。


六、Nginx HTTPS 反向代理时的 WordPress 特殊处理

实际访问链路如下:

浏览器
   ↓ HTTPS
宿主机 Nginx
   ↓ HTTP
WordPress Docker

WordPress 容器实际收到的是 HTTP 请求。

如果不处理,可能出现:

  • HTTPS 无限重定向

  • WordPress 后台跳回 HTTP

  • 登录异常

  • Mixed Content

  • 静态资源协议错误

因此建议在 wp-config.php 中加入:

if (
    isset($_SERVER['HTTP_X_FORWARDED_PROTO']) &&
    $_SERVER['HTTP_X_FORWARDED_PROTO'] === 'https'
) {
    $_SERVER['HTTPS'] = 'on';
}

最终相关配置可以整理为:

define( 'DB_NAME', 'wordpress' );
define( 'DB_USER', 'wordpress' );
define( 'DB_PASSWORD', 'wordpress123456' );
define( 'DB_HOST', 'mysql' );
​
define( 'WP_HOME', 'https://www.a.example.com' );
define( 'WP_SITEURL', 'https://www.a.example.com' );
​
if (
    isset($_SERVER['HTTP_X_FORWARDED_PROTO']) &&
    $_SERVER['HTTP_X_FORWARDED_PROTO'] === 'https'
) {
    $_SERVER['HTTPS'] = 'on';
}

七、先测试 WordPress 本身

在配置 Nginx 和 SSL 前,先确认 WordPress 自身正常。

执行:

curl -I http://127.0.0.1:8080

当时返回:

HTTP/1.1 200 OK
Server: Apache/2.4.68 (Debian)
X-Powered-By: PHP/8.2.33

并且 WordPress 返回了:

Link: <https://www.a.example.com/wp-json/>

说明以下组件基本正常:

Docker
MySQL
PHP
WordPress

八、一台宿主机 Nginx 管理多个系统

服务器上除了 WordPress,还有其他系统,例如:

b.example.cn
www.a.example.com

不需要每个项目单独安装一套 Nginx。

推荐结构:

/etc/nginx/conf.d/
├── h2esl.conf
├── a.example.conf
└── future-project.conf

每个项目独立一个配置文件。

这种结构比较适合一台服务器部署多个系统。


九、配置 WordPress Nginx

创建配置文件:

sudo vim /etc/nginx/conf.d/a.example.conf

HTTP 配置:

# 裸域名 HTTP www HTTP -> www HTTPS
server {
    listen 80;
    server_name a.example.com www.a.example.com;

    return 301 https://www.a.example.com$request_uri;
}


server {
    listen 443 ssl;
    server_name a.example.com;

    ssl_certificate /etc/letsencrypt/live/www.a.example.com/fullchain.pem;
    ssl_certificate_key /etc/letsencrypt/live/www.a.example.com/privkey.pem;

    include /etc/letsencrypt/options-ssl-nginx.conf;
    ssl_dhparam /etc/letsencrypt/ssl-dhparams.pem;

    return 301 https://www.a.example.com$request_uri;
}

# www HTTPS 主站
server {
    listen 443 ssl;
    server_name www.a.example.com;

    client_max_body_size 100M;

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

    location / {
        proxy_pass http://127.0.0.1:8080;

        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 X-Forwarded-Host $host;
        proxy_set_header X-Forwarded-Port $server_port;

        proxy_connect_timeout 60s;
        proxy_send_timeout 300s;
        proxy_read_timeout 300s;
    }
}

这里设置:

client_max_body_size 100M;

主要是考虑 WordPress 经常需要上传:

  • 图片

  • 插件

  • 主题

  • ZIP 文件

  • 备份文件

Nginx 默认上传限制通常不够。

配置完成后检查:

sudo nginx -t

如果看到:

syntax is ok
test is successful

重新加载:

sudo systemctl reload nginx

十、Certbot 第一次申请失败

执行:

sudo certbot --nginx -d www.a.example.com  
  • 如果你希望 裸域名 a.example.comwww.a.example.com 都支持 HTTPS,申请证书时应该一起加上,我这里只申请了www的。

结果失败:

Certbot failed to authenticate some domains

Domain: www.a.example.com

Type: unauthorized

Detail:
77.37.95.246: Invalid response from
http://www.a.example.com/.well-known/acme-challenge/...
404

一开始很容易怀疑:

  • Nginx 配置有问题

  • WordPress 返回 404

  • Certbot 配置有问题

但真正关键的信息其实是:

77.37.95.246

十一、排查 DNS

执行:

dig +short A www.a.example.com

得到:

77.37.95.246

但是新服务器实际公网 IP 是:

43.153.18.224

这就说明:

www.a.example.com

实际上还指向旧服务器。

Let’s Encrypt 在验证时访问的是:

http://www.a.example.com/.well-known/acme-challenge/xxx

而 DNS 告诉它:

www.a.example.com
→ 77.37.95.246

所以请求直接去了旧服务器。

新服务器上的 Certbot 自然无法完成验证。


十二、切换 DNS 到新服务器

进入域名 DNS 管理后台,将 www.a.example.com 的 A 记录从:

77.37.95.246

修改为:

43.153.18.224

修改后检查:

dig +short A www.a.example.com

直到返回:

43.153.18.224

然后重新执行:

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

这一次证书成功申请。


十三、旧服务器还有 SSL,会冲突吗

不会。

同一个域名完全可以存在多份有效 SSL 证书,例如:

旧服务器
www.a.example.com 证书

新服务器
www.a.example.com 证书

证书并不会因为新服务器重新申请,就把旧服务器上的证书立即作废。

真正决定用户访问哪台服务器的是:

DNS

当 DNS 指向:

43.153.18.224

正常用户就会访问新服务器。

不过旧服务器后续如果继续通过 HTTP-01 自动续期 SSL,就可能失败,因为 Let’s Encrypt 的验证请求已经会访问新服务器。

因此迁移完成后,应该最终关闭旧服务器上的:

  • WordPress

  • Nginx

  • Certbot 自动续期


十四、访问 WordPress,却显示另一个网站的 SSL 证书

证书申请成功后,又遇到了一个比较奇怪的问题。

访问:

https://www.a.example.com

浏览器一度显示的是:

b.example.cn

的证书。

因为这台服务器同时运行:

b.example.cn
www.a.example.com

第一反应很自然:

两个域名的 SSL 证书是不是配混了?

于是检查:

sudo cat /etc/nginx/conf.d/a.example.conf

结果配置实际上是正确的:

server_name www.a.example.com;

listen 443 ssl;

ssl_certificate /etc/letsencrypt/live/www.a.example.com/fullchain.pem;
ssl_certificate_key /etc/letsencrypt/live/www.a.example.com/privkey.pem;

十五、检查整个 Nginx 的 SSL 配置

执行:

sudo nginx -T | grep -n -E "server_name|listen 443|ssl_certificate"

可以看到:

server_name b.example.cn;
listen 443 ssl;
ssl_certificate /etc/letsencrypt/live/b.example.cn/fullchain.pem;

同时也有:

server_name www.a.example.com;
listen 443 ssl;
ssl_certificate /etc/letsencrypt/live/www.a.example.com/fullchain.pem;

说明:

  • b.example.cn 使用自己的证书

  • www.a.example.com 也使用自己的证书

Nginx 配置并没有串。


十六、SNI:为什么一台服务器能配置多个 SSL 证书

这里涉及一个非常重要的概念:

SNI
Server Name Indication

一台服务器可能只有一个公网 IP:

43.153.18.224

但是同时运行:

b.example.cn
www.a.example.com
xxx.example.com

这些网站全部监听:

443

客户端建立 TLS 连接时,会把自己要访问的域名一起发送给 Nginx。

例如:

我要访问 www.a.example.com

Nginx 根据这个域名匹配:

server_name www.a.example.com;

然后返回对应证书:

ssl_certificate /etc/letsencrypt/live/www.a.example.com/fullchain.pem;

因此下面这种架构完全没有问题:

同一个 IP
+
同一个 443
+
多个域名
+
多个 SSL 证书

十七、使用 OpenSSL 检查真实返回的证书

浏览器可能存在:

  • DNS 缓存

  • TLS 连接复用

  • HTTP/2 长连接

  • 证书状态缓存

所以遇到 SSL 问题时,不应该只看浏览器。

可以直接执行:

echo | openssl s_client \
  -connect www.a.example.com:443 \
  -servername www.a.example.com \
  2>/dev/null \
  | openssl x509 \
  -noout \
  -subject \
  -ext subjectAltName

这里最关键的是:

-servername www.a.example.com

它会模拟浏览器发送 SNI。

实际返回:

subject=CN = www.a.example.com

X509v3 Subject Alternative Name:
    DNS:www.a.example.com

说明服务器实际上已经返回正确证书。


十八、区分本机 Nginx 问题和公网问题

还可以绕过 DNS,直接测试本机 Nginx:

echo | openssl s_client \
  -connect 127.0.0.1:443 \
  -servername www.a.example.com \
  2>/dev/null \
  | openssl x509 \
  -noout \
  -subject \
  -ext subjectAltName

返回:

subject=CN = www.a.example.com
DNS:www.a.example.com

再测试公网:

echo | openssl s_client \
  -connect www.a.example.com:443 \
  -servername www.a.example.com \
  2>/dev/null \
  | openssl x509 \
  -noout \
  -subject \
  -ext subjectAltName

返回同样正确。

因此可以确认:

本机 Nginx    正常
公网 DNS      正常
SNI           正常
SSL 证书      正常

浏览器之前看到 b.example.cn 的证书,更可能是迁移过程中以下因素造成的临时现象:

  • DNS 尚未完全切换

  • 浏览器 DNS 缓存

  • 浏览器复用了旧 TLS 连接

  • Nginx reload 前后的旧连接

  • DNS TTL 尚未完全过期

重新建立连接后恢复正常。


十九、检查 IPv6 AAAA 记录

还有一个经常被忽略的问题。

执行:

dig +short A www.a.example.com
dig +short AAAA www.a.example.com

其中:

  • A 记录代表 IPv4

  • AAAA 记录代表 IPv6

有时候会出现:

A
→ 新服务器

AAAA
→ 旧服务器

很多浏览器会优先尝试 IPv6。

这就可能出现:

命令行测试正常
浏览器却访问旧服务器

本次:

dig +short AAAA www.a.example.com

没有返回结果,因此不存在 IPv6 解析干扰。


二十、Certbot 自动续期

证书成功后,还需要检查自动续期。

查看 Certbot 定时器:

sudo systemctl status certbot.timer

Ubuntu 使用 apt 安装 Certbot 后,一般已经自带:

certbot.timer

因此通常不需要自己再写 crontab

然后执行模拟续期:

sudo certbot renew --dry-run

如果看到类似:

Congratulations, all simulated renewals succeeded

说明自动续期机制正常。

查看当前服务器全部证书:

sudo certbot certificates

多系统服务器可能类似:

/etc/letsencrypt/live/
├── b.example.cn/
└── www.a.example.com/

每个域名单独管理自己的证书。


二十一、最终服务器架构

迁移完成后的整体结构如下:

                        Internet
                           │
                           ▼
                   43.153.18.224
                           │
                    Host Nginx
                     80 / 443
                    ┌──────┴──────┐
                    │             │
                    ▼             ▼
        b.example.cn   www.a.example.com
                    │             │
                    ▼             ▼
              H2A Docker     127.0.0.1:8080
                                  │
                                  ▼
                            WordPress Docker
                                  │
                                  ▼
                                 mysql
                                  │
                                  ▼
                             MySQL Docker

Certbot:

/etc/letsencrypt/live/
├── b.example.cn/
└── www.a.example.com/

Nginx:

/etc/nginx/conf.d/
├── h2esl.conf
└── a.example.conf

这种结构后续增加第三个、第四个系统也比较简单。


二十二、常用排查命令

查看域名 IPv4

dig +short A www.a.example.com

查看域名 IPv6

dig +short AAAA www.a.example.com

检查 Nginx 配置

sudo nginx -t

查看 Nginx 最终加载的完整配置

sudo nginx -T

快速检查所有 SSL 虚拟主机

sudo nginx -T | grep -n -E "server_name|listen 443|ssl_certificate"

检查域名实际返回的 SSL 证书

echo | openssl s_client \
  -connect www.a.example.com:443 \
  -servername www.a.example.com \
  2>/dev/null \
  | openssl x509 \
  -noout \
  -subject \
  -ext subjectAltName

绕过 DNS,直接检查本机 Nginx

echo | openssl s_client \
  -connect 127.0.0.1:443 \
  -servername www.a.example.com \
  2>/dev/null \
  | openssl x509 \
  -noout \
  -subject \
  -ext subjectAltName

查看 Certbot 证书

sudo certbot certificates

测试自动续期

sudo certbot renew --dry-run

二十三、SSL 问题推荐排查顺序

以后遇到以下问题:

  • 证书错误

  • 证书串站

  • 访问到了其他网站

  • Certbot 验证失败

  • 浏览器提示证书域名不匹配

不建议一上来就修改 Nginx。

推荐按照下面顺序排查。

1. 先查 DNS

dig +short A 域名
dig +short AAAA 域名

首先确认域名到底指向哪台服务器。

2. 再查 Nginx

sudo nginx -T | grep -n -E "server_name|listen 443|ssl_certificate"

检查:

域名
↓
server_name
↓
ssl_certificate

是否一一对应。

3. 检查本机 SNI

echo | openssl s_client \
  -connect 127.0.0.1:443 \
  -servername 域名 \
  2>/dev/null \
  | openssl x509 \
  -noout \
  -subject \
  -ext subjectAltName

确认本机 Nginx 是否选对证书。

4. 检查公网 SNI

echo | openssl s_client \
  -connect 域名:443 \
  -servername 域名 \
  2>/dev/null \
  | openssl x509 \
  -noout \
  -subject \
  -ext subjectAltName

确认公网实际返回什么证书。

通过这四步,基本就可以判断问题属于:

DNS
Nginx
SSL
IPv6
浏览器缓存

中的哪一层。


总结

这次 WordPress 迁移本身并不复杂,真正容易踩坑的是:

Docker
Nginx
DNS
HTTPS
WordPress

几个组件叠加后,一个问题往往会表现成完全不同的现象。

这次最典型的两个问题:

Certbot 申请失败

最终并不是 Certbot 本身的问题,而是:

DNS 仍然指向旧服务器。

WordPress 域名显示了另一个网站的 SSL 证书

最终验证发现:

Nginx 配置正确
证书正确
SNI 正确
DNS 正确

更可能是迁移过程中 DNS、浏览器缓存或旧 TLS 连接尚未完全刷新造成的临时现象。

对于一台运行多个网站的服务器,我更推荐采用:

宿主机 Nginx
+
每个项目独立 conf
+
Docker 业务容器
+
Docker 端口只绑定 127.0.0.1
+
Certbot 统一管理 HTTPS

这种方式结构清晰,也方便以后继续增加新的系统和域名。

而排查 HTTPS 问题时,有一条命令尤其值得保存:

echo | openssl s_client \
  -connect 域名:443 \
  -servername 域名 \
  2>/dev/null \
  | openssl x509 \
  -noout \
  -subject \
  -ext subjectAltName

相比浏览器提示,它能更直接地告诉我们:

当前服务器在 TLS 握手时,到底给这个域名返回了哪一张证书。

支持与分享

如果这篇文章对你有帮助,欢迎分享给更多人或者给予支持!

评论