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 是什么意思
-i 是 docker 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 DockerWordPress 容器实际收到的是 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.confHTTP 配置:
# 裸域名 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.com和www.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.comNginx 根据这个域名匹配:
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记录代表 IPv4AAAA记录代表 IPv6
有时候会出现:
A
→ 新服务器
AAAA
→ 旧服务器很多浏览器会优先尝试 IPv6。
这就可能出现:
命令行测试正常
浏览器却访问旧服务器本次:
dig +short AAAA www.a.example.com没有返回结果,因此不存在 IPv6 解析干扰。
二十、Certbot 自动续期
证书成功后,还需要检查自动续期。
查看 Certbot 定时器:
sudo systemctl status certbot.timerUbuntu 使用 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 DockerCertbot:
/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 握手时,到底给这个域名返回了哪一张证书。
