Apache Basics & When to Use What
Why This Matters
Apache HTTP Server (commonly just "Apache") has been around since 1995. It was the dominant web server for over two decades and still powers roughly a quarter of all websites. Many enterprise applications, hosting control panels (cPanel, Plesk), and PHP applications (WordPress, Drupal, Magento) are built with Apache in mind.
Even if your new projects use Nginx, you will inevitably encounter Apache in production environments, inherited infrastructure, or specific situations where Apache is the better choice. Understanding Apache is not optional -- it is a professional requirement.
This chapter covers Apache's architecture, configuration, key modules, and provides clear guidance on when to choose Apache versus Nginx.
Try This Right Now
On a Debian/Ubuntu system:
$ sudo apt update && sudo apt install -y apache2
$ sudo systemctl start apache2
$ curl -I http://localhost
You should see:
HTTP/1.1 200 OK
Server: Apache/2.4.58 (Ubuntu)
Content-Type: text/html; charset=UTF-8
...
Distro Note: On RHEL/CentOS/Fedora, the package is called
httpd, notapache2:$ sudo dnf install -y httpd $ sudo systemctl start httpdThe service name, binary name, and configuration paths all differ between Debian and RHEL families. We will cover both.
Apache vs Nginx: When to Use Each
Before diving into Apache's internals, let us address the question you are already asking.
┌──────────────────────────────────────────────────────────────┐
│ Apache vs Nginx │
├────────────────────┬─────────────────────────────────────────┤
│ │ Apache │ Nginx │
├────────────────────┼──────────────────────┼───────────────────┤
│ Architecture │ Process/thread-based │ Event-driven │
│ Config model │ Distributed (.htaccess)│ Centralized │
│ Module loading │ Dynamic (at runtime) │ Mostly compile-time│
│ PHP integration │ mod_php (embedded) │ PHP-FPM (external)│
│ Static files │ Good │ Excellent │
│ Reverse proxy │ Good (mod_proxy) │ Excellent │
│ Concurrency │ Good (event MPM) │ Excellent │
│ Memory per conn │ Higher │ Lower │
│ .htaccess support │ Yes │ No │
│ Shared hosting │ Better (per-dir conf)│ Not suited │
│ Learning curve │ Gentle │ Moderate │
└────────────────────┴──────────────────────┴───────────────────┘
Choose Apache when:
- You need
.htaccessfiles (shared hosting, user-controlled directories) - You are running PHP with
mod_phpand want simplicity - You need dynamic module loading without recompiling
- You are maintaining existing Apache infrastructure
- You need per-directory configuration overrides
Choose Nginx when:
- You need maximum performance for static files and reverse proxying
- You are handling a large number of concurrent connections
- You want a simpler, centralized configuration model
- You are building microservice architectures with many backends
- Memory efficiency matters (containers, cloud instances)
The most common pattern in production: Nginx in front as a reverse proxy, with Apache behind running PHP or legacy applications. You get the best of both worlds.
Apache Architecture: MPM Models
Apache uses Multi-Processing Modules (MPMs) to determine how it handles connections. Understanding MPMs is key to understanding Apache's behavior and performance.
prefork MPM
┌──────────────────────────────────────────────────────────┐
│ prefork MPM │
│ │
│ ┌──────────────┐ │
│ │ Master Process│ │
│ └──────┬───────┘ │
│ │ │
│ ┌────┼────────────────────────┐ │
│ │ │ │ │ │
│ ┌─┴─┐ ┌┴──┐ ┌───┴┐ ┌────┐ ┌───┴┐ │
│ │P1 │ │P2 │ │ P3 │ │ P4 │ │ P5 │ One process per │
│ │1 │ │1 │ │ 1 │ │ 1 │ │ 1 │ connection. │
│ │req│ │req│ │ req│ │ req│ │ req│ No threads. │
│ └───┘ └───┘ └────┘ └────┘ └────┘ Safe for non- │
│ thread-safe libs. │
└──────────────────────────────────────────────────────────┘
- Creates a separate process for each connection
- Maximum compatibility (safe for non-thread-safe PHP modules)
- Highest memory usage (each process is a full copy)
- Use case: Legacy PHP applications with non-thread-safe extensions
worker MPM
┌──────────────────────────────────────────────────────────┐
│ worker MPM │
│ │
│ ┌──────────────┐ │
│ │ Master Process│ │
│ └──────┬───────┘ │
│ │ │
│ ┌────┼──────────┐ │
│ │ │ │ │
│ ┌─┴──────┐ ┌─────┴───┐ │
│ │Process 1│ │Process 2│ Each process has multiple │
│ │ T1 T2 │ │ T1 T2 │ threads. Each thread handles │
│ │ T3 T4 │ │ T3 T4 │ one connection. │
│ │ T5 T6 │ │ T5 T6 │ Better memory usage than │
│ └────────┘ └─────────┘ prefork. │
└──────────────────────────────────────────────────────────┘
- Each process spawns multiple threads
- Each thread handles one connection
- Less memory than prefork (threads share process memory)
- Use case: High-traffic sites with thread-safe applications
event MPM (Recommended)
┌──────────────────────────────────────────────────────────┐
│ event MPM │
│ │
│ Like worker MPM, but with a dedicated listener thread. │
│ Keep-alive connections don't tie up worker threads. │
│ │
│ ┌──────────┐ │
│ │Process 1 │ Listener thread handles idle keep-alive │
│ │ Listener │ connections. Worker threads only handle │
│ │ W1 W2 W3│ active requests. Much more efficient. │
│ │ W4 W5 W6│ │
│ └──────────┘ │
└──────────────────────────────────────────────────────────┘
- Improves on worker by handling keep-alive connections asynchronously
- A listener thread manages idle connections without consuming a worker thread
- Default on modern Apache installations
- Use case: General purpose, the best choice for most new deployments
Hands-On: Check Your MPM
# Debian/Ubuntu
$ apachectl -V | grep MPM
Server MPM: event
# Or check the loaded modules
$ apachectl -M | grep mpm
mpm_event_module (shared)
# RHEL/CentOS/Fedora
$ httpd -V | grep MPM
Server MPM: event
Switching MPMs (Debian/Ubuntu)
# Disable current MPM, enable a different one
$ sudo a2dismod mpm_event
$ sudo a2enmod mpm_prefork
$ sudo systemctl restart apache2
Safety Warning: Switching MPMs requires a restart (not just reload) and will briefly drop all connections. Do this during a maintenance window.
Configuration File Structure
Debian/Ubuntu Layout
/etc/apache2/
├── apache2.conf # Main config
├── ports.conf # Listen directives (ports 80, 443)
├── envvars # Environment variables (user, group, paths)
├── sites-available/ # All site configs
│ ├── 000-default.conf # Default HTTP site
│ └── default-ssl.conf # Default HTTPS site template
├── sites-enabled/ # Symlinks to active sites
│ └── 000-default.conf -> ../sites-available/000-default.conf
├── mods-available/ # All available modules
├── mods-enabled/ # Symlinks to active modules
├── conf-available/ # Additional config fragments
└── conf-enabled/ # Active config fragments
RHEL/CentOS/Fedora Layout
/etc/httpd/
├── conf/
│ └── httpd.conf # Main config (everything in one file)
├── conf.d/ # Additional configs (*.conf auto-loaded)
│ ├── ssl.conf # SSL/TLS configuration
│ └── welcome.conf # Default welcome page
├── conf.modules.d/ # Module loading configs
│ ├── 00-base.conf
│ ├── 00-ssl.conf
│ └── ...
└── logs -> /var/log/httpd # Log symlink
Distro Note: The Debian layout is more modular (separate dirs for sites, mods, confs). The RHEL layout is flatter (mostly in
httpd.confandconf.d/). Both achieve the same result.
VirtualHost Configuration
VirtualHosts are Apache's equivalent of Nginx's server blocks.
Basic VirtualHost
$ sudo nano /etc/apache2/sites-available/mysite.conf
<VirtualHost *:80>
ServerName mysite.example.com
ServerAlias www.mysite.example.com
DocumentRoot /var/www/mysite
<Directory /var/www/mysite>
AllowOverride All
Require all granted
</Directory>
ErrorLog ${APACHE_LOG_DIR}/mysite-error.log
CustomLog ${APACHE_LOG_DIR}/mysite-access.log combined
</VirtualHost>
Enable it:
# Debian/Ubuntu
$ sudo a2ensite mysite.conf
$ sudo systemctl reload apache2
# RHEL (just drop the file in conf.d/)
$ sudo cp mysite.conf /etc/httpd/conf.d/
$ sudo systemctl reload httpd
Hands-On: Create a VirtualHost
# Create document root and content
$ sudo mkdir -p /var/www/mysite
$ echo '<h1>Apache says hello!</h1>' | sudo tee /var/www/mysite/index.html
$ sudo chown -R www-data:www-data /var/www/mysite
# Create VirtualHost config
$ sudo tee /etc/apache2/sites-available/mysite.conf > /dev/null << 'EOF'
<VirtualHost *:80>
ServerName mysite.example.com
DocumentRoot /var/www/mysite
<Directory /var/www/mysite>
AllowOverride All
Require all granted
</Directory>
ErrorLog ${APACHE_LOG_DIR}/mysite-error.log
CustomLog ${APACHE_LOG_DIR}/mysite-access.log combined
</VirtualHost>
EOF
# Enable and reload
$ sudo a2ensite mysite.conf
$ sudo apache2ctl configtest # Test config (like nginx -t)
Syntax OK
$ sudo systemctl reload apache2
# Test
$ curl -H "Host: mysite.example.com" http://localhost
<h1>Apache says hello!</h1>
Disabling Sites
# Debian/Ubuntu
$ sudo a2dissite mysite.conf
$ sudo systemctl reload apache2
Think About It: Both Apache and Nginx use the
Hostheader to route requests to virtual hosts. What happens if you do not set aServerNamein your VirtualHost? (Answer: Apache will use the first VirtualHost it finds as the default, similar to Nginx'sdefault_server.)
The .htaccess File
This is Apache's killer feature that Nginx does not have. .htaccess files allow per-directory configuration without editing the main config or reloading Apache.
How .htaccess Works
When Apache receives a request for /var/www/mysite/blog/post.html, it checks for .htaccess files in every directory along the path:
/var/www/.htaccess (if exists, apply it)
/var/www/mysite/.htaccess (if exists, apply it -- overrides parent)
/var/www/mysite/blog/.htaccess (if exists, apply it -- overrides parent)
Enabling .htaccess
.htaccess processing is controlled by AllowOverride:
<Directory /var/www/mysite>
AllowOverride All # Allow .htaccess to override everything
Require all granted
</Directory>
AllowOverride options:
None--.htaccessfiles are completely ignored (best for performance)All--.htaccesscan override any directiveFileInfo-- allows MIME types, redirects, rewritingAuthConfig-- allows authentication directivesIndexes-- allows directory index settings
Common .htaccess Uses
# /var/www/mysite/.htaccess
# Redirect HTTP to HTTPS
RewriteEngine On
RewriteCond %{HTTPS} off
RewriteRule ^ https://%{HTTP_HOST}%{REQUEST_URI} [L,R=301]
# Custom error pages
ErrorDocument 404 /errors/404.html
ErrorDocument 500 /errors/500.html
# Password protection
AuthType Basic
AuthName "Restricted Area"
AuthUserFile /etc/apache2/.htpasswd
Require valid-user
# Block access to sensitive files
<FilesMatch "\.(env|git|htpasswd|log)$">
Require all denied
</FilesMatch>
# Cache static assets
<IfModule mod_expires.c>
ExpiresActive On
ExpiresByType image/jpeg "access plus 1 year"
ExpiresByType text/css "access plus 1 month"
ExpiresByType application/javascript "access plus 1 month"
</IfModule>
Safety Warning: Every
.htaccessfile causes a filesystem stat on every request for every directory in the path. On high-traffic sites, this has measurable performance impact. In production, prefer putting directives in the main config and settingAllowOverride None.
mod_rewrite: URL Rewriting
mod_rewrite is one of Apache's most powerful and most confusing modules. It rewrites URLs based on regular expressions.
Enabling mod_rewrite
$ sudo a2enmod rewrite
$ sudo systemctl restart apache2
Common Rewrite Rules
# In VirtualHost or .htaccess
RewriteEngine On
# Redirect old URLs to new ones
RewriteRule ^/old-page$ /new-page [R=301,L]
# Pretty URLs: /products/42 -> /product.php?id=42
RewriteRule ^/products/([0-9]+)$ /product.php?id=$1 [L,QSA]
# Remove trailing slashes
RewriteCond %{REQUEST_FILENAME} !-d
RewriteRule ^(.+)/$ /$1 [R=301,L]
# Front controller pattern (like WordPress, Laravel)
RewriteCond %{REQUEST_FILENAME} !-f
RewriteCond %{REQUEST_FILENAME} !-d
RewriteRule ^ /index.php [L]
Rewrite flags:
[L]-- Last rule, stop processing[R=301]-- External redirect with status code[QSA]-- Append query string to the rewritten URL[NC]-- Case-insensitive matching[F]-- Return 403 Forbidden
Hands-On: Set Up URL Rewriting
# Enable mod_rewrite
$ sudo a2enmod rewrite
$ sudo systemctl restart apache2
# Create a test site
$ sudo mkdir -p /var/www/rewrite-demo
$ echo "Main page" | sudo tee /var/www/rewrite-demo/index.html
$ echo "User profile page" | sudo tee /var/www/rewrite-demo/user.php
# Create .htaccess with rewrite rules
$ sudo tee /var/www/rewrite-demo/.htaccess > /dev/null << 'EOF'
RewriteEngine On
# /users/alice -> /user.php?name=alice
RewriteRule ^users/([a-z]+)$ user.php?name=$1 [L,QSA]
EOF
# Update VirtualHost to allow overrides
# (ensure AllowOverride All in the Directory block)
# Test
$ curl "http://localhost/users/alice"
# Should serve user.php with name=alice
Enabling and Disabling Modules
Apache's module system is one of its greatest strengths. Modules are loaded dynamically -- no recompilation needed.
Debian/Ubuntu: a2enmod / a2dismod
# List available modules
$ ls /etc/apache2/mods-available/ | head -20
# List enabled modules
$ apachectl -M
# Enable a module
$ sudo a2enmod ssl
$ sudo a2enmod headers
$ sudo a2enmod proxy
$ sudo a2enmod proxy_http
# Disable a module
$ sudo a2dismod autoindex
# Always restart after changing modules
$ sudo systemctl restart apache2
RHEL/CentOS/Fedora
On RHEL, modules are managed through config files in /etc/httpd/conf.modules.d/:
# List loaded modules
$ httpd -M
# Modules are loaded via LoadModule directives
$ cat /etc/httpd/conf.modules.d/00-base.conf
LoadModule mpm_event_module modules/mod_mpm_event.so
LoadModule unixd_module modules/mod_unixd.so
...
# To disable a module, comment out its LoadModule line
$ sudo sed -i 's/^LoadModule autoindex/# LoadModule autoindex/' /etc/httpd/conf.modules.d/00-base.conf
$ sudo systemctl restart httpd
Essential Modules
| Module | Purpose |
|---|---|
mod_ssl | HTTPS/TLS support |
mod_rewrite | URL rewriting |
mod_headers | Custom HTTP headers |
mod_proxy | Reverse proxy functionality |
mod_proxy_http | HTTP backend proxying |
mod_proxy_wstunnel | WebSocket proxying |
mod_deflate | Response compression (gzip) |
mod_expires | Cache control headers |
mod_auth_basic | HTTP Basic authentication |
mod_security | Web Application Firewall (WAF) |
Basic Authentication
Apache has built-in support for HTTP Basic Auth:
# Create a password file
$ sudo htpasswd -c /etc/apache2/.htpasswd admin
New password: ****
Re-type new password: ****
Adding password for user admin
# Add another user (-c creates the file; omit for subsequent users)
$ sudo htpasswd /etc/apache2/.htpasswd developer
In VirtualHost config:
<VirtualHost *:80>
ServerName internal.example.com
DocumentRoot /var/www/internal
<Directory /var/www/internal>
AuthType Basic
AuthName "Internal Access Only"
AuthUserFile /etc/apache2/.htpasswd
Require valid-user
</Directory>
</VirtualHost>
Or in .htaccess:
AuthType Basic
AuthName "Restricted"
AuthUserFile /etc/apache2/.htpasswd
Require user admin
Test it:
# Without credentials (401 Unauthorized)
$ curl -I http://internal.example.com
HTTP/1.1 401 Unauthorized
# With credentials
$ curl -u admin:password http://internal.example.com
Apache as a Reverse Proxy
Apache can function as a reverse proxy using mod_proxy:
# Enable required modules
$ sudo a2enmod proxy proxy_http proxy_balancer lbmethod_byrequests
$ sudo systemctl restart apache2
Simple Reverse Proxy
<VirtualHost *:80>
ServerName api.example.com
ProxyPreserveHost On
ProxyPass / http://127.0.0.1:3000/
ProxyPassReverse / http://127.0.0.1:3000/
ErrorLog ${APACHE_LOG_DIR}/api-error.log
CustomLog ${APACHE_LOG_DIR}/api-access.log combined
</VirtualHost>
ProxyPreserveHost On-- forward the originalHostheader to the backendProxyPass-- forward requests to the backendProxyPassReverse-- rewriteLocationheaders in responses so redirects work correctly
Load Balancing with mod_proxy_balancer
<VirtualHost *:80>
ServerName app.example.com
<Proxy "balancer://app_cluster">
BalancerMember http://10.0.1.10:3000
BalancerMember http://10.0.1.11:3000
BalancerMember http://10.0.1.12:3000
ProxySet lbmethod=byrequests # Round-robin
</Proxy>
ProxyPreserveHost On
ProxyPass / balancer://app_cluster/
ProxyPassReverse / balancer://app_cluster/
</VirtualHost>
Load balancing methods:
byrequests-- round-robin by request countbytraffic-- distribute by bytes transferredbybusyness-- send to least busy workerheartbeat-- use heartbeat monitoring
Hands-On: Complete Apache Setup
Let us set up a realistic Apache configuration:
# 1. Install Apache and enable essential modules
$ sudo apt update && sudo apt install -y apache2
$ sudo a2enmod rewrite ssl headers proxy proxy_http
$ sudo systemctl restart apache2
# 2. Create a site with URL rewriting and security headers
$ sudo mkdir -p /var/www/production
$ echo '<h1>Production Site</h1>' | sudo tee /var/www/production/index.html
$ sudo chown -R www-data:www-data /var/www/production
# 3. Create the VirtualHost
$ sudo tee /etc/apache2/sites-available/production.conf > /dev/null << 'CONF'
<VirtualHost *:80>
ServerName www.example.com
DocumentRoot /var/www/production
<Directory /var/www/production>
AllowOverride None
Require all granted
# Rewrite rules (in config, not .htaccess, for performance)
RewriteEngine On
RewriteCond %{REQUEST_FILENAME} !-f
RewriteCond %{REQUEST_FILENAME} !-d
RewriteRule ^ /index.html [L]
</Directory>
# Security headers
Header always set X-Frame-Options "SAMEORIGIN"
Header always set X-Content-Type-Options "nosniff"
Header always set X-XSS-Protection "1; mode=block"
Header always set Referrer-Policy "strict-origin-when-cross-origin"
# Hide Apache version
ServerSignature Off
# Block hidden files
<FilesMatch "^\.">
Require all denied
</FilesMatch>
# Logging
ErrorLog ${APACHE_LOG_DIR}/production-error.log
CustomLog ${APACHE_LOG_DIR}/production-access.log combined
</VirtualHost>
CONF
# 4. Enable the site, disable the default
$ sudo a2ensite production.conf
$ sudo a2dissite 000-default.conf
$ sudo apache2ctl configtest
$ sudo systemctl reload apache2
# 5. Test
$ curl -I http://localhost
HTTP/1.1 200 OK
X-Frame-Options: SAMEORIGIN
X-Content-Type-Options: nosniff
Debug This
After enabling mod_rewrite and adding rewrite rules in .htaccess, your URLs are not being rewritten. All requests return the literal file path.
Debugging steps:
# 1. Is mod_rewrite loaded?
$ apachectl -M | grep rewrite
rewrite_module (shared)
# If this is empty, run: sudo a2enmod rewrite && sudo systemctl restart apache2
# 2. Is AllowOverride set correctly?
$ grep -A3 "Directory /var/www" /etc/apache2/sites-enabled/mysite.conf
# If AllowOverride is "None", .htaccess is being ignored.
# Change to "AllowOverride All" (or "AllowOverride FileInfo")
# 3. Is RewriteEngine On in .htaccess?
$ cat /var/www/mysite/.htaccess
# Forgetting "RewriteEngine On" is the #1 mistake
# 4. Check Apache error log for rewrite issues
$ sudo tail -20 /var/log/apache2/error.log
# 5. Enable rewrite logging (temporarily, for debugging)
# In VirtualHost:
# LogLevel alert rewrite:trace3
$ sudo systemctl reload apache2
# Now check error.log for detailed rewrite processing
Common causes:
AllowOverride Nonein the VirtualHost (most common)mod_rewritenot enabled- Missing
RewriteEngine Onin.htaccess .htaccessfile has wrong permissions (Apache cannot read it)
Configuration Testing
Always test Apache configuration before reloading:
# Debian/Ubuntu
$ sudo apache2ctl configtest
Syntax OK
# Or
$ sudo apachectl -t
Syntax OK
# RHEL/CentOS
$ sudo httpd -t
Syntax OK
For a verbose dump of the entire parsed configuration:
# Show all virtual hosts
$ sudo apachectl -S
VirtualHost configuration:
*:80 www.example.com (/etc/apache2/sites-enabled/production.conf:1)
# Show all loaded modules
$ sudo apachectl -M
# Show compiled-in modules
$ sudo apachectl -l
What Just Happened?
┌──────────────────────────────────────────────────────────────┐
│ Chapter 46 Recap │
├──────────────────────────────────────────────────────────────┤
│ │
│ Apache HTTP Server uses MPM models for concurrency: │
│ - prefork: one process per connection (legacy) │
│ - worker: threads within processes │
│ - event: async keep-alive handling (recommended) │
│ │
│ Key concepts: │
│ - VirtualHost = virtual host (like Nginx server blocks) │
│ - .htaccess = per-directory config (Apache's unique feature) │
│ - mod_rewrite = powerful URL rewriting │
│ - a2enmod/a2dismod = enable/disable modules (Debian) │
│ │
│ Apache vs Nginx: │
│ - Apache: .htaccess, mod_php, dynamic modules, shared hosting│
│ - Nginx: performance, memory efficiency, reverse proxy │
│ - Best pattern: Nginx in front, Apache behind for PHP/legacy │
│ │
│ Config: /etc/apache2/ (Debian) /etc/httpd/ (RHEL) │
│ Logs: /var/log/apache2/ /var/log/httpd/ │
│ Test: apache2ctl configtest httpd -t │
│ │
└──────────────────────────────────────────────────────────────┘
Try This
Exercise 1: MPM Comparison
Switch between event and prefork MPMs. Use ps aux | grep apache to compare the process model. With prefork, start ab -n 100 -c 10 http://localhost/ (Apache Bench) and watch processes spawn.
$ sudo apt install -y apache2-utils
$ ab -n 1000 -c 50 http://localhost/
Exercise 2: .htaccess Mastery
Create a site with these .htaccess rules:
- Password-protect the
/admin/directory - Set up a custom 404 page
- Redirect
/old-blogto/blogwith a 301 - Block requests with empty User-Agent headers
Exercise 3: Reverse Proxy
Set up Apache as a reverse proxy for a Python (python3 -m http.server 8888) or Node.js backend. Verify that ProxyPreserveHost correctly passes the Host header.
Exercise 4: Module Exploration
Enable mod_status to get a real-time Apache status page:
<Location "/server-status">
SetHandler server-status
Require ip 127.0.0.1
</Location>
Visit http://localhost/server-status and explore the information provided.
Bonus Challenge
Set up Nginx in front of Apache. Nginx handles static files and proxies PHP requests to Apache. Apache runs mod_php to process the PHP. This is the "Nginx + Apache" pattern used by many high-traffic WordPress sites.
What Comes Next
Both Nginx and Apache can do load balancing, but they are web servers first and load balancers second. In the next chapter, we cover HAProxy -- a tool that is a load balancer first and foremost, with features that neither Nginx nor Apache can match.