Apache Virtual Hosts and Server Administration
At a glance#
- Purpose: Configure and administer Apache HTTP Server, including virtual hosts, modules, logging and reverse proxying.
- Applies to: Apache 2.4 on Ubuntu/Debian and RHEL-family systems.
- Risk: Medium — a config error can take every hosted site down at once.
- Time: 45 minutes for a new virtual host.
Overview#
Apache serves multiple sites from one server using virtual hosts, matched by the Host header. The two distribution families lay Apache out very differently, which is the most common source of confusion when moving between servers:
| Ubuntu / Debian | RHEL / Rocky / AlmaLinux | |
|---|---|---|
| Service | apache2 | httpd |
| Config root | /etc/apache2/ | /etc/httpd/ |
| Main config | apache2.conf | conf/httpd.conf |
| Site configs | sites-available/ + sites-enabled/ | conf.d/*.conf |
| Enable a site | a2ensite | Drop a file in conf.d/ |
| Modules | a2enmod / a2dismod | conf.modules.d/*.conf |
| Test config | apache2ctl configtest | httpd -t |
| Runs as | www-data | apache |
| Logs | /var/log/apache2/ | /var/log/httpd/ |
Commands below show the Ubuntu form first; substitute from this table for RHEL.
Before you start#
- Root or
sudoaccess. - DNS already resolving the hostname to this server.
- The document root path decided.
Confirm Apache is installed and running:
apache2 -v || httpd -v
sudo systemctl status apache2 || sudo systemctl status httpdCreating a virtual host#
1. Create the document root#
sudo mkdir -p /var/www/example.com/public_html
sudo chown -R www-data:www-data /var/www/example.com
sudo chmod -R 755 /var/www/example.com
echo "<h1>example.com</h1>" | sudo tee /var/www/example.com/public_html/index.htmlNote: On RHEL the web user isapache, notwww-data. Getting this wrong produces 403 errors that look like a permissions bug in the application.
2. Create the virtual host file#
sudo nano /etc/apache2/sites-available/example.com.conf<VirtualHost *:80>
ServerName example.com
ServerAlias www.example.com
DocumentRoot /var/www/example.com/public_html
<Directory /var/www/example.com/public_html>
Options -Indexes +FollowSymLinks
AllowOverride All
Require all granted
</Directory>
ErrorLog ${APACHE_LOG_DIR}/example.com-error.log
CustomLog ${APACHE_LOG_DIR}/example.com-access.log combined
</VirtualHost>Key directives:
| Directive | Purpose |
|---|---|
ServerName | Primary hostname this block answers for |
ServerAlias | Additional hostnames |
Options -Indexes | Prevents directory listing when no index file exists |
AllowOverride All | Permits .htaccess — set to None if unused, it is faster |
Require all granted | Apache 2.4 access control; the old Allow from all no longer works |
Warning: Options -Indexes matters. Without it, Apache lists the contents of any directory lacking an index file, which routinely exposes backups, config files and database dumps.
3. Enable the site#
# Ubuntu / Debian
sudo a2ensite example.com.conf
sudo apache2ctl configtest
sudo systemctl reload apache2On RHEL the file just needs to live in /etc/httpd/conf.d/, then:
sudo httpd -t
sudo systemctl reload httpdNote: Usereload, notrestart. Reload applies new configuration without dropping active connections. Always run the config test first — a reload with broken config is ignored, but a restart with broken config leaves Apache stopped.
4. Disable the default site#
The default site catches any request that matches no virtual host, and will serve the Apache placeholder page on unconfigured domains:
sudo a2dissite 000-default.conf
sudo systemctl reload apache2Common module tasks#
# List loaded modules
apache2ctl -M
# Enable frequently needed modules
sudo a2enmod rewrite headers ssl proxy proxy_http deflate
# Disable one
sudo a2dismod status
sudo systemctl restart apache2Modules require a restart, not a reload.
Reverse proxy to an application#
Common where an application server listens on localhost and Apache handles TLS and the public port:
<VirtualHost *:80>
ServerName app.example.com
ProxyPreserveHost On
ProxyPass / http://127.0.0.1:8080/
ProxyPassReverse / http://127.0.0.1:8080/
<Proxy *>
Require all granted
</Proxy>
ErrorLog ${APACHE_LOG_DIR}/app-error.log
CustomLog ${APACHE_LOG_DIR}/app-access.log combined
</VirtualHost>sudo a2enmod proxy proxy_http
sudo systemctl restart apache2Note:ProxyPreserveHost Onpasses the originalHostheader through. Without it, applications generate redirect URLs pointing at127.0.0.1and login loops follow.
Redirects#
# Whole site to HTTPS
<VirtualHost *:80>
ServerName example.com
Redirect permanent / https://example.com/
</VirtualHost>
# Single path
Redirect permanent /old-page /new-page
# Non-www to www
<VirtualHost *:80>
ServerName www.example.com
ServerAlias example.com
RewriteEngine On
RewriteCond %{HTTP_HOST} !^www\. [NC]
RewriteRule ^(.*)$ https://www.%{HTTP_HOST}$1 [R=301,L]
</VirtualHost>Log management#
# Live tail
sudo tail -f /var/log/apache2/example.com-access.log
sudo tail -f /var/log/apache2/example.com-error.log
# Top client IPs
sudo awk '{print $1}' /var/log/apache2/example.com-access.log | sort | uniq -c | sort -rn | head -20
# Count responses by status code
sudo awk '{print $9}' /var/log/apache2/example.com-access.log | sort | uniq -c | sort -rn
# Requests returning 500
sudo awk '$9 == 500' /var/log/apache2/example.com-access.log | tail -20
# Most requested URLs
sudo awk '{print $7}' /var/log/apache2/example.com-access.log | sort | uniq -c | sort -rn | head -20Log rotation is configured by the package in /etc/logrotate.d/apache2. Confirm it is working — busy sites fill a partition quickly:
sudo logrotate -d /etc/logrotate.d/apache2Performance basics#
# Which MPM is in use
apache2ctl -V | grep MPMevent is the modern default and handles concurrency best. prefork is required only when using mod_php; switching to PHP-FPM allows event and uses far less memory.
Enable compression:
sudo a2enmod deflate<IfModule mod_deflate.c>
AddOutputFilterByType DEFLATE text/html text/plain text/xml text/css \
application/javascript application/json
</IfModule>Check server status:
sudo a2enmod status
sudo systemctl reload apache2
curl http://localhost/server-status?autoWarning: Restrict /server-status to localhost. It exposes request details and client IPs.
Verification#
# Config valid
sudo apache2ctl configtest
# Site responds with correct content
curl -sI http://example.com | head -1
curl -s http://example.com | head -5
# Virtual host actually matched
apache2ctl -S
# Listening on the expected ports
sudo ss -tulpn | grep -E 'apache|httpd'apache2ctl -S is the fastest way to see which virtual host serves which name, and to spot overlapping ServerName entries.
Troubleshooting#
| Symptom | Cause and fix |
|---|---|
| 403 Forbidden | Permissions or a missing Require all granted. Check the web user can traverse every parent directory. |
| Default Apache page instead of the site | Virtual host not enabled, or ServerName does not match. Check apache2ctl -S. |
| Config test fails after edit | The output names the file and line. Fix before reloading. |
| Apache will not start after restart | Broken config, or another process on port 80. Check `sudo ss -tulpn |
.htaccess rules ignored | AllowOverride None. Set to All for that directory. |
| 500 Internal Server Error | Application error. Read the site's ErrorLog — Apache logs the cause there. |
| Wrong site served for a domain | Two virtual hosts share a ServerName, so the first match wins. Review apache2ctl -S. |
| Site works by IP but not hostname | DNS, not Apache. See the DNS troubleshooting article. |
| High memory usage | prefork MPM with mod_php. Move to PHP-FPM and the event MPM. |