Nginx vs Apache - Production Web Server Configuration
Nginx Apache Production
devopsNginx vs Apache - Production Web Server Configuration
Choosing the right web server is one of the most impactful infrastructure decisions in any web project. Nginx and Apache have dominated the market for years, yet they differ fundamentally in architecture, request handling, and the scenarios where each one excels. In this article, we provide a comprehensive comparison of both servers, presenting production-ready configurations, performance optimizations, and recommendations on when to choose which server.
Architecture - Event-Driven vs Process-Based#
The fundamental difference between Nginx and Apache lies in how they handle incoming connections. Understanding this distinction is essential for proper production configuration.
Nginx - Event-Driven Architecture#
Nginx uses an asynchronous, event-driven model for handling connections. A single worker process can handle thousands of simultaneous connections thanks to non-blocking I/O and an event loop (epoll on Linux, kqueue on BSD).
# /etc/nginx/nginx.conf - Main configuration
user www-data;
worker_processes auto; # One worker per CPU core
worker_rlimit_nofile 65535; # Open file descriptor limit
pid /run/nginx.pid;
events {
worker_connections 4096; # Connections per worker
multi_accept on; # Accept multiple connections at once
use epoll; # Event mechanism on Linux
}
http {
sendfile on;
tcp_nopush on;
tcp_nodelay on;
keepalive_timeout 65;
types_hash_max_size 2048;
server_tokens off; # Hide server version
include /etc/nginx/mime.types;
default_type application/octet-stream;
# Logging
access_log /var/log/nginx/access.log;
error_log /var/log/nginx/error.log warn;
include /etc/nginx/conf.d/*.conf;
include /etc/nginx/sites-enabled/*;
}
In this model, a single worker process with 4096 connections across 4 CPU cores can theoretically handle up to 16,384 concurrent connections - without spawning a new process or thread for each request.
Apache - Process/Thread-Based Architecture#
Apache traditionally uses the prefork model (a separate process per request) or worker/event MPM (a combination of processes and threads). In modern installations, event MPM is recommended:
# /etc/apache2/mods-available/mpm_event.conf
<IfModule mpm_event_module>
StartServers 4
MinSpareThreads 25
MaxSpareThreads 75
ThreadLimit 64
ThreadsPerChild 25
MaxRequestWorkers 400
MaxConnectionsPerChild 10000
ServerLimit 16
</IfModule>
Apache with event MPM is significantly more efficient than prefork, but still requires more resources per connection than Nginx. Each MaxRequestWorkers slot corresponds to one concurrently served request.
Nginx as a Reverse Proxy#
Nginx is most commonly used as a reverse proxy in front of application servers. Here is a complete production configuration:
# /etc/nginx/sites-available/app.conf
upstream backend_app {
least_conn; # Load balancing - least connections
server 127.0.0.1:3000 weight=3; # Primary instance
server 127.0.0.1:3001 weight=2; # Secondary instance
server 127.0.0.1:3002 backup; # Backup - only when primaries fail
keepalive 32; # Keep-alive connection pool
}
server {
listen 80;
server_name example.com www.example.com;
return 301 https://$server_name$request_uri;
}
server {
listen 443 ssl http2;
server_name example.com www.example.com;
# SSL - configuration below
include /etc/nginx/snippets/ssl-params.conf;
ssl_certificate /etc/letsencrypt/live/example.com/fullchain.pem;
ssl_certificate_key /etc/letsencrypt/live/example.com/privkey.pem;
# Request body size
client_max_body_size 64M;
client_body_buffer_size 128k;
# Proxy to backend
location / {
proxy_pass http://backend_app;
proxy_http_version 1.1;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection "upgrade";
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 timeouts
proxy_connect_timeout 60s;
proxy_send_timeout 60s;
proxy_read_timeout 60s;
# Buffering
proxy_buffering on;
proxy_buffer_size 4k;
proxy_buffers 8 4k;
}
# Static files served directly by Nginx
location /static/ {
alias /var/www/app/static/;
expires 1y;
add_header Cache-Control "public, immutable";
access_log off;
}
location /_next/static/ {
alias /var/www/app/.next/static/;
expires 1y;
add_header Cache-Control "public, immutable";
access_log off;
}
}
Apache - Virtual Hosts and .htaccess#
Apache offers a powerful configuration system based on Virtual Hosts and .htaccess files:
# /etc/apache2/sites-available/app.conf
<VirtualHost *:80>
ServerName example.com
ServerAlias www.example.com
Redirect permanent / https://example.com/
</VirtualHost>
<VirtualHost *:443>
ServerName example.com
ServerAlias www.example.com
DocumentRoot /var/www/app/public
# SSL
SSLEngine on
SSLCertificateFile /etc/letsencrypt/live/example.com/fullchain.pem
SSLCertificateKeyFile /etc/letsencrypt/live/example.com/privkey.pem
Include /etc/letsencrypt/options-ssl-apache.conf
# Logging
ErrorLog ${APACHE_LOG_DIR}/app-error.log
CustomLog ${APACHE_LOG_DIR}/app-access.log combined
# Document root directory
<Directory /var/www/app/public>
AllowOverride All
Require all granted
Options -Indexes +FollowSymLinks
</Directory>
# Proxy to Node.js / Next.js
ProxyPreserveHost On
ProxyPass /api http://127.0.0.1:3000/api
ProxyPassReverse /api http://127.0.0.1:3000/api
# Static files
<LocationMatch "^/static/">
ExpiresActive On
ExpiresDefault "access plus 1 year"
Header set Cache-Control "public, immutable"
</LocationMatch>
# Security headers
Header always set X-Content-Type-Options "nosniff"
Header always set X-Frame-Options "SAMEORIGIN"
Header always set X-XSS-Protection "1; mode=block"
Header always set Referrer-Policy "strict-origin-when-cross-origin"
</VirtualHost>
The .htaccess file for a PHP/Laravel application:
# /var/www/app/public/.htaccess
<IfModule mod_rewrite.c>
RewriteEngine On
RewriteBase /
# Redirect to HTTPS
RewriteCond %{HTTPS} off
RewriteRule ^(.*)$ https://%{HTTP_HOST}%{REQUEST_URI} [L,R=301]
# Remove trailing slash
RewriteCond %{REQUEST_FILENAME} !-d
RewriteRule ^(.*)/$ /$1 [L,R=301]
# Front controller - Laravel
RewriteCond %{REQUEST_FILENAME} !-d
RewriteCond %{REQUEST_FILENAME} !-f
RewriteRule ^ index.php [L]
</IfModule>
# Compression
<IfModule mod_deflate.c>
AddOutputFilterByType DEFLATE text/html text/plain text/css
AddOutputFilterByType DEFLATE text/javascript application/javascript
AddOutputFilterByType DEFLATE application/json application/xml
AddOutputFilterByType DEFLATE image/svg+xml
</IfModule>
# Browser caching
<IfModule mod_expires.c>
ExpiresActive On
ExpiresByType image/webp "access plus 1 year"
ExpiresByType image/jpeg "access plus 1 year"
ExpiresByType image/png "access plus 1 year"
ExpiresByType text/css "access plus 1 month"
ExpiresByType application/javascript "access plus 1 month"
ExpiresByType font/woff2 "access plus 1 year"
</IfModule>
SSL/TLS with Let's Encrypt#
SSL configuration is critical for both security and SEO. Here are the optimal settings for both servers.
Nginx - SSL Snippet#
# /etc/nginx/snippets/ssl-params.conf
ssl_protocols TLSv1.2 TLSv1.3;
ssl_ciphers ECDHE-ECDSA-AES128-GCM-SHA256:ECDHE-RSA-AES128-GCM-SHA256:ECDHE-ECDSA-AES256-GCM-SHA384:ECDHE-RSA-AES256-GCM-SHA384;
ssl_prefer_server_ciphers off;
# OCSP Stapling
ssl_stapling on;
ssl_stapling_verify on;
resolver 8.8.8.8 8.8.4.4 valid=300s;
resolver_timeout 5s;
# SSL sessions
ssl_session_cache shared:SSL:10m;
ssl_session_timeout 1d;
ssl_session_tickets off;
# HSTS - Strict Transport Security
add_header Strict-Transport-Security "max-age=63072000; includeSubDomains; preload" always;
# Diffie-Hellman parameters
ssl_dhparam /etc/nginx/dhparam.pem;
Generating DH parameters:
openssl dhparam -out /etc/nginx/dhparam.pem 4096
Automatic Certificate Renewal#
# Certbot with automatic renewal
sudo certbot --nginx -d example.com -d www.example.com
# Crontab - renew every 12 hours
0 */12 * * * certbot renew --quiet --deploy-hook "systemctl reload nginx"
PHP-FPM - Configuration for Nginx and Apache#
PHP-FPM (FastCGI Process Manager) is the recommended way to run PHP with both Nginx and Apache.
PHP-FPM Pool Configuration#
; /etc/php/8.3/fpm/pool.d/www.conf
[www]
user = www-data
group = www-data
; UNIX socket - faster than TCP
listen = /run/php/php8.3-fpm.sock
listen.owner = www-data
listen.group = www-data
; Process management
pm = dynamic
pm.max_children = 50
pm.start_servers = 10
pm.min_spare_servers = 5
pm.max_spare_servers = 20
pm.max_requests = 1000
; Slow log for debugging
slowlog = /var/log/php-fpm/slow.log
request_slowlog_timeout = 5s
; Limits
request_terminate_timeout = 120s
php_admin_value[memory_limit] = 256M
php_admin_value[upload_max_filesize] = 64M
php_admin_value[post_max_size] = 64M
; OPcache
php_admin_value[opcache.enable] = 1
php_admin_value[opcache.memory_consumption] = 256
php_admin_value[opcache.max_accelerated_files] = 20000
php_admin_value[opcache.validate_timestamps] = 0
Nginx with PHP-FPM#
server {
listen 443 ssl http2;
server_name laravel-app.com;
root /var/www/laravel/public;
index index.php;
include /etc/nginx/snippets/ssl-params.conf;
ssl_certificate /etc/letsencrypt/live/laravel-app.com/fullchain.pem;
ssl_certificate_key /etc/letsencrypt/live/laravel-app.com/privkey.pem;
location / {
try_files $uri $uri/ /index.php?$query_string;
}
location ~ \.php$ {
fastcgi_pass unix:/run/php/php8.3-fpm.sock;
fastcgi_param SCRIPT_FILENAME $realpath_root$fastcgi_script_name;
include fastcgi_params;
# FastCGI buffering
fastcgi_buffering on;
fastcgi_buffer_size 16k;
fastcgi_buffers 16 16k;
# Timeouts
fastcgi_connect_timeout 60;
fastcgi_send_timeout 120;
fastcgi_read_timeout 120;
}
# Block access to hidden files
location ~ /\.(?!well-known) {
deny all;
}
# Block access to sensitive files
location ~* \.(env|log|git|sql|bak)$ {
deny all;
return 404;
}
}
Apache with PHP-FPM#
<VirtualHost *:443>
ServerName laravel-app.com
DocumentRoot /var/www/laravel/public
# PHP-FPM via proxy
<FilesMatch \.php$>
SetHandler "proxy:unix:/run/php/php8.3-fpm.sock|fcgi://localhost"
</FilesMatch>
<Directory /var/www/laravel/public>
AllowOverride All
Require all granted
Options -Indexes
</Directory>
# Block access to hidden files
<DirectoryMatch "/\.">
Require all denied
</DirectoryMatch>
</VirtualHost>
Load Balancing#
Nginx - Advanced Load Balancing#
upstream php_backend {
# Algorithm - ip_hash ensures sticky sessions
ip_hash;
server 10.0.0.10:9000 weight=5 max_fails=3 fail_timeout=30s;
server 10.0.0.11:9000 weight=3 max_fails=3 fail_timeout=30s;
server 10.0.0.12:9000 weight=2 max_fails=3 fail_timeout=30s;
server 10.0.0.13:9000 backup;
keepalive 64;
}
upstream node_backend {
least_conn;
server 127.0.0.1:3000;
server 127.0.0.1:3001;
server 127.0.0.1:3002;
keepalive 32;
}
server {
listen 443 ssl http2;
server_name app.example.com;
location /api/ {
proxy_pass http://php_backend;
proxy_next_upstream error timeout http_500 http_502 http_503;
proxy_next_upstream_tries 3;
}
location / {
proxy_pass http://node_backend;
proxy_next_upstream error timeout http_502;
}
}
Apache - mod_proxy_balancer#
<Proxy "balancer://app_cluster">
BalancerMember "http://10.0.0.10:8080" route=node1 loadfactor=5
BalancerMember "http://10.0.0.11:8080" route=node2 loadfactor=3
BalancerMember "http://10.0.0.12:8080" route=node3 loadfactor=2 status=+H
ProxySet lbmethod=byrequests stickysession=JSESSIONID
</Proxy>
<VirtualHost *:443>
ServerName app.example.com
ProxyPreserveHost On
ProxyPass / "balancer://app_cluster/"
ProxyPassReverse / "balancer://app_cluster/"
# Load balancer management panel
<Location "/balancer-manager">
SetHandler balancer-manager
Require ip 10.0.0.0/8
</Location>
</VirtualHost>
Caching Configuration#
Nginx - FastCGI Cache and Proxy Cache#
# Cache zone definition (in http block)
fastcgi_cache_path /var/cache/nginx/fastcgi
levels=1:2
keys_zone=PHPCACHE:100m
max_size=2g
inactive=60m
use_temp_path=off;
proxy_cache_path /var/cache/nginx/proxy
levels=1:2
keys_zone=PROXYCACHE:100m
max_size=5g
inactive=24h
use_temp_path=off;
server {
# FastCGI cache for PHP
location ~ \.php$ {
fastcgi_pass unix:/run/php/php8.3-fpm.sock;
include fastcgi_params;
fastcgi_cache PHPCACHE;
fastcgi_cache_valid 200 301 302 60m;
fastcgi_cache_valid 404 1m;
fastcgi_cache_key "$scheme$request_method$host$request_uri";
fastcgi_cache_use_stale error timeout updating http_500 http_503;
fastcgi_cache_bypass $cookie_nocache $arg_nocache;
add_header X-Cache-Status $upstream_cache_status;
}
# Proxy cache for public API
location /api/public/ {
proxy_pass http://backend_app;
proxy_cache PROXYCACHE;
proxy_cache_valid 200 10m;
proxy_cache_key "$request_uri";
add_header X-Cache-Status $upstream_cache_status;
}
}
Apache - mod_cache#
# Enable cache modules
# a2enmod cache cache_disk headers
CacheQuickHandler off
CacheLock on
CacheLockPath /tmp/mod_cache-lock
CacheLockMaxAge 5
<VirtualHost *:443>
CacheEnable disk /
CacheRoot /var/cache/apache2/mod_cache_disk
CacheDefaultExpire 3600
CacheMaxExpire 86400
CacheIgnoreNoLastMod On
# Do not cache admin panel
CacheDisable /admin
CacheDisable /api/auth
# Cache headers
<LocationMatch "^/static/">
Header set Cache-Control "public, max-age=31536000, immutable"
</LocationMatch>
</VirtualHost>
Gzip and Brotli Compression#
Nginx#
# Gzip
gzip on;
gzip_vary on;
gzip_proxied any;
gzip_comp_level 5;
gzip_min_length 256;
gzip_types
text/plain
text/css
text/javascript
application/javascript
application/json
application/xml
application/xml+rss
image/svg+xml
font/woff2;
# Brotli (requires ngx_brotli module)
brotli on;
brotli_comp_level 6;
brotli_static on; # Serve pre-compressed .br files
brotli_types
text/plain
text/css
text/javascript
application/javascript
application/json
application/xml
image/svg+xml
font/woff2;
Apache#
# mod_deflate (gzip)
<IfModule mod_deflate.c>
SetOutputFilter DEFLATE
DeflateCompressionLevel 5
AddOutputFilterByType DEFLATE text/html text/plain text/css
AddOutputFilterByType DEFLATE text/javascript application/javascript
AddOutputFilterByType DEFLATE application/json application/xml
AddOutputFilterByType DEFLATE image/svg+xml font/woff2
# Don't compress already compressed files
SetEnvIfNoCase Request_URI \.(?:gif|jpe?g|png|webp|gz|zip|br)$ no-gzip
</IfModule>
# Brotli (requires mod_brotli - Apache 2.4.26+)
<IfModule mod_brotli.c>
AddOutputFilterByType BROTLI_COMPRESS text/html text/plain text/css
AddOutputFilterByType BROTLI_COMPRESS text/javascript application/javascript
AddOutputFilterByType BROTLI_COMPRESS application/json application/xml
BrotliCompressionQuality 5
</IfModule>
Security Headers#
Nginx - Complete Security Headers Configuration#
# /etc/nginx/snippets/security-headers.conf
add_header X-Content-Type-Options "nosniff" always;
add_header X-Frame-Options "SAMEORIGIN" always;
add_header X-XSS-Protection "1; mode=block" always;
add_header Referrer-Policy "strict-origin-when-cross-origin" always;
add_header Permissions-Policy "camera=(), microphone=(), geolocation=()" always;
add_header Content-Security-Policy "default-src 'self'; script-src 'self' 'unsafe-inline' https://cdn.jsdelivr.net; style-src 'self' 'unsafe-inline' https://fonts.googleapis.com; font-src 'self' https://fonts.gstatic.com; img-src 'self' data: https:; connect-src 'self' https://api.example.com;" always;
# Remove headers that reveal technology stack
proxy_hide_header X-Powered-By;
fastcgi_hide_header X-Powered-By;
Rate Limiting#
Nginx#
# Rate limiting zone definitions (in http block)
limit_req_zone $binary_remote_addr zone=api:10m rate=10r/s;
limit_req_zone $binary_remote_addr zone=login:10m rate=1r/s;
limit_conn_zone $binary_remote_addr zone=addr:10m;
server {
# General rate limit for API
location /api/ {
limit_req zone=api burst=20 nodelay;
limit_req_status 429;
proxy_pass http://backend_app;
}
# Strict rate limit for login
location /api/auth/login {
limit_req zone=login burst=3 nodelay;
limit_req_status 429;
proxy_pass http://backend_app;
}
# Concurrent connection limit
location /downloads/ {
limit_conn addr 5;
limit_rate 1m; # 1MB/s per connection
alias /var/www/downloads/;
}
}
Apache - mod_ratelimit and mod_evasive#
# mod_ratelimit - bandwidth limiting
<Location "/downloads">
SetOutputFilter RATE_LIMIT
SetEnv rate-limit 1024 # 1024 KB/s
</Location>
# mod_evasive - DDoS protection
<IfModule mod_evasive20.c>
DOSHashTableSize 3097
DOSPageCount 5 # Max 5 requests per page/s
DOSSiteCount 50 # Max 50 requests per site/s
DOSPageInterval 1
DOSSiteInterval 1
DOSBlockingPeriod 60 # Block for 60s
DOSEmailNotify admin@example.com
</IfModule>
WebSocket Proxying#
Nginx#
# Map for WebSocket upgrade handling
map $http_upgrade $connection_upgrade {
default upgrade;
'' close;
}
server {
location /ws/ {
proxy_pass http://127.0.0.1:3000;
proxy_http_version 1.1;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection $connection_upgrade;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
# WebSocket timeout (longer than standard)
proxy_read_timeout 3600s;
proxy_send_timeout 3600s;
}
}
Apache#
# Required modules: mod_proxy_wstunnel
# a2enmod proxy_wstunnel
<VirtualHost *:443>
# WebSocket proxy
RewriteEngine On
RewriteCond %{HTTP:Upgrade} =websocket [NC]
RewriteRule /ws/(.*) ws://127.0.0.1:3000/ws/$1 [P,L]
ProxyPass /ws/ ws://127.0.0.1:3000/ws/
ProxyPassReverse /ws/ ws://127.0.0.1:3000/ws/
# WebSocket timeout
ProxyTimeout 3600
</VirtualHost>
Performance Comparison#
Below are typical benchmark results for both servers across different scenarios:
| Metric | Nginx | Apache (event MPM) | |--------|-------|---------------------| | Static files (req/s) | ~25,000 | ~10,000 | | Reverse proxy (req/s) | ~18,000 | ~8,000 | | PHP-FPM (req/s) | ~3,500 | ~3,200 | | RAM usage (1,000 conn.) | ~50 MB | ~200 MB | | RAM usage (10,000 conn.) | ~80 MB | ~1.5 GB | | Response time p99 | ~2 ms | ~8 ms | | Max concurrent connections | ~100,000+ | ~10,000 |
Nginx dominates in scenarios with many concurrent connections and static file serving. For dynamic PHP-FPM requests, the gap narrows because PHP itself becomes the bottleneck.
Nginx for Next.js / Node.js#
# Production Nginx configuration for Next.js
upstream nextjs {
server 127.0.0.1:3000;
keepalive 64;
}
server {
listen 443 ssl http2;
server_name app.example.com;
include /etc/nginx/snippets/ssl-params.conf;
include /etc/nginx/snippets/security-headers.conf;
ssl_certificate /etc/letsencrypt/live/app.example.com/fullchain.pem;
ssl_certificate_key /etc/letsencrypt/live/app.example.com/privkey.pem;
# Next.js static assets - aggressive caching
location /_next/static/ {
proxy_pass http://nextjs;
expires 1y;
add_header Cache-Control "public, immutable";
access_log off;
}
# Next.js image optimization
location /_next/image {
proxy_pass http://nextjs;
proxy_cache PROXYCACHE;
proxy_cache_valid 200 24h;
proxy_cache_key "$request_uri";
}
# Public assets
location /public/ {
alias /var/www/app/public/;
expires 30d;
add_header Cache-Control "public";
access_log off;
}
# Everything else to Next.js
location / {
proxy_pass http://nextjs;
proxy_http_version 1.1;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection "upgrade";
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;
}
}
Apache for PHP / Laravel#
# Production Apache configuration for Laravel
<VirtualHost *:443>
ServerName laravel.example.com
DocumentRoot /var/www/laravel/public
SSLEngine on
SSLCertificateFile /etc/letsencrypt/live/laravel.example.com/fullchain.pem
SSLCertificateKeyFile /etc/letsencrypt/live/laravel.example.com/privkey.pem
# PHP-FPM
<FilesMatch \.php$>
SetHandler "proxy:unix:/run/php/php8.3-fpm.sock|fcgi://localhost"
</FilesMatch>
# Document root
<Directory /var/www/laravel/public>
AllowOverride All
Require all granted
Options -Indexes +FollowSymLinks -MultiViews
</Directory>
# Block access to sensitive directories
<DirectoryMatch "^/var/www/laravel/(storage|vendor|bootstrap/cache)">
Require all denied
</DirectoryMatch>
# Block .env files
<FilesMatch "^\.env">
Require all denied
</FilesMatch>
# Security headers
Header always set X-Content-Type-Options "nosniff"
Header always set X-Frame-Options "SAMEORIGIN"
Header always set Referrer-Policy "strict-origin-when-cross-origin"
Header always set Strict-Transport-Security "max-age=63072000; includeSubDomains"
# Compression
SetOutputFilter DEFLATE
AddOutputFilterByType DEFLATE text/html text/css application/javascript application/json
# Logging
ErrorLog ${APACHE_LOG_DIR}/laravel-error.log
CustomLog ${APACHE_LOG_DIR}/laravel-access.log combined
</VirtualHost>
When to Choose Nginx vs Apache#
Choose Nginx when:#
- Building a Node.js / Next.js application - Nginx is the natural reverse proxy for Node applications
- Handling many concurrent connections - WebSocket, SSE, long polling
- Serving a lot of static files - Nginx is significantly more efficient
- Load balancing - built-in load balancing algorithms are flexible and performant
- Microservices - as an API gateway routing to multiple backends
- Limited resources - lower RAM and CPU consumption
- Containerization (Docker/K8s) - smaller footprint, faster startup
Choose Apache when:#
- Shared hosting -
.htaccessallows per-directory configuration without server restart - PHP/Laravel applications - native integration with mod_php (though PHP-FPM is recommended)
- Dynamic configuration needed -
.htaccessenables changes without restart - Complex rewrite rules - mod_rewrite is more feature-rich
- Multiple sites on one server - configuration isolation via
.htaccess - Existing infrastructure - migrating from Apache to Nginx is not always cost-effective
Hybrid Approach#
In many production environments, the best solution is a combination of both servers:
Client -> Nginx (reverse proxy, SSL, cache, static files)
|-- Node.js / Next.js (port 3000)
|-- Apache + PHP-FPM (port 8080)
|-- Other microservices
Nginx serves as the front-end proxy handling SSL termination, compression, caching, and static files, while Apache handles PHP applications with the full power of .htaccess and mod_rewrite.
Conclusion#
Both Nginx and Apache are mature, reliable web servers. Nginx dominates in scenarios requiring high performance, large numbers of concurrent connections, and reverse proxy workloads. Apache offers greater configuration flexibility and remains the traditional choice for PHP applications. In modern architectures, using Nginx as a front-end proxy with Apache as a PHP backend is a proven approach that combines the strengths of both servers.
Need professional web server configuration? The MDS Software Solutions Group team specializes in web infrastructure optimization. From Nginx and Apache configuration, through CI/CD deployments, to cloud application scaling - we will help you build a performant and secure infrastructure. Contact us to discuss your project.
Team of programming experts specializing in modern web technologies.