共计 25966 个字符,预计需要花费 65 分钟才能阅读完成。
本文完整记录使用 Docker Compose 编排 Nginx + WordPress + MariaDB + phpMyAdmin 的全过程。涵盖架构设计、容器编排、Nginx 多层配置、PHP 安全加固、MariaDB 调优、HTTP/3 支持等方方面面。所有配置文件均附完整代码与逐行解读。
一、架构总览
1.1 全局架构图

1.2 组件职责
| 层级 | 组件 | 镜像 | 职责 |
|---|---|---|---|
| 入口层 | Nginx | nginx:1.30.4-alpine |
TLS 终结、反向代理、限流、安全头、HTTP/3 |
| 应用层 | WordPress | wordpress:7.0.2-php8.4-apache |
博客业务逻辑 |
| 数据层 | MariaDB | mariadb:11.4 |
数据持久化 |
| 管理层 | phpMyAdmin | phpmyadmin:5.2 |
数据库可视化管理 |
1.3 网络隔离策略
本方案定义了三条 Docker 网络,这是整个安全架构的基石:
| 网络名 | 类型 | 成员 | 说明 |
|---|---|---|---|
web |
普通 | Nginx ↔ WordPress | 对外服务通道 |
backend |
internal | WordPress ↔ MariaDB ↔ phpMyAdmin | 数据库通道,无外网出口 |
admin |
普通 | Nginx ↔ phpMyAdmin | 管理通道 |
关键设计: backend 网络设置了 internal: true,这意味着该网络中的容器既无法主动访问外网,也无法被外网直接访问。即使 WordPress 被攻破,攻击者也无法从外部直接连接数据库端口。
1.4 请求流转路径

二、环境准备
2.1 服务器要求
- 系统: Linux(推荐 Ubuntu 22.04+ / Debian 12+)
- Docker: Engine 24+、Compose V2
- 内存: ≥ 2GB(WordPress + MariaDB)
- 端口: 80/tcp、443/tcp、443/udp(QUIC)
- 磁盘: ≥ 20GB(含数据库和日志空间)
2.2 安装 Docker
# 官方一键安装
curl -fsSL https://get.docker.com | sh
# 验证
docker --version
docker compose version
2.3 宿主机目录规划
在写任何配置之前,先规划好文件布局。所有配置和数据都集中在 /web 下,方便备份和迁移:
/web/
├── docker-compose.yml # 编排文件
├── .env # 环境变量(敏感信息)
├── nginx/
│ ├── nginx.conf # Nginx 主配置
│ ├── conf.d/ # 站点配置
│ │ ├── default.conf # 默认 server(拒绝未知请求)
│ │ ├── abc.com.conf # 主站配置
│ │ └── pma.abc.com.conf # phpMyAdmin 配置
│ ├── snippets/
│ │ └── proxy-wordpress.conf # 反向代理公共片段
│ ├── html/ # 静态文件(可选)
│ ├── htpasswd/ # 可选 Basic Auth 密码文件
│ └── log/ # Nginx 日志
├── wp/ # WordPress 文件(持久化)
├── php/
│ └── upload.ini # PHP 自定义配置
├── mysql/
│ ├── conf.d/ # MariaDB 自定义配置
│ │ └── custom.cnf
│ └── log/ # 数据库日志(慢查询等)
└── ssl/ # SSL 证书
├── fullchain.cer
└── abc.com.key
# 一次性创建所有目录
mkdir -p /web/{nginx/{conf.d,snippets,html,htpasswd,log},wp,php,mysql/{conf.d,log}}
2.4 SSL 证书申请(acme.sh)
使用 acme.sh 通过 DNS-01 方式申请证书,支持通配符:
# 安装 acme.sh
curl https://get.acme.sh | sh -s email=your@email.com
# 以 Cloudflare DNS 为例(需 API Token)
export CF_Token="your_cloudflare_api_token"
# 申请 ECC 证书(主域 + 通配符)
~/.acme.sh/acme.sh --issue --dns dns_cf -d abc.com -d "*.abc.com" -k ec-256
# 安装证书到指定路径 在整个服务部署运行后 还需要补上--reloadcmd参数 让 Nginx 在证书续期成功后自动加载新证书 详见13.1小结
~/.acme.sh/acme.sh --install-cert -d abc.com \
--key-file /web/ssl/abc.com.key \
--fullchain-file /web/ssl/fullchain.cer \
--ecc
注意: 通过 DNS-01 方式申请的证书,fullchain.cer 中可能不包含 OCSP Responder 的 URL(这在某些 CA 配置中很常见)。因此后文 Nginx 配置中 ssl_stapling 相关指令默认注释,详见第六节。
2.5 数据库日志目录权限
MariaDB 容器内 mysql 用户的默认 UID/GID 为 999。如果将日志目录挂载到宿主机,需要提前设置权限:
chown -R 999:999 /web/mysql/log
chmod 750 /web/mysql/log
三、Docker Compose 编排
这是整个项目的核心文件,定义了所有容器、网络、卷的关系。
3.1 完整 docker-compose.yml
name: wpsite
# ─── 日志策略(YAML 锚点,全局复用) ───
x-logging: &default-logging
driver: json-file
options:
max-size: "10m"
max-file: "3"
services:
# ═══════════════════════════════════════════════
# Nginx — 反向代理 / TLS 终结 / HTTP/3
# ═══════════════════════════════════════════════
nginx:
image: nginx:1.30.4-alpine
container_name: nginx
restart: unless-stopped
depends_on:
- wordpress
ports:
- "80:80/tcp"
- "443:443/tcp"
- "443:443/udp" # HTTP/3 QUIC,缺一不可
environment:
TZ: Asia/Shanghai
volumes:
# Nginx 配置(只读挂载)
- /web/nginx/nginx.conf:/etc/nginx/nginx.conf:ro
- /web/nginx/conf.d:/etc/nginx/conf.d:ro
- /web/nginx/snippets:/etc/nginx/snippets:ro
- /web/nginx/html:/usr/share/nginx/html:ro
# SSL 证书(只读挂载)
- /web/ssl/fullchain.cer:/etc/ssl/fullchain.cer:ro
- /web/ssl/abc.com.key:/etc/ssl/abc.com.key:ro
# Basic Auth 密码文件
- /web/nginx/htpasswd:/etc/nginx/htpasswd:ro
# 日志(可写)
- /web/nginx/log:/var/log/nginx
# 穿透到宿主机(适用于后端程序直接装在宿主机的场景)
extra_hosts:
- "host.docker.internal:host-gateway"
networks:
- web
- admin
logging: *default-logging
healthcheck:
test: ["CMD", "wget", "-qO-", "http://127.0.0.1/nginx-health"]
interval: 30s
timeout: 5s
retries: 3
start_period: 10s
# ═══════════════════════════════════════════════
# WordPress — 博客应用
# ═══════════════════════════════════════════════
wordpress:
image: wordpress:7.0.2-php8.4-apache
container_name: wp
restart: unless-stopped
depends_on:
mysql:
condition: service_healthy # 等数据库真正就绪
environment:
WORDPRESS_DB_HOST: mysql:3306
WORDPRESS_DB_NAME: ${WORDPRESS_DB_NAME}
WORDPRESS_DB_USER: ${WORDPRESS_DB_USER}
WORDPRESS_DB_PASSWORD: ${WORDPRESS_DB_PASSWORD}
TZ: Asia/Shanghai
# 注入 wp-config.php 额外配置
# 注意:如果 /web/wp/wp-config.php 已存在,以下配置不会覆盖
WORDPRESS_CONFIG_EXTRA: |
define('DISALLOW_FILE_EDIT', true);
define('WP_AUTO_UPDATE_CORE', 'minor');
define('WP_MEMORY_LIMIT', '256M');
define('WP_MAX_MEMORY_LIMIT', '512M');
volumes:
- /web/wp:/var/www/html
- /web/php/upload.ini:/usr/local/etc/php/conf.d/upload.ini:ro
networks:
- web
- backend
logging: *default-logging
# ═══════════════════════════════════════════════
# MariaDB — 数据库
# ═══════════════════════════════════════════════
mysql:
image: mariadb:11.4
container_name: wpdb
hostname: mysql
restart: unless-stopped
environment:
MARIADB_DATABASE: ${WORDPRESS_DB_NAME}
MARIADB_USER: ${WORDPRESS_DB_USER}
MARIADB_PASSWORD: ${WORDPRESS_DB_PASSWORD}
MARIADB_ROOT_PASSWORD: ${DB_ROOT_PASSWORD}
TZ: Asia/Shanghai
command:
- --character-set-server=utf8mb4
- --collation-server=utf8mb4_unicode_ci
- --skip-name-resolve
- --max_allowed_packet=256M
- --innodb_buffer_pool_size=256M
volumes:
- db_data:/var/lib/mysql
- /web/mysql/conf.d:/etc/mysql/conf.d:ro
- /web/mysql/log:/var/log/mysql
networks:
- backend # 仅 backend,不暴露到 web
logging: *default-logging
healthcheck:
# MariaDB 官方镜像自带 healthcheck.sh
test: ["CMD", "healthcheck.sh", "--connect", "--innodb_initialized"]
interval: 10s
timeout: 5s
retries: 10
start_period: 30s
# ═══════════════════════════════════════════════
# phpMyAdmin — 数据库管理
# ═══════════════════════════════════════════════
dbweb:
image: phpmyadmin:5.2
container_name: dbadmin
restart: unless-stopped
# 可选:使用 profiles 按需启动
# 启用:docker compose --profile admin up -d
# 停止:docker compose --profile admin stop
# profiles:
# - admin
depends_on:
mysql:
condition: service_healthy
environment:
PMA_HOST: mysql
PMA_PORT: "3306"
UPLOAD_LIMIT: 100M
TZ: Asia/Shanghai
# 应急通道:仅监听本机 8098,通过 SSH 隧道访问
ports:
- "127.0.0.1:8098:80"
volumes:
- /web/php/upload.ini:/usr/local/etc/php/conf.d/upload.ini:ro
networks:
- backend
- admin
logging: *default-logging
# ═══════════════════════════════════════════════
# 网络定义
# ═══════════════════════════════════════════════
networks:
web:
admin:
backend:
internal: true # 关键:backend 网络无外网出口
# ═══════════════════════════════════════════════
# 持久化卷
# ═══════════════════════════════════════════════
volumes:
db_data:
3.2 环境变量文件 .env
在 docker-compose.yml 同目录下创建 .env(切勿提交到 Git):
# /web/.env
WORDPRESS_DB_NAME=wordpress
WORDPRESS_DB_USER=wpuser
WORDPRESS_DB_PASSWORD=自己生成高强度数据库密码
DB_ROOT_PASSWORD=自己生成高强度数据库系统密码
3.3 关键设计逐条解读
① 日志轮转(YAML 锚点)
x-logging: &default-logging
driver: json-file
options:
max-size: "10m"
max-file: "3"
所有容器统一使用 json-file 驱动,每个日志文件最大 10MB,最多保留 3 个。防止日志无限增长撑爆磁盘。通过 YAML 锚点 &default-logging 定义一次,各服务用 *default-logging 引用。
② 健康检查与启动顺序
depends_on:
mysql:
condition: service_healthy
WordPress 和 phpMyAdmin 都不会在 MariaDB 真正就绪之前启动。MariaDB 的 healthcheck 使用官方自带的 healthcheck.sh,确认 InnoDB 初始化完成后才标记为 healthy。start_period: 30s 给数据库预留了 30 秒的启动宽限期。
③ 网络隔离
networks:
backend:
internal: true
internal: true 是安全核心。该网络中的容器:
- ❌ 无法主动访问外网
- ❌ 无法被外网直接访问
- ✅ 仅能通过网络内的容器名互相通信
④ phpMyAdmin 双通道访问
ports:
- "127.0.0.1:8098:80" # 应急通道
networks:
- backend
- admin # Nginx 反代通道
- 正常访问: 通过
pma.abc.com域名 → Nginx 反代 → phpMyAdmin - 应急访问: SSH 隧道到
127.0.0.1:8098,即使 Nginx 挂了也能管理数据库
# SSH 隧道示例
ssh -L 8098:127.0.0.1:8098 user@your-server
# 浏览器访问 http://localhost:8098
⑤ WordPress 安全常量注入
WORDPRESS_CONFIG_EXTRA: |
define('DISALLOW_FILE_EDIT', true);
define('WP_AUTO_UPDATE_CORE', 'minor');
define('WP_MEMORY_LIMIT', '256M');
define('WP_MAX_MEMORY_LIMIT', '512M');
| 常量 | 作用 |
|---|---|
DISALLOW_FILE_EDIT |
禁止后台编辑主题/插件文件,防止 webshell 注入 |
WP_AUTO_UPDATE_CORE |
仅自动更新小版本(安全补丁),大版本手动确认 |
WP_MEMORY_LIMIT |
前台内存上限 256M |
WP_MAX_MEMORY_LIMIT |
后台内存上限 512M |
⚠️ 如果
/web/wp/wp-config.php已存在(迁移场景),WORDPRESS_CONFIG_EXTRA不会覆盖已有文件,需手动添加这些常量。
四、Nginx 主配置
文件路径:/web/nginx/nginx.conf
这是 Nginx 的全局配置,定义了 worker、日志、TLS、限流、upstream 等基础参数。
user nginx;
worker_processes auto;
worker_rlimit_nofile 65535;
error_log /var/log/nginx/error.log warn;
pid /var/run/nginx.pid;
events {
worker_connections 4096;
multi_accept on;
use epoll;
}
http {
include /etc/nginx/mime.types;
default_type application/octet-stream;
# 隐藏 Nginx 版本号
server_tokens off;
server_names_hash_bucket_size 64;
# ─── 日志格式 ───
log_format main '$remote_addr - $remote_user [$time_local] "$request" '
'$status $body_bytes_sent "$http_referer" '
'"$http_user_agent" "$http_x_forwarded_for" '
'rt=$request_time uct="$upstream_connect_time" '
'uht="$upstream_header_time" urt="$upstream_response_time"';
access_log /var/log/nginx/access.log main;
# ─── 性能优化 ───
sendfile on;
tcp_nopush on;
tcp_nodelay on;
keepalive_timeout 65s;
keepalive_requests 1000;
# ─── 客户端限制 ───
client_max_body_size 100m;
client_body_buffer_size 128k;
client_body_timeout 30s;
client_header_timeout 30s;
send_timeout 30s;
# ─── TLS 全局配置 ───
ssl_protocols TLSv1.2 TLSv1.3;
ssl_ciphers ECDHE-ECDSA-AES256-GCM-SHA384:ECDHE-RSA-AES256-GCM-SHA384:ECDHE-ECDSA-AES128-GCM-SHA256:ECDHE-RSA-AES128-GCM-SHA256:ECDHE-ECDSA-CHACHA20-POLY1305:ECDHE-RSA-CHACHA20-POLY1305:TLS_AES_128_GCM_SHA256:TLS_AES_256_GCM_SHA384:TLS_CHACHA20_POLY1305_SHA256;
ssl_prefer_server_ciphers off;
ssl_session_cache shared:SSL:10m;
ssl_session_timeout 1d;
ssl_session_tickets off;
# ─── 限流区域定义 ───
limit_req_zone $binary_remote_addr zone=general:20m rate=50r/s;
limit_req_zone $binary_remote_addr zone=login:10m rate=10r/m;
limit_req_zone $binary_remote_addr zone=admin:10m rate=10r/m;
limit_conn_zone $binary_remote_addr zone=addr:20m;
limit_req_status 429;
limit_conn_status 429;
# ─── WebSocket / Upgrade 支持 ───
map $http_upgrade $connection_upgrade {
default upgrade;
'' "";
}
# ─── Docker 内置 DNS ───
# 本方案中 Nginx 通过容器名 wordpress / dbweb 访问后端
resolver 127.0.0.11 valid=10s ipv6=off;
resolver_timeout 5s;
# ─── Upstream 连接池 ───
# 在 Nginx 中,只有使用 upstream 块才能启用连接池,
# 只有写成静态配置(非变量)才能有长连接(keepalive)。
upstream wordpress_backend {
server wordpress:80 max_fails=3 fail_timeout=10s;
keepalive 32;
}
upstream phpmyadmin_backend {
server dbweb:80 max_fails=3 fail_timeout=10s;
keepalive 8;
}
# ─── 真实 IP(CDN/反代后使用,默认注释) ───
# 如果站点位于 Cloudflare、CDN 或负载均衡后面,需要获取真实 IP:
#
# set_real_ip_from 173.245.48.0/20;
# set_real_ip_from 103.21.244.0/22;
# real_ip_header CF-Connecting-IP;
#
# 或普通反向代理:
# real_ip_header X-Forwarded-For;
include /etc/nginx/conf.d/*.conf;
}
4.1 重点解读
日志格式中的 upstream 计时:
rt=$request_time uct="$upstream_connect_time"
uht="$upstream_header_time" urt="$upstream_response_time"
这四个时间指标能帮你精准定位性能瓶颈:
rt:Nginx 处理请求的总时间uct:与后端建立连接的时间(高 → 连接池不够或后端过载)uht:等待后端返回第一个字节的时间(高 → 后端处理慢)urt:后端完整响应时间
限流策略设计:
| 区域 | 速率 | 用途 |
|---|---|---|
general |
50 r/s | 全站通用 |
login |
10 r/min | WordPress 登录页(防暴力破解) |
admin |
10 r/min | 管理后台 |
addr(连接数) |
— | 并发连接限制 |
超限返回 429 Too Many Requests,而非默认的 503。
ssl_session_tickets off 的意义:
Session Ticket 使用一个长期密钥加密会话状态。如果该密钥泄露,攻击者可以解密历史流量,破坏前向保密性(Forward Secrecy)。关闭后改用 ssl_session_cache 服务端缓存,性能损失极小。
upstream keepalive:
upstream wordpress_backend {
server wordpress:80 max_fails=3 fail_timeout=10s;
keepalive 32;
}
keepalive 32:与 WordPress 后端保持最多 32 个长连接,避免每次请求都建立新 TCP 连接max_fails=3 fail_timeout=10s:连续失败 3 次后,10 秒内不再转发到该后端
⚠️ 使用 upstream keepalive 时,
proxy_pass必须写http://wordpress_backend(upstream 名称),不能写变量形式,否则 keepalive 不生效。
五、默认 Server 配置
文件路径:/web/nginx/conf.d/default.conf
这是安全兜底配置,处理所有未匹配到具体站点的请求。
# ─── HTTP 默认 server:未知 Host 一律拒绝 ───
server {
listen 80 default_server;
server_name _;
# Docker 健康检查端点
location = /nginx-health {
access_log off;
default_type text/plain;
return 200 "ok\n";
}
# 未知 Host 直接拒绝,避免开放跳转
location / {
return 403;
}
}
# ─── HTTPS 默认 server:未知 SNI 直接拒绝握手 ───
server {
listen 443 ssl default_server;
server_name _;
# Nginx >= 1.19.4
# 在 TLS 握手阶段就断开连接,不泄露任何证书信息
ssl_reject_handshake on;
}
# ─── 显式禁止通过公网 IP 访问 HTTP ───
server {
listen 80;
server_name 服务器公网IP; # 替换为你的服务器公网 IP
location / {
return 403;
}
}
5.1 为什么需要这个配置?
| 场景 | 没有 default.conf | 有 default.conf |
|---|---|---|
| IP 直接访问 | 返回默认站点内容 | 403 拒绝 |
| 恶意扫描器探测 | 泄露证书信息 | ssl_reject_handshake 直接断连 |
| 未配置的域名 | 可能命中错误的 server | 403 拒绝 |
| SEO 重复内容 | IP 和域名都能访问 | 仅域名可达 |
ssl_reject_handshake on 是 Nginx 1.19.4 引入的指令,它在 TLS ClientHello 阶段检查 SNI,如果不匹配任何 server_name,直接发送 unrecognized_name 告警并断开连接,不发送任何证书。
六、主站配置(abc.com)
文件路径:/web/nginx/conf.d/abc.com.conf
这是最核心、最复杂的配置文件,包含 HTTPS 跳转、安全头、限流、敏感路径拦截等。
# ═══════════════════════════════════════
# HTTP → HTTPS 跳转
# ═══════════════════════════════════════
server {
listen 80;
server_name abc.com www.abc.com;
location / {
return 301 https://abc.com$request_uri;
}
}
# ═══════════════════════════════════════
# www → 主域跳转(HTTPS)
# ═══════════════════════════════════════
server {
listen 443 ssl;
http2 on;
server_name www.abc.com;
ssl_certificate /web/ssl/fullchain.cer;
ssl_certificate_key /web/ssl/abc.com.key;
add_header Strict-Transport-Security "max-age=63072000; includeSubDomains; preload" always;
add_header X-Content-Type-Options "nosniff" always;
add_header Referrer-Policy "strict-origin-when-cross-origin" always;
location / {
return 301 https://abc.com$request_uri;
}
}
# ═══════════════════════════════════════
# 主站:abc.com
# ═══════════════════════════════════════
server {
listen 443 ssl;
# HTTP/3 / QUIC
# 如果 nginx -V 不支持 HTTP/3,请删除或注释下面这行
listen 443 quic reuseport;
http2 on;
server_name abc.com;
ssl_certificate /web/ssl/fullchain.cer;
ssl_certificate_key /web/ssl/abc.com.key;
# ─── OCSP Stapling(本方案注释) ───
# 通过 acme.sh DNS-01 方式申请的证书,fullchain.cer 中可能
# 不包含 OCSP Responder 的 URL(某些 CA 配置中很常见),
# 而且额外的 resolver 还可能干扰 Docker 内置 DNS 解析。
#
# ssl_stapling on;
# ssl_stapling_verify on;
# ssl_trusted_certificate /etc/ssl/fullchain.cer;
# resolver 1.1.1.1 8.8.8.8 valid=300s;
# resolver_timeout 5s;
# ─── 安全响应头 ───
add_header Strict-Transport-Security "max-age=63072000; includeSubDomains; preload" always;
add_header X-Content-Type-Options "nosniff" always;
add_header X-Frame-Options "SAMEORIGIN" always;
add_header Referrer-Policy "strict-origin-when-cross-origin" always;
add_header Permissions-Policy "geolocation=(), microphone=(), camera=()" always;
add_header Alt-Svc 'h3=":443"; ma=86400' always;
# ─── CSP(建议先测试再强制启用) ───
# WordPress 插件/主题经常需要 inline script/style,所以默认不强制。
#
# add_header Content-Security-Policy-Report-Only "default-src 'self'; \
# script-src 'self' 'unsafe-inline' 'unsafe-eval'; \
# style-src 'self' 'unsafe-inline'; \
# img-src 'self' data: https:; \
# font-src 'self' data:; \
# connect-src 'self'; \
# frame-ancestors 'self'; \
# object-src 'none'; \
# base-uri 'self'; \
# form-action 'self'; \
# upgrade-insecure-requests" always;
# ─── 上传限制(需与 PHP upload.ini 一致) ───
client_max_body_size 100m;
# ─── 健康检查 ───
location = /nginx-health {
access_log off;
default_type text/plain;
return 200 "ok\n";
}
# ─── 禁止 XML-RPC ───
# WordPress 最大攻击面之一。如果你明确需要 Jetpack/XML-RPC,
# 请不要直接全网开放,应限制来源 IP。
location = /xmlrpc.php {
return 444;
}
# ─── 禁止访问 WordPress 敏感文件 ───
location ~* /(wp-config\.php|readme\.html|license\.txt|wp-config-sample\.php)$ {
return 404;
}
# ─── 禁止访问隐藏文件和敏感后缀 ───
location ~* /\.(env|git|svn|htaccess|htpasswd|ini|log|sql|bak|old|save|swp)$ {
return 404;
}
location ~ /\. {
return 404;
}
# ─── 禁止 uploads 目录执行 PHP(防 webshell) ───
location ~* /wp-content/uploads/.*\.php$ {
return 403;
}
# ─── WordPress 登录限流 ───
location = /wp-login.php {
limit_req zone=login burst=5 nodelay;
limit_conn addr 8;
include /etc/nginx/snippets/proxy-wordpress.conf;
}
# ─── admin-ajax 限流 ───
location = /wp-admin/admin-ajax.php {
limit_req zone=general burst=50 nodelay;
limit_conn addr 30;
include /etc/nginx/snippets/proxy-wordpress.conf;
}
# ─── 主站请求 ───
location / {
limit_req zone=general burst=100 nodelay;
limit_conn addr 100;
include /etc/nginx/snippets/proxy-wordpress.conf;
}
}
6.1 安全响应头逐条说明
| 响应头 | 值 | 作用 |
|---|---|---|
Strict-Transport-Security |
max-age=63072000; includeSubDomains; preload |
强制浏览器 2 年内只用 HTTPS,含子域名,可申请 preload 列表 |
X-Content-Type-Options |
nosniff |
禁止浏览器猜测 MIME 类型,防 MIME 嗅探攻击 |
X-Frame-Options |
SAMEORIGIN |
仅允许同源 iframe,防点击劫持 |
Referrer-Policy |
strict-origin-when-cross-origin |
跨域时不发送完整 URL,保护隐私 |
Permissions-Policy |
geolocation=(), microphone=(), camera=() |
禁用地理位置、麦克风、摄像头 API |
Alt-Svc |
h3=":443"; ma=86400 |
通告 HTTP/3 可用性 |
6.2 敏感路径拦截一览
/xmlrpc.php → 444(直接断连,无响应体)
/wp-config.php → 404
/readme.html → 404
/license.txt → 404
/wp-config-sample.php → 404
/.env / .git / .htaccess / *.log ... → 404
/wp-content/uploads/*.php → 403(防上传目录执行脚本)
6.3 限流参数解读
# 登录页:每分钟 10 次,突发允许额外 5 次(不排队)
location = /wp-login.php {
limit_req zone=login burst=5 nodelay;
limit_conn addr 8;
}
rate=10r/m:稳态每分钟 10 个请求burst=5:允许瞬间突发 5 个请求(令牌桶)nodelay:突发请求不排队,直接处理(超限才拒绝)limit_conn addr 8:单个 IP 最多 8 个并发连接
暴力破解工具通常每秒发送数十次请求,会在第 16 次(10+5+1)时开始收到 429。
七、反向代理公共配置
文件路径:/web/nginx/snippets/proxy-wordpress.conf
将反向代理参数抽成独立 snippet,多个 location 复用,修改一处全局生效。
# WordPress 反向代理公共配置
proxy_pass http://wordpress_backend;
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;
# ─── WebSocket 支持(条件判断,避免对所有请求设置) ───
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection $connection_upgrade;
proxy_redirect off;
# ─── 代理超时 ───
proxy_connect_timeout 10s;
proxy_send_timeout 60s;
proxy_read_timeout 60s;
# ─── 缓冲区 ───
proxy_buffering on;
proxy_request_buffering on;
proxy_buffer_size 16k;
proxy_buffers 8 16k;
proxy_busy_buffers_size 32k;
7.1 关键参数说明
| 参数 | 说明 |
|---|---|
proxy_http_version 1.1 |
upstream keepalive 必须使用 HTTP/1.1 |
X-Forwarded-Proto |
让 WordPress 知道原始请求是 HTTPS(影响 is_ssl() 判断) |
Upgrade + Connection |
配合 nginx.conf 中的 map 指令,仅在客户端发送 Upgrade 头时才设置 |
proxy_buffering on |
Nginx 先缓存后端响应再发给客户端,保护后端慢连接 |
proxy_request_buffering on |
Nginx 先接收完客户端请求体再转发,防止慢客户端占用后端 |
八、phpMyAdmin 配置(pma.abc.com)
文件路径:/web/nginx/conf.d/pma.abc.com.conf
为 phpMyAdmin 提供独立的子域名访问入口,并施加额外的安全限制。
# ─── HTTP 跳转到 HTTPS ───
server {
listen 80;
server_name pma.abc.com;
location / {
return 301 https://pma.abc.com$request_uri;
}
}
# ─── phpMyAdmin HTTPS ───
server {
# Nginx 底层处理 UDP (QUIC) 端口的技术机制决定了:
# 只要主站 abc.com 开启了 HTTP/3(listen 443 quic reuseport),
# pma.abc.com 默认也就自动支持 HTTP/3 了,完全不需要在这里重复写一遍
# 而且 UDP 端口 (reuseport) 只能声明一次。
listen 443 ssl;
http2 on;
server_name pma.abc.com;
# 注意:证书必须包含 pma.abc.com(通配符 *.abc.com 已覆盖)
ssl_certificate /web/ssl/fullchain.cer;
ssl_certificate_key /web/ssl/abc.com.key;
# 管理后台不要被搜索引擎收录
add_header X-Robots-Tag "noindex, nofollow" always;
add_header X-Content-Type-Options "nosniff" always;
# 管理界面禁止被 iframe 嵌入(比主站更严格:DENY)
add_header X-Frame-Options "DENY" always;
# 不发送 Referer
add_header Referrer-Policy "no-referrer" always;
add_header Strict-Transport-Security "max-age=63072000" always;
add_header Alt-Svc 'h3=":443"; ma=86400' always; # 通告 HTTP/3 server 的可用性
# ─── 访问控制(按需启用) ───
# 默认只允许本机/容器内部。
# 如需远程管理,请添加你的固定出口 IP:
# allow 127.0.0.1;
# allow 203.0.113.10;
# deny all;
# 如果启用了 Basic Auth,则同时要求 IP 白名单 + Basic Auth:
# 创建密码文件:
# mkdir -p /web/nginx/htpasswd
# htpasswd -c /web/nginx/htpasswd/dbadmin admin
#
# auth_basic "DB Admin";
# auth_basic_user_file /etc/nginx/htpasswd/dbadmin;
client_max_body_size 100m;
location / {
# ⚠️ 注意:不要对 phpMyAdmin 启用 limit_req!
# limit_req zone=admin burst=5 nodelay;
# phpMyAdmin 页面会加载大量静态资源(JS/CSS),
# 限流会拦截这些请求,导致页面空白。
# 仅保留连接数限制即可。
limit_conn addr 5;
proxy_pass http://phpmyadmin_backend;
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;
# WebSocket 支持
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection $connection_upgrade;
proxy_redirect off;
proxy_connect_timeout 10s;
proxy_send_timeout 60s;
proxy_read_timeout 60s;
}
# 禁止访问隐藏文件
location ~ /\. {
return 404;
}
}
8.1 踩坑记录:limit_req 导致空白页
# ❌ 错误:对 phpMyAdmin 启用请求限流
limit_req zone=admin burst=5 nodelay;
# ✅ 正确:仅限制并发连接数
limit_conn addr 5;
phpMyAdmin 首次加载时会并发请求十几个 JS/CSS 文件。如果启用 limit_req(10r/m),这些静态资源请求会被大量 429 拒绝,导致页面完全空白。
8.2 HTTP/3 的 reuseport 只需声明一次
# 主站(abc.com.conf)中:
listen 443 quic reuseport; # ← 唯一声明处
# 子站(pma.abc.com.conf)中:
listen 443 ssl; # ← 不需要再写 quic reuseport
Nginx 的 QUIC 监听是端口级别的。reuseport 只能在第一个绑定该端口的 server 块中声明。只要主站开启了 443 quic,同一 Nginx 实例上的所有 HTTPS 站点自动获得 HTTP/3 支持(通过 Alt-Svc 头通告)。
8.3 安全加固选项
phpMyAdmin 是高危管理入口,建议至少启用以下一项额外保护:
方案 A:IP 白名单
allow 127.0.0.1;
allow 203.0.113.10; # 你的固定出口 IP
deny all;
方案 B:Basic Auth
# 创建密码文件
mkdir -p /web/nginx/htpasswd
htpasswd -c /web/nginx/htpasswd/dbadmin admin
auth_basic "DB Admin";
auth_basic_user_file /etc/nginx/htpasswd/dbadmin;
方案 C:A + B 叠加(推荐)
九、PHP 自定义配置
文件路径:/web/php/upload.ini
此文件同时挂载到 WordPress 和 phpMyAdmin 容器中,统一 PHP 运行时行为。
# ═══════════════════════════════════════
# 上传大小(需与 Nginx client_max_body_size、WordPress 需求一致)
# ═══════════════════════════════════════
file_uploads = On
upload_max_filesize = 100M
post_max_size = 100M
max_file_uploads = 20
# ═══════════════════════════════════════
# 执行时间和内存
# ═══════════════════════════════════════
memory_limit = 256M
max_execution_time = 600
max_input_time = 120
# ═══════════════════════════════════════
# 禁用危险函数(根据实际需求调整)
# ═══════════════════════════════════════
disable_functions = exec,passthru,shell_exec,system,proc_open,popen,curl_exec,curl_multi_exec,parse_ini_file,show_source
# ═══════════════════════════════════════
# 安全选项
# ═══════════════════════════════════════
allow_url_fopen = Off
allow_url_include = Off
9.1 上传链路一致性
上传大文件时,请求经过三层限制,任何一层不满足都会失败:
客户端 → Nginx (client_max_body_size 100m)
→ PHP (post_max_size 100M / upload_max_filesize 100M)
→ WordPress (WP_MEMORY_LIMIT 等)
三处必须对齐,取最小值生效:
| 层级 | 配置项 | 本方案值 |
|---|---|---|
| Nginx | client_max_body_size |
100m |
| PHP | upload_max_filesize |
100M |
| PHP | post_max_size |
100M |
| phpMyAdmin | UPLOAD_LIMIT(环境变量) |
100M |
9.2 危险函数禁用说明
disable_functions = exec,passthru,shell_exec,system,proc_open,popen,curl_exec,curl_multi_exec,parse_ini_file,show_source
| 函数 | 风险 |
|---|---|
exec / system / passthru / shell_exec |
执行系统命令 |
proc_open / popen |
打开进程 |
curl_exec / curl_multi_exec |
可能被利用发起 SSRF |
parse_ini_file |
读取任意配置文件 |
show_source |
泄露源码 |
⚠️ 注意: 某些 WordPress 插件可能依赖
curl_exec(如远程图片下载、API 调用)。如果禁用后插件报错,可酌情移除curl_exec,curl_multi_exec。
9.3 allow_url_fopen = Off 的影响
关闭后,PHP 的 fopen()、file_get_contents() 等函数无法直接读取远程 URL。这能有效防止:
- 远程文件包含(RFI)
- SSRF 攻击
但部分插件可能需要此功能。如遇兼容性问题,可改为 On 并配合 allow_url_include = Off(后者必须保持 Off)。
9.4 在 docker-compose.yml 中的挂载方式
# WordPress 容器
wordpress:
volumes:
- /web/php/upload.ini:/usr/local/etc/php/conf.d/upload.ini:ro
# phpMyAdmin 容器
dbweb:
volumes:
- /web/php/upload.ini:/usr/local/etc/php/conf.d/upload.ini:ro
PHP 官方镜像会自动加载 /usr/local/etc/php/conf.d/ 目录下的所有 .ini 文件,无需修改 php.ini 主文件。
十、MariaDB 自定义配置
文件路径:/web/mysql/conf.d/custom.cnf
此文件挂载到 MariaDB 容器的 /etc/mysql/conf.d/ 目录,容器启动时自动加载。
[mysqld]
# ─── 基础字符集 ───
character-set-server = utf8mb4
collation-server = utf8mb4_unicode_ci
# ─── 上传/导入相关 ───
max_allowed_packet = 256M
# ─── InnoDB 优化(根据服务器内存调整) ───
innodb_buffer_pool_size = 256M
innodb_log_file_size = 64M
innodb_flush_log_at_trx_commit = 2
# ─── 连接 ───
max_connections = 100
wait_timeout = 600
interactive_timeout = 600
# ─── 日志 ───
# 开启自定义日志路径,需要在宿主机执行以下命令,
# 将日志目录的所有权赋予容器内的 mysql 用户(UID 999):
# chown -R 999:999 /web/mysql/log
# chmod 750 /web/mysql/log
# 错误日志不需要强制写入文件,直接让它输出到控制台。
# 排查启动报错、连接断开等问题时,直接敲:
# docker logs wpdb
# docker logs --tail 50 wpdb
# 反而更直观、更方便。
# log_error = /var/log/mysql/error.log
# 慢查询日志
slow_query_log = 1
slow_query_log_file = /var/log/mysql/slow.log
long_query_time = 2
# 通用查询日志(不建议生产长期开启,仅排障时使用)
# general_log = 1
# general_log_file = /var/log/mysql/general.log
10.1 InnoDB 调优详解
| 参数 | 值 | 说明 |
|---|---|---|
innodb_buffer_pool_size |
256M | InnoDB 缓冲池,缓存数据和索引。设为可用内存的 50%~70%(2GB 内存服务器建议 256M~512M) |
innodb_log_file_size |
64M | redo log 大小。太小会频繁刷盘,太大影响崩溃恢复速度 |
innodb_flush_log_at_trx_commit |
2 | 性能与安全的权衡点(见下表) |
innodb_flush_log_at_trx_commit 取值对比:
| 值 | 行为 | 安全性 | 性能 |
|---|---|---|---|
| 0 | 每秒刷一次 log + flush | ❌ 崩溃丢 1 秒数据 | 最快 |
| 1 | 每次事务都刷 log + flush | ✅ 最安全(ACID) | 最慢 |
| 2 | 每次事务刷 log,每秒 flush | ⚠️ OS 崩溃可能丢 1 秒 | 推荐 |
对于博客场景,2 是最佳选择:MySQL 崩溃不丢数据,仅操作系统级崩溃(断电)可能丢最近 1 秒的事务。
10.2 连接参数
max_connections = 100
wait_timeout = 600
interactive_timeout = 600
max_connections = 100:WordPress + phpMyAdmin 的并发连接远达不到 100,足够用wait_timeout / interactive_timeout = 600:空闲连接 10 分钟后自动断开,防止连接泄漏
10.3 慢查询日志
slow_query_log = 1
slow_query_log_file = /var/log/mysql/slow.log
long_query_time = 2
超过 2 秒的查询会被记录到 /var/log/mysql/slow.log(映射到宿主机 /web/mysql/log/slow.log)。
查看慢查询:
# 实时跟踪
docker exec wpdb tail -f /var/log/mysql/slow.log
# 或直接看宿主机文件
tail -100 /web/mysql/log/slow.log
10.4 日志目录权限(重要)
MariaDB 容器内 mysql 用户的 UID/GID 为 999。如果宿主机日志目录权限不对,容器会启动失败:
chown -R 999:999 /web/mysql/log
chmod 750 /web/mysql/log
10.5 与 docker-compose.yml command 的关系
docker-compose.yml 中的 command 和 custom.cnf 都能设置参数。优先级:command > 配置文件。
本方案中 command 设置了基础参数(字符集、buffer pool),custom.cnf 提供更精细的调优。两者不冲突,command 中的参数会覆盖配置文件中的同名参数。
十一、安全防护体系总结
将前述所有安全措施汇总为一张纵深防御矩阵:
11.1 网络层
| 措施 | 实现方式 |
|---|---|
| 数据库不暴露公网 | backend 网络 internal: true |
| phpMyAdmin 不暴露公网 | 仅 127.0.0.1:8098 + Nginx 反代 |
| 默认拒绝未知请求 | default.conf 返回 403 |
| 未知 SNI 拒绝握手 | ssl_reject_handshake on |
| IP 直接访问拒绝 | 显式 server 返回 403 |
11.2 传输层
| 措施 | 实现方式 |
|---|---|
| 仅 TLS 1.2 / 1.3 | ssl_protocols |
| 强密码套件(ECDHE + AEAD) | ssl_ciphers |
| 前向保密 | ssl_session_tickets off |
| HSTS(2年 + preload) | Strict-Transport-Security |
| HTTP → HTTPS 强制跳转 | 301 redirect |
11.3 应用层(Nginx 拦截)
| 措施 | 实现方式 |
|---|---|
| 关闭 XML-RPC | location = /xmlrpc.php { return 444; } |
| 禁止敏感文件访问 | wp-config.php、readme.html 等返回 404 |
| 禁止上传目录执行 PHP | uploads/*.php 返回 403 |
| 禁止隐藏文件 | location ~ /\. 返回 404 |
| 登录暴力破解防护 | limit_req zone=login 10r/m |
| 全站限流 | limit_req zone=general 50r/s |
11.4 应用层(WordPress)
| 措施 | 实现方式 |
|---|---|
| 禁止后台文件编辑 | DISALLOW_FILE_EDIT = true |
| 仅自动更新小版本 | WP_AUTO_UPDATE_CORE = 'minor' |
11.5 运行时层(PHP)
| 措施 | 实现方式 |
|---|---|
| 禁用危险函数 | disable_functions |
| 禁止远程文件包含 | allow_url_include = Off |
| 禁止远程文件打开 | allow_url_fopen = Off(可选) |
11.6 安全响应头
| 响应头 | 主站 | phpMyAdmin |
|---|---|---|
| HSTS | ✅ 2年 + preload | ✅ 2年 |
| X-Content-Type-Options | ✅ nosniff | ✅ nosniff |
| X-Frame-Options | ✅ SAMEORIGIN | ✅ DENY(更严格) |
| Referrer-Policy | ✅ strict-origin | ✅ no-referrer(更严格) |
| Permissions-Policy | ✅ | — |
| X-Robots-Tag | — | ✅ noindex, nofollow |
| Alt-Svc (HTTP/3) | ✅ | 自动继承 |
十二、HTTP/3 与 QUIC
12.1 什么是 HTTP/3?
HTTP/3 基于 QUIC 协议(运行在 UDP 之上),相比 HTTP/2(基于 TCP):
| 特性 | HTTP/2 (TCP) | HTTP/3 (QUIC/UDP) |
|---|---|---|
| 连接建立 | 1-RTT(TLS 握手) | 0-RTT(复用连接) |
| 队头阻塞 | TCP 层存在 | 完全消除 |
| 连接迁移 | IP 变化需重连 | Connection ID 无缝迁移 |
| 加密 | TLS 1.2/1.3 | 内置 TLS 1.3 |
12.2 启用步骤
① 确认 Nginx 支持 QUIC:
docker exec nginx nginx -V 2>&1 | grep -o 'with-http_v3_module'
# 输出:with-http_v3_module
nginx:1.30.4-alpine官方镜像已内置 HTTP/3 模块。
② 主站配置中监听 QUIC:
listen 443 quic reuseport;
quic:启用 QUIC 协议reuseport:多 worker 共享 UDP 端口,提升并发
③ 通告 HTTP/3 可用性:
add_header Alt-Svc 'h3=":443"; ma=86400' always;
浏览器收到此头后,下次访问将尝试 HTTP/3。ma=86400 表示声明有效 24 小时。
④ Docker Compose 开放 UDP 端口:
ports:
- "443:443/tcp"
- "443:443/udp" # ← 这行不能少!
⑤ 防火墙放行 UDP 443:
# UFW
sudo ufw allow 443/udp
# 或 iptables
sudo iptables -A INPUT -p udp --dport 443 -j ACCEPT
12.3 子站自动继承 HTTP/3
如第八节所述,reuseport 只能声明一次。主站 abc.com 声明后,pma.abc.com 等所有同实例站点自动获得 HTTP/3 支持,无需重复配置。
12.4 验证
# curl 验证(需 HTTP/3 支持)
curl --http3 -I https://pma.abc.com
# 在线检测
# https://http3check.net/?test=pma.abc.com
响应中应包含:
alt-svc: h3=":443"; ma=86400
十三、部署与验证
13.1 启动
cd /opt
# 首次启动
docker compose up -d
# 查看状态
docker compose ps
# 查看各服务日志
docker compose logs -f nginx
docker compose logs -f wordpress
docker compose logs -f mysql
# 再次安装一遍证书到指定路径 补上--reloadcmd参数 让 Nginx 在证书续期成功后自动加载新证书。
~/.acme.sh/acme.sh --install-cert -d abc.com \
--key-file /web/ssl/abc.com.key \
--fullchain-file /web/ssl/fullchain.cer \
--ecc \
--reloadcmd "docker exec nginx nginx -s reload"
13.2 验证清单
# 1. Nginx 健康检查
curl http://127.0.0.1/nginx-health
# 期望:ok
# 2. HTTPS 正常访问
curl -I https://abc.com
# 期望:200 + 安全响应头
# 3. HTTP 自动跳转
curl -I http://abc.com
# 期望:301 → https://abc.com
# 4. www 跳转到主域
curl -I https://www.abc.com
# 期望:301 → https://abc.com
# 5. IP 直接访问被拒
curl -I http://服务器公网IP
# 期望:403
# 6. XML-RPC 被拦截
curl -I https://abc.com/xmlrpc.php
# 期望:无响应(444 直接断连)
# 7. 敏感文件被拦截
curl -I https://abc.com/wp-config.php
# 期望:404
curl -I https://abc.com/readme.html
# 期望:404
# 8. 登录限流测试
for i in $(seq 1 20); do
curl -s -o /dev/null -w "%{http_code} " https://abc.com/wp-login.php
done
echo
# 期望:前 ~15 次 200,之后出现 429
# 9. HTTP/3 验证
curl --http3 -I https://abc.com
# 期望:alt-svc: h3=":443"; ma=86400
# 10. phpMyAdmin 访问
curl -I https://pma.abc.com
# 期望:200(或 Basic Auth 401)
13.3 日常运维
# 修改 Nginx 配置后重载
docker exec nginx nginx -t && docker exec nginx nginx -s reload
# 更新 WordPress 镜像
docker compose pull wordpress
docker compose up -d wordpress
# 数据库备份 也能通过 pma.abc.com 进行操作
docker exec wpdb mariadb-dump -u root -p"$DB_ROOT_PASSWORD" \
wordpress > backup_$(date +%F).sql
# 查看容器资源占用
docker stats --no-stream
# 查看慢查询
tail -50 /web/mysql/log/slow.log
# 查看 Nginx 访问日志(含 upstream 计时)
tail -f /web/nginx/log/access.log
# 查看 Nginx 错误日志
tail -f /web/nginx/log/error.log
十四、总结
完整配置文件清单
| 文件 | 宿主机路径 | 用途 |
|---|---|---|
docker-compose.yml |
/web/docker-compose.yml |
容器编排 |
.env |
/web/.env |
敏感环境变量 |
nginx.conf |
/web/nginx/nginx.conf |
Nginx 全局配置 |
default.conf |
/web/nginx/conf.d/default.conf |
默认 server / 安全兜底 |
abc.com.conf |
/web/nginx/conf.d/abc.com.conf |
主站配置 |
pma.abc.com.conf |
/web/nginx/conf.d/pma.abc.com.conf |
phpMyAdmin 配置 |
proxy-wordpress.conf |
/web/nginx/snippets/proxy-wordpress.conf |
反向代理片段 |
upload.ini |
/web/php/upload.ini |
PHP 运行时配置 |
custom.cnf |
/web/mysql/conf.d/custom.cnf |
MariaDB 调优 |
设计哲学
- 最小暴露原则:只开放 80/443,数据库和管理工具不直接对外
- 纵深防御:TLS → 限流 → 路径拦截 → 应用加固 → PHP 函数禁用,层层设防
- 可观测性:详细日志格式(含 upstream 计时)、健康检查、慢查询日志、日志轮转
- 性能与安全的平衡:keepalive 连接池、HTTP/3、合理的限流阈值、InnoDB 调优
- 可维护性:配置模块化(snippet 复用)、YAML 锚点、环境变量分离、注释详尽