KB
Web Servers

Apache Virtual Hosts and Server Administration

6 min read1121 words18 code blocks

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 / DebianRHEL / Rocky / AlmaLinux
Serviceapache2httpd
Config root/etc/apache2//etc/httpd/
Main configapache2.confconf/httpd.conf
Site configssites-available/ + sites-enabled/conf.d/*.conf
Enable a sitea2ensiteDrop a file in conf.d/
Modulesa2enmod / a2dismodconf.modules.d/*.conf
Test configapache2ctl configtesthttpd -t
Runs aswww-dataapache
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 sudo access.
  • DNS already resolving the hostname to this server.
  • The document root path decided.

Confirm Apache is installed and running:

bash
apache2 -v || httpd -v
sudo systemctl status apache2 || sudo systemctl status httpd

Creating a virtual host#

1. Create the document root#

bash
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.html
Note: On RHEL the web user is apache, not www-data. Getting this wrong produces 403 errors that look like a permissions bug in the application.

2. Create the virtual host file#

bash
sudo nano /etc/apache2/sites-available/example.com.conf
apache
<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:

DirectivePurpose
ServerNamePrimary hostname this block answers for
ServerAliasAdditional hostnames
Options -IndexesPrevents directory listing when no index file exists
AllowOverride AllPermits .htaccess — set to None if unused, it is faster
Require all grantedApache 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#

bash
# Ubuntu / Debian
sudo a2ensite example.com.conf
sudo apache2ctl configtest
sudo systemctl reload apache2

On RHEL the file just needs to live in /etc/httpd/conf.d/, then:

bash
sudo httpd -t
sudo systemctl reload httpd
Note: Use reload, not restart. 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:

bash
sudo a2dissite 000-default.conf
sudo systemctl reload apache2

Common module tasks#

bash
# 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 apache2

Modules 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:

apache
<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>
bash
sudo a2enmod proxy proxy_http
sudo systemctl restart apache2
Note: ProxyPreserveHost On passes the original Host header through. Without it, applications generate redirect URLs pointing at 127.0.0.1 and login loops follow.

Redirects#

apache
# 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#

bash
# 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 -20

Log rotation is configured by the package in /etc/logrotate.d/apache2. Confirm it is working — busy sites fill a partition quickly:

bash
sudo logrotate -d /etc/logrotate.d/apache2

Performance basics#

bash
# Which MPM is in use
apache2ctl -V | grep MPM

event 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:

bash
sudo a2enmod deflate
apache
<IfModule mod_deflate.c>
    AddOutputFilterByType DEFLATE text/html text/plain text/xml text/css \
                                  application/javascript application/json
</IfModule>

Check server status:

bash
sudo a2enmod status
sudo systemctl reload apache2
curl http://localhost/server-status?auto
Warning: Restrict /server-status to localhost. It exposes request details and client IPs.

Verification#

bash
# 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#

SymptomCause and fix
403 ForbiddenPermissions or a missing Require all granted. Check the web user can traverse every parent directory.
Default Apache page instead of the siteVirtual host not enabled, or ServerName does not match. Check apache2ctl -S.
Config test fails after editThe output names the file and line. Fix before reloading.
Apache will not start after restartBroken config, or another process on port 80. Check `sudo ss -tulpn
.htaccess rules ignoredAllowOverride None. Set to All for that directory.
500 Internal Server ErrorApplication error. Read the site's ErrorLog — Apache logs the cause there.
Wrong site served for a domainTwo virtual hosts share a ServerName, so the first match wins. Review apache2ctl -S.
Site works by IP but not hostnameDNS, not Apache. See the DNS troubleshooting article.
High memory usageprefork MPM with mod_php. Move to PHP-FPM and the event MPM.