Docker WordPress 建站指南:从零到生产级部署

51次阅读
一条评论

共计 25966 个字符,预计需要花费 65 分钟才能阅读完成。

本文完整记录使用 Docker Compose 编排 Nginx + WordPress + MariaDB + phpMyAdmin 的全过程。涵盖架构设计、容器编排、Nginx 多层配置、PHP 安全加固、MariaDB 调优、HTTP/3 支持等方方面面。所有配置文件均附完整代码与逐行解读。


内容索引

一、架构总览

1.1 全局架构图

Docker Wordpress 建站指南:从零到生产级部署

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 请求流转路径

Docker Wordpress 建站指南:从零到生产级部署

二、环境准备

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

此文件同时挂载到 WordPressphpMyAdmin 容器中,统一 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 中的 commandcustom.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.phpreadme.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 调优

设计哲学

  1. 最小暴露原则:只开放 80/443,数据库和管理工具不直接对外
  2. 纵深防御:TLS → 限流 → 路径拦截 → 应用加固 → PHP 函数禁用,层层设防
  3. 可观测性:详细日志格式(含 upstream 计时)、健康检查、慢查询日志、日志轮转
  4. 性能与安全的平衡:keepalive 连接池、HTTP/3、合理的限流阈值、InnoDB 调优
  5. 可维护性:配置模块化(snippet 复用)、YAML 锚点、环境变量分离、注释详尽

正文完
 0
评论(一条评论)
simos
2026-08-13 11:00:25 回复

很详实的建站文章,先收藏下,有啥疑问再问站长。

 Android  Firefox  英国