如果你靠写 PHP 吃饭(或者刚入门),迟早会跟 Web 服务器打交道。在 Linux 上,这通常意味着 Apache 或 Nginx。两者都非常成熟稳定,也都能很好地跑你的 PHP 应用——但它们的设计理念不同,也各自有一些「坑点」。
这篇指南面向 PHP 开发者,按步骤来,比较偏实战。我们会一起完成这些事情:安装整套环境、配好虚拟主机(在 Nginx 里叫 server block)、接上 PHP-FPM、设置合理的性能和安全默认值、开启 TLS,以及排查那些一定会碰到的诡异问题。你可以把它当成部署时的检查清单。
Apache 是模块驱动(module-based)的,使用 MPM(Multi-Processing Modules,多进程模块)来处理请求:
mpm_prefork + mod_php
每个进程只处理一个请求,并且直接把 PHP 嵌入进 Apache 进程。简单粗暴,但非常吃内存,扩展性差。新部署不推荐再用这一套。
mpm_event(或 worker)+ 通过 proxy_fcgi 使用 PHP-FPM
现代方式:Apache 高效处理 HTTP,把 PHP 执行交给 PHP-FPM。相比 mod_php 更好扩展。
Nginx 是事件驱动、异步架构。它完全不会直接运行 PHP,只会把 PHP 请求转发到 PHP-FPM。它在下面这些场景里很强:提供静态资源、作为反向代理、在高并发低内存场景下保持性能稳定。
到了 2025 年,大部分 PHP 团队不管选 Apache 还是 Nginx,都会用 PHP-FPM。
简单判断:
.htaccess(老项目或需要目录级别 override),Apache 会更顺手。:8080)跑带 .htaccess 的老应用。sudo 权限。/var/www/,Apache 和 Nginx 都会用到。小建议:保持系统精简。没在用的 Web 服务器干脆卸掉,避免占用
:80/:443端口导致冲突。
# 更新软件源
sudo apt update
# Apache 相关(现代方案:event MPM + PHP-FPM)
sudo apt install -y apache2 libapache2-mod-fcgid \
php8.2-fpm php8.2-cli php8.2-mbstring php8.2-xml php8.2-curl php8.2-mysql php8.2-opcache
# Nginx 相关
sudo apt install -y nginx php8.2-fpm
# 常用工具
sudo apt install -y unzip curl ufw certbot python3-certbot-apache python3-certbot-nginx# PHP 及扩展
sudo dnf install -y @php:8.2 php-fpm php-cli php-mbstring php-xml php-curl php-mysqlnd php-opcache
# Web 服务器
sudo dnf install -y httpd mod_fcgid
sudo dnf install -y nginx certbot python3-certbot-apache python3-certbot-nginx
# 启动并设置开机自启
sudo systemctl enable --now php-fpm
sudo systemctl enable --now httpd
sudo systemctl enable --now nginx路径约定:
/etc/apache2/,Nginx → /etc/nginx//etc/httpd/,Nginx → /etc/nginx/PHP-FPM 负责以进程池的形式运行你的 PHP 代码。
# 查看状态
sudo systemctl status php8.2-fpm # Ubuntu/Debian
sudo systemctl status php-fpm # RHEL 系/run/php/php8.2-fpm.sock(速度快,Ubuntu/Debian 默认)127.0.0.1:9000(适合 chroot/容器之间或多主机之间调用)/etc/php/8.2/fpm/pool.d/www.conf/etc/php-fpm.d/www.conf示例配置(节选):
user = www-data ; Ubuntu 上通常是 www-data,RHEL 是 apache/nginx
group = www-data
listen = /run/php/php8.2-fpm.sock ; 或 127.0.0.1:9000
pm = dynamic
pm.max_children = 20
pm.start_servers = 4
pm.min_spare_servers = 4
pm.max_spare_servers = 8粗略经验:
pm.max_children ≈ (给 PHP 预留的内存总量) / (单个 PHP 进程平均占用内存 MB)
用ps/top/htop实际观察进程内存占用再调。
变更后重载:
sudo systemctl reload php8.2-fpm # 或 php-fpm安全/隔离建议:每个应用单独一个 PHP-FPM 进程池(不同的
listensocket 和user),避免一个站拖垮其它站。
mpm_event + PHP-FPM) Ubuntu / Debian:
sudo a2dismod mpm_prefork php* || true
sudo a2enmod mpm_event proxy proxy_fcgi setenvif http2 headers rewrite
sudo a2enconf php8.2-fpm
sudo systemctl restart apache2RHEL:确保在 /etc/httpd/conf.modules.d/ 中加载了 mod_proxy、mod_proxy_fcgi、mod_http2 等模块。
sudo mkdir -p /var/www/example/public
echo "<?php phpinfo();" | sudo tee /var/www/example/public/index.php >/dev/null
sudo chown -R www-data:www-data /var/www/exampleApache 虚拟主机(Ubuntu 风格:/etc/apache2/sites-available/example.conf):
<VirtualHost *:80>
ServerName example.test
DocumentRoot /var/www/example/public
<Directory /var/www/example/public>
AllowOverride None
Require all granted
</Directory>
# 通过 PHP-FPM 处理 PHP(Unix socket)
<FilesMatch \.php$>
SetHandler "proxy:unix:/run/php/php8.2-fpm.sock|fcgi://localhost/"
</FilesMatch>
# 安全相关 Header
Header always set X-Frame-Options SAMEORIGIN
Header always set X-Content-Type-Options nosniff
Header always set Referrer-Policy no-referrer-when-downgrade
Header always set Permissions-Policy "geolocation=()"
ErrorLog ${APACHE_LOG_DIR}/example-error.log
CustomLog ${APACHE_LOG_DIR}/example-access.log combined
</VirtualHost>启用并重载:
sudo a2ensite example.conf
sudo a2dissite 000-default.conf
sudo apachectl configtest
sudo systemctl reload apache2访问 http://example.test(记得在 /etc/hosts 里加一行:127.0.0.1 example.test)。
如果你必须使用
.htaccess,在<Directory>里改成AllowOverride All。但从性能角度看,推荐把规则移进虚拟主机配置里,保持AllowOverride None。
创建同样的文档根目录:
sudo mkdir -p /var/www/example/public
echo "<?php phpinfo();" | sudo tee /var/www/example/public/index.php >/dev/null
sudo chown -R www-data:www-data /var/www/exampleNginx server block(Ubuntu:/etc/nginx/sites-available/example,再软链到 sites-enabled):
server {
listen 80;
server_name example.test;
root /var/www/example/public;
index index.php index.html;
# 请求路由(适用于 Laravel/WordPress 等)
location / {
try_files $uri $uri/ /index.php?$query_string;
}
# PHP 处理
location ~ \.php$ {
# 阻止直接访问不存在的脚本
try_files $uri =404;
# Debian/Ubuntu 帮助文件,设置 SCRIPT_FILENAME 等
include snippets/fastcgi-php.conf;
# RHEL/其他发行版(备用写法):
# include fastcgi_params;
# fastcgi_param SCRIPT_FILENAME $realpath_root$fastcgi_script_name;
fastcgi_pass unix:/run/php/php8.2-fpm.sock;
fastcgi_read_timeout 60;
}
# 拒绝隐藏/敏感文件
location ~ /\. {
deny all;
}
# 安全相关 Header
add_header X-Frame-Options SAMEORIGIN;
add_header X-Content-Type-Options nosniff;
add_header Referrer-Policy no-referrer-when-downgrade;
add_header Permissions-Policy "geolocation=()";
access_log /var/log/nginx/example-access.log;
error_log /var/log/nginx/example-error.log warn;
}启用并测试:
sudo ln -s /etc/nginx/sites-available/example /etc/nginx/sites-enabled/
sudo nginx -t
sudo systemctl reload nginxNginx 没有
.htaccess,所有路由和重写都在配置里写死。
适用于:
.htaccessUbuntu:编辑 /etc/apache2/ports.conf → Listen 8080,并把虚拟主机改成 <VirtualHost *:8080>。
RHEL:在 /etc/httpd/conf/httpd.conf 或相应 vhost 文件里做类似调整。
sudo systemctl restart apache2 # 或 httpd# 在 http{} 里(可选)
# proxy_cache_path /var/cache/nginx levels=1:2 keys_zone=my_cache:10m max_size=100m inactive=60m use_temp_path=off;
server {
listen 80;
server_name example.test;
location / {
proxy_pass http://127.0.0.1:8080;
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_cache my_cache;
# proxy_cache_valid 200 1m;
# add_header X-Cache $upstream_cache_status;
}
# 静态文件直接由 Nginx 提供
location ~* \.(jpg|jpeg|png|gif|css|js|ico|svg|woff2?)$ {
expires 7d;
access_log off;
try_files $uri @backend;
}
location @backend {
proxy_pass http://127.0.0.1:8080;
}
}前提:
# Apache
sudo certbot --apache -d example.com -d www.example.com
# Nginx
sudo certbot --nginx -d example.com -d www.example.com选择自动重定向到 HTTPS。Certbot 会自动配置定时任务来续期。
启用 HTTP/2:
Protocols h2 http/1.1listen 443 ssl http2;如果你已经完全切到 HTTPS,可以再加 HSTS(谨慎开启):
add_header Strict-Transport-Security "max-age=31536000; includeSubDomains" always;HTTP/3(QUIC)需要 Nginx 带 QUIC/HTTP3 模块的构建,并在配置里写类似 listen 443 quic reuseport;——支持情况取决于发行版。
.htaccess) vhost 中:
<Directory /var/www/example/public>
AllowOverride None
Require all granted
</Directory>
RewriteEngine On
RewriteCond %{REQUEST_FILENAME} !-f
RewriteCond %{REQUEST_FILENAME} !-d
RewriteRule ^ index.php [L].htaccess 版本:
<IfModule mod_rewrite.c>
RewriteEngine On
RewriteCond %{REQUEST_FILENAME} !-f
RewriteCond %{REQUEST_FILENAME} !-d
RewriteRule ^ index.php [L]
</IfModule>location / {
try_files $uri $uri/ /index.php?$query_string;
}location / {
try_files $uri $uri/ /index.php?$args;
}Apache:
<FilesMatch "(^\.|\.env|composer\.(json|lock)|Dockerfile|docker-compose\.yml)">
Require all denied
</FilesMatch>Nginx:
location ~* (^/\.|/\.env|composer\.(json|lock)|Dockerfile|docker-compose\.yml)$ {
deny all;
}mpm_event + PHP-FPM) Ubuntu:/etc/apache2/mods-available/mpm_event.conf(或对应文件):
<IfModule mpm_event_module>
StartServers 2
MinSpareThreads 25
MaxSpareThreads 75
ThreadLimit 64
ThreadsPerChild 25
MaxRequestWorkers 150
MaxConnectionsPerChild 0
</IfModule>
KeepAlive On
MaxKeepAliveRequests 100
KeepAliveTimeout 2指导原则:
mpm_event + PHP-FPM(非必要不要再用 mod_php + prefork)。MaxRequestWorkers 的大小要结合内存预算来算:Apache + PHP-FPM + OS 的内存占用加起来不能爆。/etc/nginx/nginx.conf:
user www-data;
worker_processes auto;
events {
worker_connections 1024; # 高并发可以再调大
}
http {
sendfile on;
tcp_nopush on;
tcp_nodelay on;
keepalive_timeout 15;
types_hash_max_size 4096;
# Gzip 压缩
gzip on;
gzip_comp_level 5;
gzip_min_length 1024;
gzip_types text/plain text/css application/json application/javascript application/xml application/rss+xml image/svg+xml;
# 可选:如果有 Brotli 模块
# brotli on;
# brotli_comp_level 5;
# brotli_types text/plain text/css application/json application/javascript application/xml image/svg+xml;
}如果匿名用户的 HTML 可缓存:
proxy_cache_path /var/cache/nginx levels=1:2 keys_zone=micro:10m max_size=256m inactive=5m use_temp_path=off;
server {
# ...
location / {
proxy_pass http://127.0.0.1:9000; # 或 Apache on :8080
proxy_cache micro;
proxy_cache_valid 200 1s;
proxy_cache_use_stale error timeout updating http_500 http_502 http_503 http_504;
add_header X-Cache $upstream_cache_status;
}
}注意:
Cache-Control: private。proxy_no_cache 禁掉缓存。location ~* \.(?:css|js|jpg|jpeg|png|gif|svg|ico|woff2?)$ {
expires 30d;
add_header Cache-Control "public, max-age=2592000, immutable";
access_log off;
try_files $uri =404;
}<IfModule mod_expires.c>
ExpiresActive On
ExpiresByType text/css "access plus 30 days"
ExpiresByType application/javascript "access plus 30 days"
ExpiresByType image/svg+xml "access plus 30 days"
ExpiresDefault "access plus 7 days"
</IfModule>
<IfModule mod_headers.c>
<FilesMatch "\.(css|js|png|jpg|jpeg|gif|svg|ico|woff2?)$">
Header set Cache-Control "public, max-age=2592000, immutable"
</FilesMatch>
</IfModule>生产环境建议:使用带 hash 的静态资源文件名,例如 app.css?v=<hash> 或 app.<hash>.css,上线时即可安全长缓存。
php.ini(CLI 与 FPM 各有一份):
; 常见调整(下面的大小仅为示例)
memory_limit = 256M
upload_max_filesize = 50M
post_max_size = 50M
max_execution_time = 60
max_input_vars = 2000
; OPcache(对性能帮助巨大)
opcache.enable=1
opcache.enable_cli=0
opcache.memory_consumption=128
opcache.interned_strings_buffer=16
opcache.max_accelerated_files=10000
opcache.validate_timestamps=0 ; 生产建议设为 0(更新代码后需要 reload)
; 如果你部署很频繁,可以保留自动检测:
; opcache.validate_timestamps=1
; opcache.revalidate_freq=2大文件上传失败?除了改 PHP 配置,还要在 Nginx 里加:
client_max_body_size 50M; # 可写在 server{} 或 http{}Ubuntu 默认 Apache/Nginx 使用 www-data,RHEL 上是 apache 或 nginx。权限建议:
sudo chown -R www-data:www-data /var/www/example
# 目录 755,文件 644
find /var/www/example -type d -exec chmod 755 {} \;
find /var/www/example -type f -exec chmod 644 {} \;不要随便给 Web 用户写权限。确实必须写的目录(比如 WordPress 的 wp-content/uploads),最好只给那一个目录开放。
ServerSignature Off 和 ServerTokens Prod(例如在 security.conf 中)。http {} 里加 server_tokens off;。add_header Content-Security-Policy "default-src 'self'; img-src 'self' data: https:;" always;可以先设得严格一点,再根据实际前端资源慢慢放宽。
sudo ufw allow "Apache Full" # 或 "Nginx Full"
sudo ufw enable
sudo ufw statussudo firewall-cmd --add-service=http --permanent
sudo firewall-cmd --add-service=https --permanent
sudo firewall-cmd --reload如果 PHP 进程需要访问网络(远程数据库、Redis 等):
sudo setsebool -P httpd_can_network_connect on如果要从默认路径以外的目录提供文件,需要配相应的 security context,例如用 semanage fcontext + restorecon 调整。
日志位置:
/var/log/apache2/(Ubuntu)或 /var/log/httpd/(RHEL)/var/log/nginx//var/log/php8.2-fpm.log(Ubuntu)或 /var/log/php-fpm/error.log(RHEL)快速查看:
# 实时查看最后几行
tail -f /var/log/nginx/error.log
tail -f /var/log/apache2/error.log
journalctl -u php8.2-fpm -f # 或 php-fpm/httpd/nginxPHP-FPM 状态与 ping(可选,方便监控):
在进程池配置中:
pm.status_path = /status
ping.path = /pingNginx 中对应 location:
location ~ ^/(status|ping)$ {
allow 127.0.0.1; deny all; # 暴露时务必限制访问
include snippets/fastcgi-php.conf;
fastcgi_pass unix:/run/php/php8.2-fpm.sock;
}502 Bad Gateway(Nginx → PHP-FPM)
fastcgi_pass 是否一致。sudo systemctl status php8.2-fpm、sudo nginx -t。503 Service Unavailable(Apache + FPM)
MaxRequestWorkers 太小,或 FPM 子进程耗尽。pm.max_children。403 Forbidden
Require all granted;RHEL 上可能是 SELinux 拦截。优雅 URL 访问 404
RewriteRule 或 .htaccess,Nginx 需要合适的 try_files。大文件上传失败
client_max_body_size 和 PHP 的 upload_max_filesize / post_max_size。端口冲突
:80。如果 Nginx 在前面,就把 Apache 改到 :8080,只作为后端。使用发布目录软链:/var/www/example/releases/<timestamp> → /var/www/example/current
Web 根指向 /var/www/example/current/public,发布时只切换 current 链接即可。
如果 opcache.validate_timestamps=0,上线后要重置 OPcache:
/opcache-reset HTTP 接口,或直接跑:php -r 'opcache_reset();'优先使用优雅重载:
sudo systemctl reload nginx # / apache2 / php-fpm这样不会粗暴中断现有连接。
# 1)安装(选择一个栈)
sudo apt install -y nginx php8.2-fpm # 或:apache2 libapache2-mod-fcgid php8.2-fpm
# 2)创建站点目录
sudo mkdir -p /var/www/example/public
echo "<?php phpinfo();" | sudo tee /var/www/example/public/index.php >/dev/null
sudo chown -R www-data:www-data /var/www/example
# 3a)Nginx server block
sudo tee /etc/nginx/sites-available/example >/dev/null <<'NGINX'
server {
listen 80;
server_name example.test;
root /var/www/example/public;
index index.php index.html;
location / { try_files $uri $uri/ /index.php?$query_string; }
location ~ \.php$ {
try_files $uri =404;
include snippets/fastcgi-php.conf;
fastcgi_pass unix:/run/php/php8.2-fpm.sock;
}
location ~ /\. { deny all; }
}
NGINX
sudo ln -s /etc/nginx/sites-available/example /etc/nginx/sites-enabled/
sudo nginx -t && sudo systemctl reload nginx
# 3b)Apache 虚拟主机(备用方案)
sudo tee /etc/apache2/sites-available/example.conf >/dev/null <<'APACHE'
<VirtualHost *:80>
ServerName example.test
DocumentRoot /var/www/example/public
<Directory /var/www/example/public>
AllowOverride None
Require all granted
</Directory>
<FilesMatch \.php$>
SetHandler "proxy:unix:/run/php/php8.2-fpm.sock|fcgi://localhost/"
</FilesMatch>
ErrorLog ${APACHE_LOG_DIR}/example-error.log
CustomLog ${APACHE_LOG_DIR}/example-access.log combined
</VirtualHost>
APACHE
sudo a2ensite example.conf && sudo a2dissite 000-default.conf
sudo apachectl configtest && sudo systemctl reload apache2
# 4)本机 hosts(本地开发)
echo "127.0.0.1 example.test" | sudo tee -a /etc/hosts
# 5)TLS(有公网 DNS 时)
# sudo certbot --nginx -d example.com -d www.example.com
# sudo certbot --apache -d example.com -d www.example.comphpinfo() 文件。