Skip to content
DevOps

Nginx vs Apache - Production Web Server Configuration

Published on:
·5 min read·Author: MDS Software Solutions Group

Nginx Apache Production

devops

Nginx 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 - .htaccess allows per-directory configuration without server restart
  • PHP/Laravel applications - native integration with mod_php (though PHP-FPM is recommended)
  • Dynamic configuration needed - .htaccess enables 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.

Author
MDS Software Solutions Group

Team of programming experts specializing in modern web technologies.

Nginx vs Apache - Production Web Server Configuration | MDS Software Solutions Group | MDS Software Solutions Group