What You Will Achieve
By the end of this guide you will have one Ubuntu server running Apache, serving two or more completely separate websites, each on its own domain name. This is called name-based virtual hosting, and it is how agencies, freelancers, and SaaS builders host client sites on a single VPS without paying for a server per site.
This guide is for you if you have a VPS or dedicated server with Ubuntu 22.04 or 24.04, a couple of domain names pointed at it, and basic comfort with the Linux terminal. No Apache experience is assumed.
Prerequisites
- Ubuntu 22.04 or 24.04 server with sudo access (a non-root user with sudo privileges).
- Apache 2.4 — we install it in Step 1 if it is not there yet.
- At least two domain names with A records pointing to your server's IP (for example
site1.example.comandsite2.example.com). DNS can take a few hours to propagate, and Step 7 shows how to test before it does. - A firewall you control (UFW on Ubuntu).
Step 1: Install Apache and Verify It Is Running
First, refresh the package index and install Apache, then make sure the service starts now and on every boot:
sudo apt update
sudo apt install -y apache2
sudo systemctl enable --now apache2
apt update downloads the latest package lists so you get a current Apache build. systemctl enable --now does two things at once: enable starts Apache automatically after a reboot, and --now starts it immediately.
Confirm it is up:
sudo systemctl status apache2
curl -I http://localhost/
curl -I fetches only the HTTP headers from your local server. A 200 OK response means Apache is serving. Visiting your server's IP in a browser should show the Apache2 Ubuntu Default Page.
Step 2: Create a Separate Folder for Each Website
Every virtual host gets its own DocumentRoot — the directory Apache serves files from for that site. Keeping them separate is what isolates the sites from each other:
sudo mkdir -p /var/www/site1.example.com/public_html
sudo mkdir -p /var/www/site2.example.com/public_html
mkdir -p creates the full path including parents, and does nothing if the folders already exist.
Now hand ownership of each site's folder to the www-data user — the account Apache runs as on Ubuntu:
sudo chown -R www-data:www-data /var/www/site1.example.com/public_html
sudo chown -R www-data:www-data /var/www/site2.example.com/public_html
sudo chmod -R 755 /var/www
If the folders were owned by root, Apache could not read new files you add later and visitors would hit 403 Forbidden errors. 755 means the owner can write while everyone (including Apache) can read and traverse directories — the standard safe permission set for web roots.
Drop a tiny test page into each site so you can tell them apart later:
echo "<h1>Site 1 is working</h1>" | sudo tee /var/www/site1.example.com/public_html/index.html
echo "<h1>Site 2 is working</h1>" | sudo tee /var/www/site2.example.com/public_html/index.html
Step 3: Create the Virtual Host Configuration Files
On Ubuntu, Apache reads site configs from /etc/apache2/sites-available/. Create one file per site. Here is the config for the first site:
sudo nano /etc/apache2/sites-available/site1.example.com.conf
Paste this in:
<VirtualHost *:80>
ServerName site1.example.com
ServerAlias www.site1.example.com
DocumentRoot /var/www/site1.example.com/public_html
ErrorLog ${APACHE_LOG_DIR}/site1.example.com-error.log
CustomLog ${APACHE_LOG_DIR}/site1.example.com-access.log combined
</VirtualHost>
What each directive does:
ServerName— the primary domain this block answers for. Apache matches theHostheader of each incoming request against this.ServerAlias— extra names for the same site, typically thewwwvariant.DocumentRoot— where this site's files live.ErrorLog/CustomLog— per-site log files. When something breaks on one site at 2 AM, separate logs save you from grepping through one giant shared log.
Create the second site's file the same way, swapping in site2.example.com:
sudo nano /etc/apache2/sites-available/site2.example.com.conf
<VirtualHost *:80>
ServerName site2.example.com
ServerAlias www.site2.example.com
DocumentRoot /var/www/site2.example.com/public_html
ErrorLog ${APACHE_LOG_DIR}/site2.example.com-error.log
CustomLog ${APACHE_LOG_DIR}/site2.example.com-access.log combined
</VirtualHost>
Step 4: Enable the Sites and Disable the Default
Files in sites-available are inert until enabled. Ubuntu's a2ensite helper creates a symlink into sites-enabled:
sudo a2ensite site1.example.com.conf
sudo a2ensite site2.example.com.conf
sudo a2dissite 000-default.conf
Disabling 000-default.conf matters: it is the catch-all that serves the Apache welcome page for any request that matches no virtual host. If you leave it enabled, mistyped domains (and bots scanning your IP) land on the default page instead of your sites.
Step 5: Test the Configuration and Reload Apache
Always validate the config before reloading — a syntax error on reload would take all your sites down, not just the new one:
sudo apache2ctl configtest
You want to see Syntax OK. Only then reload:
sudo systemctl reload apache2
Use reload, not restart, whenever you change config. Reload re-reads the configuration without dropping active connections, so visitors experience zero downtime.
Step 6: Open the Firewall
Ubuntu's UFW ships with application profiles for Apache. Allow HTTP and HTTPS traffic:
sudo ufw allow 'Apache Full'
sudo ufw status
The Apache Full profile opens ports 80 and 443. (Opening 443 now saves you a step later when you add HTTPS — our companion guide covers Let's Encrypt on Ubuntu.) Skip this step only if you manage firewall rules elsewhere, like a cloud security group.
Step 7: Point Your Domains at the Server
In your DNS provider's panel, create A records for each domain and its www subdomain, all pointing to your server's public IP:
site1.example.com A 203.0.113.10
www.site1.example.com A 203.0.113.10
site2.example.com A 203.0.113.10
www.site2.example.com A 203.0.113.10
DNS propagation can take minutes to hours. To test before it propagates, add temporary entries to the hosts file on your own laptop (not the server):
203.0.113.10 site1.example.com www.site1.example.com
203.0.113.10 site2.example.com www.site2.example.com
On Windows this file is C:\Windows\System32\drivers\etc\hosts; on macOS and Linux it is /etc/hosts. Your browser will then resolve the domains straight to your server, bypassing DNS entirely. Remove these lines once real DNS is live.
Verify It Worked
Run these checks in order:
- Virtual host map:
sudo apache2ctl -Sprints every virtual host Apache loaded and which IP:port each listens on. Both your domains should appear under*:80. This is the single best debugging command for virtual hosts. - Per-site response before DNS:
curl -H "Host: site1.example.com" http://203.0.113.10/should return "Site 1 is working", and the same withsite2.example.comshould return "Site 2 is working". The-H "Host: ..."flag fakes the header Apache uses to pick the virtual host, so this works even with no DNS. - Browser test: visit
http://site1.example.comandhttp://site2.example.com— each shows its own page. - Logs:
sudo tail -f /var/log/apache2/site1.example.com-access.logwhile loading the site to watch requests land in the right per-site log.
Common Issues and Fixes
1. 403 Forbidden on a new site
The most common virtual-host error. It almost always means filesystem permissions: the www-data user cannot read the DocumentRoot. Fix ownership and permissions, then reload:
sudo chown -R www-data:www-data /var/www/site1.example.com/public_html
sudo chmod -R 755 /var/www/site1.example.com
sudo systemctl reload apache2
If permissions are fine, check you did not add a restrictive <Directory> block — Apache 2.4 requires Require all granted inside any custom directory block.
2. The Apache default page shows instead of your site
Either you forgot a2ensite + reload, or 000-default.conf is still enabled and answering first. Run sudo apache2ctl -S — if your domain is missing from the list, the site was never enabled. If it is there but the default page still shows, disable the default with sudo a2dissite 000-default.conf and reload.
3. "Could not reliably determine the server's fully qualified domain name"
This warning on reload is harmless, but it clutters logs. Silence it by setting a global ServerName:
echo "ServerName localhost" | sudo tee /etc/apache2/conf-available/servername.conf
sudo a2enconf servername
sudo systemctl reload apache2
4. configtest reports a syntax error after editing
Usually an unclosed <VirtualHost> tag or a typo in a directive name. apache2ctl configtest prints the file and line number — fix it there, re-run until you see Syntax OK, and never reload on a failing test.
5. Changes do not appear in the browser
You edited the file in sites-available but forgot to reload, or your browser cached the old page. Reload Apache with sudo systemctl reload apache2 and hard-refresh with Ctrl+Shift+R. Also confirm you edited the .conf in sites-available — never edit the symlinks in sites-enabled directly.
Where to Go From Here
You now have true multi-site hosting: add a third or fourth site by repeating Steps 2–5, each with its own domain, files, and logs. The natural next step is HTTPS — grab free certificates with Certbot (sudo certbot --apache -d site1.example.com -d www.site1.example.com), which creates a separate *:443 virtual host per domain automatically.
Setting up Apache virtual hosts is routine work, but getting permissions, DNS, and SSL right across many client sites takes experience. If you would rather hand it off, XpertBees sets up and manages production Apache and Nginx servers for businesses every day — reach out and we will take it from here.



