建站问答(六):WordPress Apache 镜像 vs FPM 镜像深度对比与迁移指南

34次阅读
没有评论

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

一、两种架构的本质区别

1.1 wordpress:*-apache(当前方案)

客户端 → Nginx (反代) → WordPress 容器内 Apache (mod_php) → PHP 处理
                              │
                              └── Apache 既是 Web 服务器,又是 PHP 运行环境
                                  PHP 作为 Apache 模块嵌入进程内部
  • 单进程模型: Apache + mod_php 合为一体
  • 请求处理: 每个 Apache worker 进程都加载完整的 PHP 解释器
  • 配置简单: 一个容器搞定一切,无需额外组件

1.2 wordpress:*-fpm

客户端 → Nginx (反代) → WordPress 容器内 Nginx/FPM → php-fpm 进程池 → PHP 处理
                              │                              │
                              └── 容器内仍需一个 Web 服务器    └── 独立的 PHP 进程池
                                  (通常是 Nginx 或 Apache)        通过 Unix Socket / TCP 通信

⚠️ 关键认知纠正: wordpress:*-fpm 镜像不包含任何 Web 服务器。它只包含 PHP-FPM 和 WordPress 文件。你必须在同一个容器中再装一个 Web 服务器(Nginx/Apache),或者用外部 Nginx 直接连 FPM——但后者需要共享文件系统,复杂度更高。

实际上,Docker 官方 wordpress:*-fpm 镜像的设计意图是:搭配一个独立的 Nginx 容器使用,两个容器通过 volume 共享 WordPress 文件,通过 TCP/Socket 传递 PHP 请求。

┌─────────────────┐         ┌──────────────────────┐
│   Nginx 容器    │  TCP    │   WordPress-FPM 容器  │
│                 │◄───────►│                      │
│  静态文件 ←─────│  volume  │  /var/www/html       │
│  PHP 请求 ──────│────────►│  php-fpm :9000       │
└─────────────────┘         └──────────────────────┘

二、性能对比

2.1 内存占用

指标 Apache (mod_php) FPM (Nginx + php-fpm)
空闲内存 ~80-120MB ~40-60MB
每并发请求内存 ~30-50MB(Apache worker + PHP) ~15-25MB(仅 FPM worker)
静态文件请求 经过 Apache + mod_php(浪费) Nginx 直接返回(零 PHP 开销)
100 并发峰值 ~3-5GB ~1.5-2.5GB

FPM 优势的核心原因: 静态资源(CSS/JS/图片)由 Nginx 直接处理,完全不经过 PHP。而 Apache mod_php 模式下,即使返回一张图片,也要启动一个携带完整 PHP 解释器的 Apache worker。

2.2 并发能力

指标 Apache (prefork) FPM
最大并发 受限于 Apache MaxRequestWorkers Nginx 异步 + FPM 进程池独立扩展
静态文件并发 与 PHP 请求竞争 worker 互不影响
长连接保持 每个连接占一个 worker Nginx 单线程处理数千连接
突发流量 worker 耗尽 → 排队/拒绝 Nginx 缓冲 + FPM 队列

2.3 实际场景判断

场景 推荐 原因
个人博客,日均 PV < 5000 Apache ✅ 简单够用,差异不可感知
中型站点,日均 PV 5000-50000 FPM ✅ 内存节省明显,静态资源快
高流量/多站点 FPM ✅✅ 并发能力和资源效率优势巨大
服务器内存 ≤ 1GB FPM ✅ Apache mod_php 可能 OOM
追求最低运维复杂度 Apache ✅ 少一个组件,少一套配置

对于本方案(个人博客): Apache 镜像完全胜任。FPM 的性能优势在小流量下几乎不可感知,但配置复杂度显著增加。选择取决于你的优先级:简单 vs 极致性能。


三、如果选用 FPM,需要改什么?

3.1 变更总览

组件 Apache 方案(当前) FPM 方案
WordPress 镜像 wordpress:7.0.2-php8.4-apache wordpress:7.0.2-php8.4-fpm
容器内 Web 服务器 Apache(内置) ❌ 无(需外部 Nginx 处理)
Nginx 反代目标 proxy_pass http://wordpress:80 fastcgi_pass wordpress:9000
静态文件 由 WordPress 容器的 Apache 提供 由 Nginx 容器直接读取共享 volume
配置文件 proxy-wordpress.conf fastcgi-wordpress.conf(全新)
Volume 共享 不需要 Nginx 和 FPM 容器必须共享 /var/www/html
PHP 配置挂载 挂到 WordPress 容器 挂到 WordPress-FPM 容器(不变)

3.2 docker-compose.yml 变更

name: wpsite

x-logging: &default-logging
  driver: json-file
  options:
    max-size: "10m"
    max-file: "3"

services:

  # ═══════════════════════════════════════════
  #  Nginx — 现在同时承担反代 + 静态文件服务
  # ═══════════════════════════════════════════
  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"
    environment:
      TZ: Asia/Shanghai
    volumes:
      - /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
      # ★ 关键变更:Nginx 需要直接读取 WordPress 文件来服务静态资源
      - /web/wp:/var/www/html:ro
      - /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 FPM — 不再包含 Web 服务器
  # ═══════════════════════════════════════════
  wordpress:
    # ★ 关键变更:从 apache 改为 fpm
    image: wordpress:7.0.2-php8.4-fpm
    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
      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:
      # ★ 同一个宿主机目录,Nginx 以 ro 挂载读静态文件,
      #   FPM 以 rw 挂载执行 PHP 和写入上传文件
      - /web/wp:/var/www/html
      - /web/php/upload.ini:/usr/local/etc/php/conf.d/upload.ini:ro
    networks:
      - web
      - backend
    logging: *default-logging
    # ★ FPM 容器不再暴露 HTTP 端口,只监听 9000(FastCGI)
    # 不需要 ports 映射,Nginx 通过 web 网络访问 wordpress:9000

  # MariaDB 和 phpMyAdmin 配置不变,此处省略
  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
    logging: *default-logging
    healthcheck:
      test: ["CMD", "healthcheck.sh", "--connect", "--innodb_initialized"]
      interval: 10s
      timeout: 5s
      retries: 10
      start_period: 30s

  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

volumes:
  db_data:

3.3 nginx.conf 变更

upstream 定义需要删除(FPM 不走 HTTP upstream),改为 FastCGI 参数:

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;

    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;

    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;

    map $http_upgrade $connection_upgrade {
        default upgrade;
        ''      "";
    }

    # ─── Docker 内置 DNS ───
    # 在纯 upstream 方案中,resolver 指令不是必需的。保留它是为了防御性配置,将来需要时无需临时添加。
    resolver 127.0.0.11 valid=10s ipv6=off;
    resolver_timeout 5s;

    # ★ 关键变更:删除 upstream wordpress_backend
    # FPM 使用 fastcgi_pass,不走 HTTP upstream
    # phpMyAdmin 仍然是 HTTP 反代,保留其 upstream
    upstream phpmyadmin_backend {
        server dbweb:80 max_fails=3 fail_timeout=10s;
        keepalive 8;
    }

    # ★ 新增:FastCGI 全局参数(所有 WordPress location 复用)
    # 这些参数对应原来 proxy-wordpress.conf 中的 proxy_* 指令
    fastcgi_param SCRIPT_FILENAME $document_root$fastcgi_script_name;
    fastcgi_param PATH_INFO $fastcgi_path_info;
    fastcgi_param QUERY_STRING $query_string;
    fastcgi_param REQUEST_METHOD $request_method;
    fastcgi_param CONTENT_TYPE $content_type;
    fastcgi_param CONTENT_LENGTH $content_length;
    fastcgi_param SCRIPT_NAME $fastcgi_script_name;
    fastcgi_param REQUEST_URI $request_uri;
    fastcgi_param DOCUMENT_URI $document_uri;
    fastcgi_param DOCUMENT_ROOT $document_root;
    fastcgi_param SERVER_PROTOCOL $server_protocol;
    fastcgi_param REQUEST_SCHEME $scheme;
    fastcgi_param HTTPS $https if_not_empty;
    fastcgi_param REMOTE_ADDR $remote_addr;
    fastcgi_param REMOTE_PORT $remote_port;
    fastcgi_param SERVER_ADDR $server_addr;
    fastcgi_param SERVER_PORT $server_port;
    fastcgi_param SERVER_NAME $server_name;

    # FastCGI 缓冲区
    fastcgi_buffer_size 16k;
    fastcgi_buffers 8 16k;
    fastcgi_busy_buffers_size 32k;
    fastcgi_read_timeout 60s;
    fastcgi_send_timeout 60s;
    fastcgi_connect_timeout 10s;

    include /etc/nginx/conf.d/*.conf;
}

3.4 新建 snippets/fastcgi-wordpress.conf(替代 proxy-wordpress.conf)

# WordPress FastCGI 公共配置(替代原来的 proxy-wordpress.conf)

# ★ 指向 FPM 容器的 9000 端口
fastcgi_pass wordpress:9000;

# WordPress 需要 index.php 作为入口
fastcgi_index index.php;

# 包含标准 FastCGI 参数
include fastcgi_params;

# ★ 关键:告诉 FPM 脚本文件的完整路径
# $document_root 来自 Nginx 的 root 指令
# $fastcgi_script_name 来自请求 URI
fastcgi_param SCRIPT_FILENAME $document_root$fastcgi_script_name;

# 传递真实客户端信息给 WordPress
fastcgi_param HTTP_X_REAL_IP $remote_addr;
fastcgi_param HTTP_X_FORWARDED_FOR $proxy_add_x_forwarded_for;
fastcgi_param HTTP_X_FORWARDED_PROTO $scheme;
fastcgi_param HTTP_X_FORWARDED_HOST $host;
fastcgi_param HTTP_X_FORWARDED_PORT $server_port;

# 禁止 FPM 缓存 PHP 文件(开发时可开启提升性能)
# fastcgi_param PHP_VALUE "opcache.enable=0";

3.5 abc.com.conf 变更

核心变化:静态文件由 Nginx 直接服务,PHP 请求走 FastCGI

# HTTP → HTTPS 跳转(不变)
server {
    listen 80;
    server_name abc.com www.abc.com;
    location / {
        return 301 https://abc.com$request_uri;
    }
}

# www → 主域跳转(不变)
server {
    listen 443 ssl;
    http2 on;
    server_name www.abc.com;
    ssl_certificate /etc/ssl/fullchain.cer;
    ssl_certificate_key /etc/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(FPM 版本)
# ═══════════════════════════════════════
server {
    listen 443 ssl;
    listen 443 quic reuseport;
    http2 on;
    server_name abc.com;

    ssl_certificate /etc/ssl/fullchain.cer;
    ssl_certificate_key /etc/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 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;

    client_max_body_size 100m;

    # ★ 关键变更:指定 WordPress 文件根目录
    # Nginx 直接从共享 volume 读取静态文件
    root /var/www/html;
    index index.php index.html;

    # 健康检查(不变)
    location = /nginx-health {
        access_log off;
        default_type text/plain;
        return 200 "ok\n";
    }

    # 禁止 XML-RPC(不变)
    location = /xmlrpc.php {
        return 444;
    }

    # 禁止敏感文件(不变)
    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 444;
    }
    location ~ /\. {
        return 404;
    }

    # 禁止 uploads 目录执行 PHP(不变)
    location ~* /wp-content/uploads/.*\.php$ {
        return 403;
    }

    # ★ 新增:静态文件直接由 Nginx 返回,不经过 FPM
    # 这是 FPM 架构性能优势的核心体现
    location ~* \.(css|js|jpg|jpeg|png|gif|ico|svg|woff|woff2|ttf|eot|avif|webp)$ {
        expires 30d;
        add_header Cache-Control "public, immutable";
        access_log off;
        # try_files 确保文件存在,不存在则返回 404
        try_files $uri =404;
    }

    # WordPress 登录限流
    location = /wp-login.php {
        limit_req zone=login burst=5 nodelay;
        limit_conn addr 8;
        # ★ 从 proxy include 改为 fastcgi include
        include /etc/nginx/snippets/fastcgi-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/fastcgi-wordpress.conf;
    }

    # ★ 主站请求:WordPress 伪静态规则 + FastCGI
    location / {
        limit_req zone=general burst=100 nodelay;
        limit_conn addr 100;

        # WordPress 永久链接支持
        try_files $uri $uri/ /index.php?$args;
    }

    # ★ PHP 请求统一走 FastCGI
    location ~ \.php$ {
        limit_req zone=general burst=100 nodelay;
        limit_conn addr 100;

        # 防止任意 PHP 文件执行(只允许存在的文件)
        try_files $uri =404;

        include /etc/nginx/snippets/fastcgi-wordpress.conf;
    }
}

3.6 default.conf 和 pma.abc.com.conf

  • default.conf: 不需要改动(不涉及 WordPress 反代)
  • pma.abc.com.conf: 不需要改动(phpMyAdmin 仍然使用 HTTP upstream 反代)

四、FPM 方案的注意事项与踩坑点

4.1 文件权限问题(最常见的坑)

# Nginx 容器以 nginx 用户(UID 101)运行
# FPM 容器以 www-data 用户(UID 33)运行
# 两者对 /web/wp 的读写权限可能冲突

# 解决方案:统一设置所有权
chown -R 33:33 /web/wp          # FPM 需要写入上传文件
chmod -R 755 /web/wp

# Nginx 以 ro 挂载,只需要读权限,不受影响

4.2 .htaccess 不再生效

Apache 镜像中,WordPress 的伪静态规则写在 .htaccess 里。FPM + Nginx 方案中,.htaccess 完全被忽略。所有重写规则必须在 Nginx 配置中手动实现:

# 这就是 .htaccess 中 RewriteRule 的 Nginx 等价写法
location / {
    try_files $uri $uri/ /index.php?$args;
}

如果你安装了依赖 .htaccess 的插件(如某些缓存插件、安全插件),需要在 Nginx 中手动转换规则。

4.3 SCRIPT_FILENAME 必须正确

这是 FPM 方案中最容易出错的配置:

# ✅ 正确
fastcgi_param SCRIPT_FILENAME $document_root$fastcgi_script_name;

# ❌ 错误:缺少 $document_root
fastcgi_param SCRIPT_FILENAME $fastcgi_script_name;
# → FPM 找不到文件 → 500 Internal Server Error

# ❌ 错误:硬编码路径
fastcgi_param SCRIPT_FILENAME /var/www/html/index.php;
# → 所有请求都执行 index.php → 页面全部相同

4.4 静态文件缓存策略

FPM 方案的一大优势是 Nginx 可以直接控制静态文件缓存。建议根据文件类型设置不同的过期时间:

# 字体和图片:长期缓存
location ~* \.(woff|woff2|ttf|eot|avif|webp|ico)$ {
    expires 1y;
    add_header Cache-Control "public, immutable";
}

# CSS/JS:中等缓存(带版本号的文件可以更长)
location ~* \.(css|js)$ {
    expires 30d;
    add_header Cache-Control "public";
}

# 用户上传的图片
location ~* /wp-content/uploads/.*\.(jpg|jpeg|png|gif|svg)$ {
    expires 90d;
    add_header Cache-Control "public";
}

五、决策建议

你的站点日均 PV 是多少?
    │
    ├── < 5000 ──→ Apache 镜像 ✅(当前方案,简单可靠)
    │
    ├── 5000 ~ 50000 ──→ 考虑 FPM
    │   └── 你能接受额外的配置复杂度吗?
    │       ├── 能 → FPM ✅
    │       └── 不能 → Apache + OPcache 也能撑住
    │
    └── > 50000 ──→ FPM ✅✅(几乎是必选项)

对于本方案的个人博客场景,Apache 镜像是更务实的选择。 FPM 的性能优势在小流量下无法体现,但配置复杂度和排障成本是实实在在的。如果未来流量增长到需要 FPM 的程度,再按本文第三节的指南迁移即可——核心改动就是三件事:换镜像、加 root、把 proxy_pass 换成 fastcgi_pass


本文内容在完整版教程 Docker WordPress 建站指南:从零到生产级部署 基础上进行讨论

正文完
 0
评论(没有评论)