Apache vs nginx, when to use which
Modern Linux has two mainstream web servers:
- Apache: process-per-request (worker/event MPM), modular,
.htaccess, PHP through mod_php. The standard for shared hosting, the LAMP stack, legacy. - nginx: event-driven, lighter on resources, simpler config, better for static + reverse proxy.
Apache still makes sense when:
- You need
.htaccessedits made by users without a restart. - You want the mod_* ecosystem (mod_wsgi, mod_authnz_ldap, mod_security).
- A ready-made product expects Apache (ZendServer, cPanel, mailman).
Otherwise reach for nginx or Caddy.
Installation, package differences
| Distribution | Package | Daemon | Config | run-as |
|---|---|---|---|---|
| RHEL/Fedora | httpd | httpd | /etc/httpd/conf/httpd.conf + conf.d/ | apache |
| Debian/Ubuntu | apache2 | apache2 | /etc/apache2/apache2.conf + sites-enabled/ | www-data |
# RHEL
sudo dnf install httpd
sudo systemctl enable --now httpd
# Debian
sudo apt install apache2
sudo systemctl enable --now apache2
Below httpd is used for brevity. On Debian replace it with apache2.
Directory layout on Debian (the "sites-available" model)
/etc/apache2/
├── apache2.conf ← main file, reads all the others
├── ports.conf ← Listen 80, 443
├── conf-available/ ← fragments
├── conf-enabled/ ← symlinks to active ones
├── mods-available/ ← all modules (.load + .conf)
├── mods-enabled/ ← symlinks to active modules
├── sites-available/ ← virtual host configs
│ ├── 000-default.conf
│ └── example.org.conf
└── sites-enabled/ ← symlinks to active ones
└── example.org.conf -> ../sites-available/example.org.conf
Activation through helper utilities:
sudo a2ensite example.org # symlink into sites-enabled
sudo a2dissite example.org # remove it
sudo a2enmod ssl rewrite # enable modules
sudo a2dismod autoindex
sudo systemctl reload apache2
RHEL has no such split. All configs are dropped into /etc/httpd/conf.d/*.conf,
modules load through LoadModule in /etc/httpd/conf.modules.d/*.conf.
Process model, why root and www-data at the same time
Apache starts as root so it can open [[capabilities|ports below 1024]]
(80, 443 require CAP_NET_BIND_SERVICE), then forks workers as an
unprivileged user (www-data or apache).
ps -ef | grep httpd
# root 1234 1 0 /usr/sbin/httpd -DFOREGROUND ← parent (master)
# apache 1235 1234 0 /usr/sbin/httpd -DFOREGROUND ← worker
# apache 1236 1234 0 /usr/sbin/httpd -DFOREGROUND ← worker
Workers handle the requests. If one of them holds a vulnerability, the exploit
gets only apache permissions, not root. So /var/www/html/ must be
readable by apache but not writable (otherwise RCE leads to a disk write).
VirtualHost, several sites on one IP
One host can serve several domains on one IP by the Host:
header from HTTP/1.1.
# /etc/apache2/sites-available/example.org.conf
<VirtualHost *:80>
ServerName example.org
ServerAlias www.example.org
DocumentRoot /var/www/example.org
ErrorLog ${APACHE_LOG_DIR}/example.org-error.log CustomLog ${APACHE_LOG_DIR}/example.org-access.log combined<Directory /var/www/example.org>
Options -Indexes +FollowSymLinks
AllowOverride All # allow .htaccess
Require all granted
</Directory>
</VirtualHost>
Gotchas:
- The first VirtualHost is the default: requests with an unknown
Host:fall into it. Often you want a default config that returns 444 to defend against "spam requests to the IP". *:80vsIP:80:*catches every interface, an explicit IP only that one.<Directory>vs<Location>vs<Files>: a directory on disk vs a URL path vs a file pattern. Mixing them up is dangerous: access control rules may silently fail to apply.
HTTPS, mod_ssl + Let's Encrypt
# 1. Enable the module
sudo a2enmod ssl
sudo systemctl reload apache2
# 2. Get a certificate (certbot writes the config itself)
sudo apt install certbot python3-certbot-apache
sudo certbot --apache -d example.org -d www.example.org
# certbot creates:
# /etc/apache2/sites-available/example.org-le-ssl.conf
A minimal SSL VirtualHost by hand:
<VirtualHost *:443>
ServerName example.org
DocumentRoot /var/www/example.org
SSLEngine on
SSLCertificateFile /etc/letsencrypt/live/example.org/fullchain.pem
SSLCertificateKeyFile /etc/letsencrypt/live/example.org/privkey.pem
# Modern ciphers
SSLProtocol all -SSLv3 -TLSv1 -TLSv1.1
SSLCipherSuite HIGH:!aNULL:!MD5
SSLHonorCipherOrder on
</VirtualHost>
The tls-handshake itself has its own article.
Reverse proxy for a backend application
<VirtualHost *:80>
ServerName api.example.org
ProxyPreserveHost On
ProxyPass / http://127.0.0.1:8000/
ProxyPassReverse / http://127.0.0.1:8000/
RequestHeader set X-Forwarded-Proto "http"
</VirtualHost>
You need the proxy and proxy_http modules:
sudo a2enmod proxy proxy_http
The backend (uvicorn/gunicorn/etc.) listens on loopback:8000, Apache on 80/443 serves TLS, headers, and rate limits.
.htaccess, per-directory rules without a reload
Apache's main feature is the .htaccess file: in any directory it can hold
config directives and applies WITHOUT a server restart. Handy for
shared hosting where the user gets only FTP access.
# /var/www/example.org/.htaccess
Options -Indexes
ErrorDocument 404 /404.html
RewriteEngine On
RewriteRule ^old-page$ /new-page [R=301,L]
For this to work, the VirtualHost must set AllowOverride All
for the directory. The default is None (.htaccess is ignored).
The cost: Apache reads .htaccess on every request in that hierarchy. Under high load, turn it off and move the rules into the main config.
Management
sudo systemctl reload httpd # re-read config without dropping connections
sudo systemctl restart httpd # full restart
sudo apachectl configtest # syntax check (== httpd -t)
sudo apachectl -S # show all VirtualHosts and their rules
sudo apachectl -M # list loaded modules
Always run configtest before reload. Otherwise a syntax error stops the
daemon from restarting, and the site goes down.
Logs
RHEL: /var/log/httpd/{access,error}_log.
Debian: /var/log/apache2/{access,error}.log.
sudo tail -f /var/log/apache2/error.log # errors in real time
sudo tail -f /var/log/apache2/access.log # requests
The combined format (default):
192.168.0.5 - - [29/Apr/2026:14:23:11 +0300] "GET / HTTP/1.1" 200 1234 "-" "Mozilla/5.0..."
↑ ↑ ↑ ↑
client request code size
The logrotate config is usually /etc/logrotate.d/httpd, rotating daily/weekly.
Security, the required minimum
- Do not run as root post-fork: confirm
User apache/User www-datais set in the config. - Hide the version:
ServerTokens Prod+ServerSignature Offinapache2.conf. - Disable directory listing:
Options -Indexesfor every<Directory>. - Block .git/.env:
<DirectoryMatch "/\\.git">+Require all denied. - HSTS:
Header always set Strict-Transport-Security "max-age=63072000". - fail2ban on error.log to ban whoever keeps hammering 404 and 401.
- mod_security + the OWASP ruleset, a WAF for the common attacks.
Debugging, the typical problems
# 1. Running?
systemctl status httpd
ss -tlnp 'sport = :80' # is it listening on the port?
# 2. Where the logs go in real time
sudo tail -F /var/log/httpd/error_log
# 3. Which VirtualHost matches a name
sudo apachectl -S | grep example.org
# 4. A simple curl test
curl -I http://localhost
curl -kvI https://example.org
Frequent errors:
- 403 Forbidden on a clean site points to SELinux on RHEL. Check
ls -Z /var/www/html, you need thehttpd_sys_content_tcontext.sudo restorecon -R /var/www/html. - Permission denied in the log means
apache/www-datacannot read the DocumentRoot.chmod o+rX -Ror the right ACL. - Address already in use at startup means someone else is already on 80
(
ss -tlnp 'sport = :80'finds it).