Saturday, September 19, 2026

Apache Virtual Host Example

What is a virtual host? Why would I want one? It's a way of saving costs. At minimum, you'll only need one physical or virtual machine, one backend instance and Apache. So an Apache virtual host manages the routing of IP requests to your backend instance. Allowing you to host multiple distinct websites (domain name or white labels). To visitors, it would appear that a website is running on its own dedicated server but in reality, behind the scenes, Apache uses incoming requests to serve different resources depending on the requested website. Did that make sense? Perhaps an example will make it concrete.

Tools and Assumptions

The following were the tools I used to demonstrate this Apache virtual host example. For the backend instance, I've created a simple Spring Boot web app that just serves a static page with a bit of white label handling. I'm assuminng you know your way around Windows and Windows Susbsystem Linux. Furthermore, you should be able to create your own backend instance and run it in Ubuntu. I won't be explaining how to create my Spring Boot web app in detail but I'll tell you what the code does.

  • A backend instance.
  • Windows 11 Home 10.0.26200
  • WSL - Ubuntu 22.04.3
  • Apache/2.4.52 (Ubuntu)
  • Good old vi and a text editor
  • IntelliJ IDEA 2023.3.4 (Community Edition)

How Does a Virtual Host Work

This is what we are going to do. We will have two websites (i.e. white labels). Let's pretend our website serves Japan and the Philippines market. In order to save on costs, we only have one server for both markets. We let Apache process the requests and pass it along the correct path resource to our single backend instance. Sounds simple? It is once you get the hang of it. Painful in the beginning though LOL.

Backend Instance

As I have mentioned earlier, our backend instance is a Spring Boot web app. Here's the bit that we are interested in.

  
@Controller
public class WelcomeController {
    @GetMapping("/welcome/{country}")
    public String welcome(@PathVariable String country, Model model) {
        switch (country) {
            case "jp":
                country = "Japan";
                break;
            default:
                country = "Philippines";
        }

        model.addAttribute("country", country);

        return "welcome";
    }
}
  
  
<!DOCTYPE HTML>
<html xmlns:th="http://www.thymeleaf.org">
<head>
    <title>Welcome Page</title>
    <meta http-equiv="Content-Type" content="text/html; charset=UTF-8" />
</head>
<body>
<p th:text="|Welcome to the ${country} site of Codesamples!|" />
</body>
</html>
  

Can you tell what this piece of code does? Right, so as you can probably tell in the WelcomeController that we check the path requested and depending on the country, we serve the specific content based on the country.

Running the Backend

For sure, you can have your own backend written in any programming language. It is up to you. The below is an excerpt from the WSL shell. Once you have it running, you should be able to curl the localhost with port. In this case curl localhost:8080 will return a result in the shell.

  
jpllosa@g16-gaming:/workspace/web-thymeleaf/target$ java -jar web-thymeleaf-0.0.1-SNAPSHOT.jar

  .   ____          _            __ _ _
 /\\ / ___'_ __ _ _(_)_ __  __ _ \ \ \ \
( ( )\___ | '_ | '_| | '_ \/ _` | \ \ \ \
 \\/  ___)| |_)| | | | | || (_| |  ) ) ) )
  '  |____| .__|_| |_|_| |_\__, | / / / /
 =========|_|==============|___/=/_/_/_/

 :: Spring Boot ::                (v4.1.1)

2026-08-29T15:58:02.437+01:00  INFO 9632 --- [web-thymeleaf] [           main] n.c.w.WebThymeleafApplication            : Starting WebThymeleafApplication v0.0.1-SNAPSHOT using Java 17.0.18 with PID 9632 (/workspace/web-thymeleaf/target/web-thymeleaf-0.0.1-SNAPSHOT.jar started by jpllosa in /workspace/web-thymeleaf/target)
2026-08-29T15:58:02.446+01:00  INFO 9632 --- [web-thymeleaf] [           main] n.c.w.WebThymeleafApplication            : No active profile set, falling back to 1 default profile: "default"
2026-08-29T15:58:03.776+01:00  INFO 9632 --- [web-thymeleaf] [           main] o.s.boot.tomcat.TomcatWebServer          : Tomcat initialized with port 8080 (http)
2026-08-29T15:58:03.792+01:00  INFO 9632 --- [web-thymeleaf] [           main] o.apache.catalina.core.StandardService   : Starting service [Tomcat]
2026-08-29T15:58:03.792+01:00  INFO 9632 --- [web-thymeleaf] [           main] o.apache.catalina.core.StandardEngine    : Starting Servlet engine: [Apache Tomcat/11.0.24]
2026-08-29T15:58:03.827+01:00  INFO 9632 --- [web-thymeleaf] [           main] b.w.c.s.WebApplicationContextInitializer : Root WebApplicationContext: initialization completed in 1265 ms
2026-08-29T15:58:04.455+01:00  INFO 9632 --- [web-thymeleaf] [           main] o.s.boot.tomcat.TomcatWebServer          : Tomcat started on port 8080 (http) with context path '/'
2026-08-29T15:58:04.465+01:00  INFO 9632 --- [web-thymeleaf] [           main] n.c.w.WebThymeleafApplication            : Started WebThymeleafApplication in 2.924 seconds (process running for 3.648)
2026-08-29T15:58:27.138+01:00  INFO 9632 --- [web-thymeleaf] [nio-8080-exec-1] o.a.c.c.C.[Tomcat].[localhost].[/]       : Initializing Spring DispatcherServlet 'dispatcherServlet'
2026-08-29T15:58:27.138+01:00  INFO 9632 --- [web-thymeleaf] [nio-8080-exec-1] o.s.web.servlet.DispatcherServlet        : Initializing Servlet 'dispatcherServlet'
2026-08-29T15:58:27.139+01:00  INFO 9632 --- [web-thymeleaf] [nio-8080-exec-1] o.s.web.servlet.DispatcherServlet        : Completed initialization in 1 ms
  

Apache Virtual Host Configuration

If you've been following my blogs, chances are you have done Apache Redirect Example. Excellent, if you have done it because you'll probably already have installed the Apache Rewrite module. If you have not, please head over to Apache Redirect Example and follow the Complex Apache Redirect section. And if you are too lazy to pop over there, then here's what I did. We also need the Proxy and HTTP Proxy modules. Restart Apache after it is installed. To check what modules you have, head over to /etc/apache2/mods-enabled directory.

  
jpllosa@g16-gaming:/etc/apache2/mods-enabled$ sudo a2enmod rewrite
[sudo] password for jpllosa:
Enabling module rewrite.
To activate the new configuration, you need to run:
  systemctl restart apache2
jpllosa@g16-gaming:/etc/apache2/mods-enabled$ ls
access_compat.load  authn_file.load  autoindex.load  env.load        mpm_event.load    rewrite.load
alias.conf          authz_core.load  deflate.conf    filter.load     negotiation.conf  setenvif.conf
alias.load          authz_host.load  deflate.load    mime.conf       negotiation.load  setenvif.load
auth_basic.load     authz_user.load  dir.conf        mime.load       reqtimeout.conf   status.conf
authn_core.load     autoindex.conf   dir.load        mpm_event.conf  reqtimeout.load   status.load
jpllosa@g16-gaming:/etc/apache2/mods-enabled$ sudo a2enmod proxy proxy_http
[sudo] password for jpllosa:
Enabling module proxy.
Considering dependency proxy for proxy_http:
Module proxy already enabled
Enabling module proxy_http.
To activate the new configuration, you need to run:
  systemctl restart apache2
jpllosa@g16-gaming:/etc/apache2/mods-enabled$ ls
access_compat.load  authn_file.load  autoindex.load  env.load        mpm_event.load    proxy_http.load  setenvif.load
alias.conf          authz_core.load  deflate.conf    filter.load     negotiation.conf  reqtimeout.conf  status.conf
alias.load          authz_host.load  deflate.load    mime.conf       negotiation.load  reqtimeout.load  status.load
auth_basic.load     authz_user.load  dir.conf        mime.load       proxy.conf        rewrite.load
authn_core.load     autoindex.conf   dir.load        mpm_event.conf  proxy.load        setenvif.conf
jpllosa@g16-gaming:/etc/apache2/mods-enabled$ sudo systemctl restart apache2
  

Next is the per website Apache virtual host configuration. Create a similar file like below in your /etc/apache2/sites-available directory. Remember we are still in WSL Ubuntu land. You can name it whatever you like. I do hope it is descriptive though. Below is the virtual host configuration for the Japan website. You may skip copying the comments but I just straight up copied it over from 000-default.conf. Make another config file for the Philippines website. Should be the same except ServerName or you could say the "jp" bits become "ph".

  
jpllosa@g16-gaming:/etc/apache2/sites-available$ cat codesamples_jp.conf
<VirtualHost *:80>
        # The ServerName directive sets the request scheme, hostname and port that
        # the server uses to identify itself. This is used when creating
        # redirection URLs. In the context of virtual hosts, the ServerName
        # specifies what hostname must appear in the request's Host: header to
        # match this virtual host. For the default virtual host (this file) this
        # value is not decisive as it is used as a last resort host regardless.
        # However, you must set it for any further virtual host explicitly.
        ServerName codesamples.jp

        ServerAdmin webmaster@localhost
        #DocumentRoot /var/www/html

        # Available loglevels: trace8, ..., trace1, debug, info, notice, warn,
        # error, crit, alert, emerg.
        # It is also possible to configure the loglevel for particular
        # modules, e.g.
        #LogLevel info ssl:warn

        ErrorLog ${APACHE_LOG_DIR}/codesamples-jp-error.log
        CustomLog ${APACHE_LOG_DIR}/codesamples-jp-access.log combined

        # For most configuration files from conf-available/, which are
        # enabled or disabled at a global level, it is possible to
        # include a line for only one particular virtual host. For example the
        # following line enables the CGI configuration for this host only
        # after it has been globally disabled with "a2disconf".
        #Include conf-available/serve-cgi-bin.conf

        ProxyPreserveHost On

        ProxyPass / http://localhost:8080/welcome/jp
        ProxyPassReverse / http://localhost:8080/welcome/jp

</VirtualHost>

jpllosa@g16-gaming:/etc/apache2/sites-available$ cat codesamples_ph.conf
<VirtualHost *:80>
        # The ServerName directive sets the request scheme, hostname and port that
        # the server uses to identify itself. This is used when creating
        # redirection URLs. In the context of virtual hosts, the ServerName
        # specifies what hostname must appear in the request's Host: header to
        # match this virtual host. For the default virtual host (this file) this
        # value is not decisive as it is used as a last resort host regardless.
        # However, you must set it for any further virtual host explicitly.
        ServerName codesamples.ph

        ServerAdmin webmaster@localhost
        #DocumentRoot /var/www/html

        # Available loglevels: trace8, ..., trace1, debug, info, notice, warn,
        # error, crit, alert, emerg.
        # It is also possible to configure the loglevel for particular
        # modules, e.g.
        #LogLevel info ssl:warn

        ErrorLog ${APACHE_LOG_DIR}/codesamples-ph-error.log
        CustomLog ${APACHE_LOG_DIR}/codesamples-ph-access.log combined

        # For most configuration files from conf-available/, which are
        # enabled or disabled at a global level, it is possible to
        # include a line for only one particular virtual host. For example the
        # following line enables the CGI configuration for this host only
        # after it has been globally disabled with "a2disconf".
        #Include conf-available/serve-cgi-bin.conf

        ProxyPreserveHost On

        ProxyPass / http://localhost:8080/welcome/ph
        ProxyPassReverse / http://localhost:8080/welcome/ph

</VirtualHost>
  

What did you notice? How are we reaching our backend instance? As you can see, we are adding the path variable based on country or should I say Apache is adding it so our backend will know what content to serve.

Below is the trail of commands you'll possibly do. After you have two Apache virtual host config files, you'll need to enabled it (e.g. a2ensite) and disabled the default Apache config (e.g. a2dissite. Obviously, reload Apache to apply the changes you did.

  
jpllosa@g16-gaming:/etc/apache2/sites-available$ sudo cp codesamples_jp.conf codesamples_ph.conf
jpllosa@g16-gaming:/etc/apache2/sites-available$ sudo vi codesamples_ph.conf
jpllosa@g16-gaming:/etc/apache2/sites-available$ sudo a2ensite codesamples_jp.conf
Enabling site codesamples_jp.
To activate the new configuration, you need to run:
  systemctl reload apache2
jpllosa@g16-gaming:/etc/apache2/sites-available$ sudo a2ensite codesamples_ph.conf
Enabling site codesamples_ph.
To activate the new configuration, you need to run:
  systemctl reload apache2
jpllosa@g16-gaming:/etc/apache2/sites-available$ sudo a2dissite 000-default.conf
Site 000-default disabled.
To activate the new configuration, you need to run:
  systemctl reload apache2
jpllosa@g16-gaming:/etc/apache2/sites-available$
  

Administrative Ubuntu Configuration

We're not done yet. See? I told you it was painful. Patience, we are nearly there. We are halfway there. What's left is getting traffic to route correctly. First is the /etc/hosts file. Update your hosts file as below. You can replace it with your own domain name as long as it matches your Apache virtual host configuration.

  
jpllosa@g16-gaming:/etc$ cat /etc/hosts
# This file was automatically generated by WSL. To stop automatic generation of this file, add the following entry to /etc/wsl.conf:
# [network]
# generateHosts = false
127.0.0.1       localhost
127.0.0.1       codesamples.jp
127.0.0.1       codesamples.ph

# The following lines are desirable for IPv6 capable hosts
::1     ip6-localhost ip6-loopback
fe00::0 ip6-localnet
ff00::0 ip6-mcastprefix
ff02::1 ip6-allnodes
ff02::2 ip6-allrouters
jpllosa@g16-gaming:/etc$
  

What this does is route requests to codesample.ph and codesample.jp to your Ubuntu box. Next is we need to know what the IP of our Linux box is. We need this IP to configure our Windows machine to route traffic to our Linux box. Here's my IP but it could be different in your system.

  
jpllosa@g16-gaming:/etc$ ip addr
1: lo:  mtu 65536 qdisc noqueue state UNKNOWN group default qlen 1000
    link/loopback 00:00:00:00:00:00 brd 00:00:00:00:00:00
    inet 127.0.0.1/8 scope host lo
       valid_lft forever preferred_lft forever
    inet 10.255.255.254/32 brd 10.255.255.254 scope global lo
       valid_lft forever preferred_lft forever
    inet6 ::1/128 scope host
       valid_lft forever preferred_lft forever
2: eth0:  mtu 1492 qdisc mq state UP group default qlen 1000
    link/ether 00:15:5d:8a:ce:ae brd ff:ff:ff:ff:ff:ff
    inet 172.18.182.45/20 brd 172.18.191.255 scope global eth0
       valid_lft forever preferred_lft forever
    inet6 fe80::215:5dff:fe8a:ceae/64 scope link
       valid_lft forever preferred_lft forever
  

When requests come in, you can tail the Apache log files. Here's an example of where the log files are located.

  
jpllosa@g16-gaming:/var/log/apache2$ ls
access.log       codesamples-jp-access.log  error.log.10.gz  error.log.14.gz  error.log.5.gz  error.log.9.gz
access.log.1     codesamples-jp-error.log   error.log.11.gz  error.log.2.gz   error.log.6.gz  other_vhosts_access.log
access.log.2.gz  error.log                  error.log.12.gz  error.log.3.gz   error.log.7.gz
access.log.3.gz  error.log.1                error.log.13.gz  error.log.4.gz   error.log.8.gz
  

Administrative Windows Configuration

Now that we got the IP, let's ping it to check connectivity. When we have connectivity, update the Windows hosts file. This will tell Windows where to route requests to our example domains. You'll need admin priviledges to update the hosts file.

  
C:\>ping 172.18.182.45

Pinging 172.18.182.45 with 32 bytes of data:
Reply from 172.18.182.45: bytes=32 time=1ms TTL=64
Reply from 172.18.182.45: bytes=32 time<1ms TTL=64
Reply from 172.18.182.45: bytes=32 time<1ms TTL=64
Reply from 172.18.182.45: bytes=32 time<1ms TTL=64

Ping statistics for 172.18.182.45:
    Packets: Sent = 4, Received = 4, Lost = 0 (0% loss),
Approximate round trip times in milli-seconds:
    Minimum = 0ms, Maximum = 1ms, Average = 0ms


C:\Windows\System32\drivers\etc>type hosts
...snipped...
172.18.182.45 codesamples.jp
172.18.182.45 codesamples.ph
  

Apache Virtual Host Demonstration

Congratulations, you made it this far. Now, the time has come to see all the work we have done. Java web app is running? Check. Apache restarted with the correct configurations? Check. Ubuntu and Windows hosts file updated? Check. Very good! Let's hit the domains. Fire up your browser and go to your website. Here are a couple of screen grabs of the two example websites with corresponding network tab open and tailing the Apache access logs at the same time. As you will experience if you are trying this out, the logs move as soon as you request for your website. Which tells us we have routed things correctly since the request has reached it's target destination. Bravo, well done!

Apache Virtual Host Summary

There you have it. Was that fun for you? You have managed the routing of IP requests to your backend. You have hosted two distinct websites and the visitors wouldn't know it was only one Java web app working behind the scenes. Have a happy virtual hosting.

Go here for more Apache fun.