How to Configure Virtual Hosts with Different Document Roots

How to configure virtual hosts with different document roots

When I started freelancing, I was hosting three or four small client sites on a single VPS, and I quickly realized I needed each one to live in its own folder with its own configuration. That’s exactly what document roots are for. In this post I’ll show you exactly how I set up multiple Apache virtual hosts, each pointing to a different document root, so you can host several independent websites from one server.

What Is a DocumentRoot and Why It Matters

The DocumentRoot directive tells Apache which folder on disk contains the files for a given website. When a visitor requests example.com/about.html, Apache looks inside that site’s configured DocumentRoot for about.html. Without properly separated document roots, all your sites would end up sharing the same files, or worse, one misconfigured site could accidentally expose another’s content.

I use this setup constantly for:

  • Hosting multiple client websites on a single server
  • Running separate staging and production environments
  • Testing multiple versions of an app side-by-side
  • Isolating a blog, a portfolio, and an API on the same box

Prerequisites

  • Apache installed and running on Linux
  • Root or sudo access
  • At least two domains or subdomains you control (or can simulate via /etc/hosts)
  • Basic command-line familiarity

Step 1: Create Separate Directories for Each Site

I start by creating a clean folder structure under /var/www/:

sudo mkdir -p /var/www/site1.com/public_html
sudo mkdir -p /var/www/site2.com/public_html

I keep a public_html subfolder inside each site’s directory as a habit — it separates web-facing files from logs or other assets I might want to store alongside the site later.

I then create a simple test page for each so I can confirm things are working:

echo "<h1>Welcome to Site 1</h1>" | sudo tee /var/www/site1.com/public_html/index.html
echo "<h1>Welcome to Site 2</h1>" | sudo tee /var/www/site2.com/public_html/index.html

Step 2: Set Correct Ownership and Permissions

Apache needs to be able to read these files. I set ownership to the web server user:

sudo chown -R www-data:www-data /var/www/site1.com
sudo chown -R www-data:www-data /var/www/site2.com
sudo chmod -R 755 /var/www/site1.com
sudo chmod -R 755 /var/www/site2.com

On CentOS/RHEL, replace www-data with apache.

Step 3: Create a Virtual Host File for Each Site

On Debian/Ubuntu, I create individual config files inside /etc/apache2/sites-available/:

sudo nano /etc/apache2/sites-available/site1.com.conf
<VirtualHost *:80>
    ServerName site1.com
    ServerAlias www.site1.com
    DocumentRoot /var/www/site1.com/public_html

    <Directory /var/www/site1.com/public_html>
        Options Indexes FollowSymLinks
        AllowOverride All
        Require all granted
    </Directory>

    ErrorLog ${APACHE_LOG_DIR}/site1.com_error.log
    CustomLog ${APACHE_LOG_DIR}/site1.com_access.log combined
</VirtualHost>

I repeat the same for the second site:

sudo nano /etc/apache2/sites-available/site2.com.conf
<VirtualHost *:80>
    ServerName site2.com
    ServerAlias www.site2.com
    DocumentRoot /var/www/site2.com/public_html

    <Directory /var/www/site2.com/public_html>
        Options Indexes FollowSymLinks
        AllowOverride All
        Require all granted
    </Directory>

    ErrorLog ${APACHE_LOG_DIR}/site2.com_error.log
    CustomLog ${APACHE_LOG_DIR}/site2.com_access.log combined
</VirtualHost>

On CentOS/RHEL, I place equivalent blocks in files under /etc/httpd/conf.d/, since that directory is loaded automatically.

Step 4: Enable the Sites and Reload Apache

On Debian/Ubuntu:

sudo a2ensite site1.com.conf
sudo a2ensite site2.com.conf
sudo systemctl reload apache2

On CentOS/RHEL, since files in conf.d are auto-loaded, I just restart the service:

sudo systemctl restart httpd

Step 5: Test with configtest and Verify Load Order

Before going further, I always run:

sudo apache2ctl configtest

Then I confirm both virtual hosts loaded correctly:

sudo apache2ctl -S

I should see both site1.com and site2.com listed with their respective document roots.

Step 6: Point DNS or Test Locally

If DNS isn’t set up yet, I test locally by editing /etc/hosts on my own machine (not the server):

203.0.113.10   site1.com
203.0.113.10   site2.com

Then I visit each domain in a browser to confirm they load their correct, distinct content.

Real-World Example: Staging and Production on One Server

A pattern I use often is running a production site alongside a staging copy:

<VirtualHost *:80>
    ServerName example.com
    DocumentRoot /var/www/example.com/production
</VirtualHost>

<VirtualHost *:80>
    ServerName staging.example.com
    DocumentRoot /var/www/example.com/staging
</VirtualHost>

Same server, same Apache instance, two totally separate document roots — perfect for testing changes before pushing live.

Step 7: Organizing Document Roots for Larger Numbers of Sites

Once I’m managing more than a handful of sites, a flat /var/www/ structure starts to feel messy. I’ve settled on a convention where each site gets its own top-level folder named after its domain, with subfolders for the actual web content, logs, and any backups:

/var/www/site1.com/
├── public_html/
├── logs/
└── backups/

I then point DocumentRoot specifically at public_html, never at the site’s root folder directly. This keeps logs, backups, and deployment scripts completely outside the web-accessible area, so a misconfiguration can’t accidentally expose them to the public internet.

Step 8: Using an Include Directory for Cleaner Management

Rather than editing one giant Apache config file, I split every site into its own file and let Apache’s Include directive pull them all in automatically. This is effectively what sites-available/sites-enabled already does on Debian-based systems, but on CentOS/RHEL I replicate the same pattern manually by confirming my main config includes:

IncludeOptional conf.d/*.conf

This means adding a new site with its own document root is as simple as dropping a new file into that directory and reloading Apache — no need to touch the main configuration file at all.

Troubleshooting Common Issues

All domains show the same content This usually means ServerName is missing or duplicated, so Apache defaults to the first matching virtual host. Check apache2ctl -S for the load order.

403 Forbidden error Check both file permissions and the <Directory> block — a missing Require all granted (Apache 2.4+) will block access even if permissions are correct.

404 errors on a working site Double check the exact path in DocumentRoot — a typo here is one of the most common mistakes.

Apache won’t reload after adding a new vhost Run apache2ctl configtest — it almost always points to a missing closing tag or stray character in the new config file.

Security Best Practices

  • I never place sensitive files (like .env or database backups) inside a public DocumentRoot.
  • I explicitly disable directory listing with Options -Indexes unless I actually want visitors browsing my file structure.
  • I keep every site’s logs separate, which makes auditing access far easier if something goes wrong.

Performance Optimization Tips

  • I keep static assets (images, CSS, JS) properly cached using mod_expires or mod_headers, configured per virtual host if needed.
  • For high-traffic sites, I consider using separate PHP-FPM pools per document root so one site’s load doesn’t affect another’s performance.

Lessons Learned Managing Dozens of Document Roots

The biggest lesson I’ve taken from years of doing this is that consistency beats cleverness. Early on I tried a few “smart” shortcuts — symlinking shared assets between sites, reusing a single document root with conditional logic to serve different content. Every one of those approaches eventually caused a confusing bug that took far longer to untangle than just keeping each site boring, isolated, and predictable would have. Now I treat every new site the same way, following the exact same folder and virtual host pattern regardless of how small or temporary it seems at the time, because temporary sites have a funny way of becoming permanent.

Step 9: Setting Different PHP Versions per Document Root

One scenario where separate document roots really prove their worth is running different PHP versions for different clients — an older site that hasn’t been updated to run on modern PHP alongside a newer one that requires the latest version. With separate document roots and separate PHP-FPM pools, I point each virtual host at its own pool’s socket:

<FilesMatch \.php$>
    SetHandler "proxy:unix:/run/php/php7.4-fpm-site1.sock|fcgi://localhost"
</FilesMatch>
<FilesMatch \.php$>
    SetHandler "proxy:unix:/run/php/php8.3-fpm-site2.sock|fcgi://localhost"
</FilesMatch>

This lets two completely different PHP versions run side by side on the same physical server, each scoped cleanly to its own document root, without either one interfering with the other.

Frequently Asked Questions

Can two virtual hosts share the same DocumentRoot? Yes, though it’s uncommon — you might do this temporarily while migrating a domain, but generally each site should have its own root for isolation.

Do I need a separate IP address for each document root? No. Name-based virtual hosting lets you host many domains, each with its own document root, on a single IP address.

What happens if two virtual hosts have the same ServerName? Apache will just use whichever one loads first, which is rarely what you want. Always keep ServerName values unique.

Can I use subdomains instead of separate domains? Absolutely — the process is identical, just use a subdomain like blog.example.com as your ServerName.

Summary and Key Takeaways

Setting up multiple document roots on Apache is really about discipline: keep each site in its own folder, give each a dedicated virtual host file, get permissions right, and always verify with configtest and apache2ctl -S before assuming it works. Once that pattern is in place, adding a fifth or sixth site becomes routine instead of stressful.

References

Total
1
Shares
Leave a Reply
Previous Post
How to use SNI (Server Name Indication) with Apache

How to Use SNI (Server Name Indication) with Apache

Next Post
How to troubleshoot virtual host configuration issues

How to Troubleshoot Virtual Host Configuration Issues

Related Posts