I run several projects off a single VPS, and virtual hosting is the reason I’ve never had to pay for a separate server per site. Apache handles this natively — one IP address, one Apache instance, and as many independent websites as the hardware can comfortably serve. Here’s exactly how I set it up.
Name-Based vs. IP-Based Virtual Hosting
- Name-based virtual hosting (what I use for basically everything): multiple domains share the same IP, and Apache tells them apart using the
Hostheader the browser sends. This is the standard for almost all shared hosting and cloud deployments today. - IP-based virtual hosting (legacy, rarely needed): each site gets its own dedicated IP. This used to be required for SSL before SNI existed; modern TLS makes it mostly unnecessary now.
I’ll focus on name-based virtual hosting here, since that’s what covers essentially every deployment I set up.
Prerequisites
- Apache installed and running
- Root or sudo access
- DNS records for each domain pointing to the server’s IP
- Separate document root directories prepared for each site
Step 1: Create Directory Structure for Each Site
sudo mkdir -p /var/www/site1.com/public_html
sudo mkdir -p /var/www/site2.com/public_html
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
sudo chown -R www-data:www-data /var/www/site1.com /var/www/site2.com
sudo chmod -R 755 /var/www/site1.com /var/www/site2.com
Step 2: Create a Virtual Host File for Each Site
Debian/Ubuntu — I create files under /etc/apache2/sites-available/:
/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>
AllowOverride All
Require all granted
</Directory>
ErrorLog ${APACHE_LOG_DIR}/site1.com-error.log
CustomLog ${APACHE_LOG_DIR}/site1.com-access.log combined
</VirtualHost>
/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>
AllowOverride All
Require all granted
</Directory>
ErrorLog ${APACHE_LOG_DIR}/site2.com-error.log
CustomLog ${APACHE_LOG_DIR}/site2.com-access.log combined
</VirtualHost>
RHEL/CentOS — I create equivalent files under /etc/httpd/conf.d/, e.g. site1.com.conf and site2.com.conf, using the same block structure. RHEL doesn’t use the sites-available/sites-enabled split by default — files dropped directly into conf.d/ are active automatically.
Step 3: Enable the Sites (Debian/Ubuntu)
sudo a2ensite site1.com.conf
sudo a2ensite site2.com.conf
sudo a2dissite 000-default.conf # optional: disable the default placeholder site
Step 4: Test and Reload
sudo apachectl configtest
sudo systemctl reload apache2
Step 5: Verify Each Site Resolves Correctly
curl -I -H "Host: site1.com" http://localhost/
curl -I -H "Host: site2.com" http://localhost/
Or, once DNS has propagated, I just pull up each domain in a browser.
How Apache Decides Which Site to Serve
For name-based virtual hosting, Apache matches incoming requests to a <VirtualHost> block in this order:
- It looks for an exact match on
ServerNameorServerAliasagainst theHostheader. - If nothing matches, it falls back to the first
<VirtualHost>block defined for that IP:port combination — this quietly becomes the default site.
That fallback behavior tripped me up early on: a request arriving with an unrecognized Host header (someone hitting the raw IP, or a misconfigured DNS record) gets served by whichever virtual host happens to be defined first. I define a catch-all default virtual host now to control this explicitly:
<VirtualHost *:80>
ServerName default-catchall
DocumentRoot /var/www/default
<Location />
Require all denied
</Location>
</VirtualHost>
I place this block first (alphabetically or by load order) so it becomes the fallback for anything unmatched.
Adding HTTPS for Multiple Sites (SNI)
Modern TLS supports SNI, letting multiple HTTPS sites with different certificates share one IP — no dedicated IP per site required.
sudo certbot --apache -d site1.com -d www.site1.com
sudo certbot --apache -d site2.com -d www.site2.com
Certbot automatically creates and manages a separate <VirtualHost *:443> block with its own certificate for each domain, alongside the existing port 80 blocks (which it can update to redirect to HTTPS too). I’ve got a full post on SSL/HTTPS setup if you want the details.
Using ServerAlias for Multiple Domain Names on One Site
If several domains or subdomains should serve identical content:
<VirtualHost *:80>
ServerName site1.com
ServerAlias www.site1.com site1.net site1.org
DocumentRoot /var/www/site1.com/public_html
</VirtualHost>
Wildcard Subdomains
To handle any subdomain of a domain within a single virtual host (common for multi-tenant setups):
<VirtualHost *:80>
ServerName example.com
ServerAlias *.example.com
DocumentRoot /var/www/example.com/public_html
# Optional: use %1 to route based on subdomain, combined with mod_rewrite
</VirtualHost>
This needs a wildcard DNS record (*.example.com) pointing at the server.
Real-World Use Cases
- Freelancers and small agencies hosting multiple client sites on a single VPS to keep infrastructure costs down.
- Multi-brand companies running several distinct marketing sites off shared infrastructure.
- Multi-tenant SaaS platforms using wildcard subdomains, routing each customer to their own workspace.
- Development environments running several local project domains (
project1.local,project2.local) simultaneously for testing.
Mistakes I’ve Made
- Forgetting
ServerAliasfor thewwwvariant, sowww.site1.comfell through to the default/first virtual host instead of the intended site. - Not enabling the site with
a2ensiteon Debian/Ubuntu, leaving the config present but inactive. - Testing before DNS had actually propagated, and chasing a “bug” that was just DNS delay.
- Leaving the default Apache placeholder site enabled and defined first, letting it silently catch all unmatched traffic.
- Accidentally reusing the exact same
ServerNameacross two different virtual host blocks (a copy-paste slip), which left only one site ever reachable.
Mistakes I’ve Made (Permissions & Structure)
- Inconsistent ownership across site directories, especially once different deployment pipelines started managing different sites on the same server.
- Mixing all sites’ files into one shared document root instead of separate directories, which made permissions, backups, and deployments per site harder than they needed to be.
Security Best Practices
- I isolate each site’s files under its own directory with correctly scoped
<Directory>permissions, avoiding one overly broad block covering all sites’ parent directory. - Where sites have different trust levels — one accepts user uploads, another doesn’t — I consider separate Apache worker pools or containers rather than co-locating high-risk and low-risk sites in the same process.
- I set up a defined catch-all/default virtual host that denies access, rather than letting an arbitrary real site silently absorb misdirected or malicious traffic aimed at the raw IP.
- I keep each site’s logs separate (
ErrorLog/CustomLogper virtual host) to make auditing and incident response per domain much simpler.
Performance Optimization
- For servers hosting many low-traffic sites, I look at
mod_worker/mod_eventMPM configurations tuned for a higher number of lightweight connections rather than the defaultpreforkMPM, depending on the app (PHP-FPM setups generally wantmod_event/mod_workerwith FastCGI rather thanmod_php, which requiresprefork). - I enable caching (
mod_expires,mod_deflate) per virtual host as appropriate to each site’s content. - I keep an eye on per-site resource consumption — a single misbehaving site (traffic spike, attack) can affect shared server resources for every co-hosted site unless I’ve set resource limits.
Troubleshooting
All domains show the same site’s content I check that ServerName is correctly and uniquely set in each virtual host block, and that DNS for each domain actually points at this server.
New site returns default Apache “It works!” page The site probably isn’t enabled (a2ensite), or Apache wasn’t reloaded after I added the config.
“www” version doesn’t work but bare domain does I add ServerAlias www.yourdomain.com and confirm a DNS record exists for the www subdomain.
One site’s SSL certificate error affects browsing another site I confirm each <VirtualHost *:443> block has its own correctly matched SSLCertificateFile/SSLCertificateKeyFile, and that SNI is functioning (nearly universal in modern browsers, though very old clients may not support it).
FAQs
How many websites can one Apache server host? No hard limit from Apache itself; the practical ceiling comes from server resources (RAM, CPU, disk I/O) and combined traffic across all hosted sites.
Do all sites need to share the same PHP/application version? No — with PHP-FPM using separate pools per site (or containerization), different sites on the same Apache instance can run different PHP versions or stacks independently.
Is name-based virtual hosting less secure than one server per site? Not inherently in my experience, but sites do share underlying OS-level resources. Proper isolation — file permissions, process isolation where it matters — mitigates most cross-site risk.
Summary and Key Takeaways
- Name-based virtual hosting lets Apache serve multiple independent websites from a single IP, distinguishing them via the
Hostheader and matchingServerName/ServerAliasdirectives. - I give each site its own directory, virtual host block, and log files for clean isolation and easier maintenance.
- I define an explicit default/catch-all virtual host to control what happens with unmatched requests, rather than relying on Apache’s implicit fallback.
- SNI makes multi-domain HTTPS straightforward with Certbot, without needing dedicated IPs per site.
References
- Apache Virtual Host Documentation: https://httpd.apache.org/docs/current/vhosts/
- Apache Name-Based Virtual Host Support: https://httpd.apache.org/docs/current/vhosts/name-based.html
- Let’s Encrypt / Certbot: https://certbot.eff.org/
