Virtual Hosts

Articles: 

In my blog article "Websites in Minutes", I talk about the steps I used to put together a memorial website for a recently-passed friend. I volunteered to create that site in part because I knew that I could construct it and eventually release it to the internet in minutes. Such is the power of an internet-exposed Linux system running an Apache webserver.


But, to build and release the site took some preparation and configuration of that system. I'd like to tell you how I did it.
Index

The Naming of Names

Before I start setting up the new site, I had to chose some names; URL names, email names, path names, and the like. It helps that I had a starting point for all the names I had to choose.
Index
my.lan
is the internal domain name of my LAN. You won't find this on the Internet; the domain doesn't resolve (and isn't even legal) outside of my lan.
DbAdmin
is the MySQL user with 'root' rights and accesses.
'drupal'@'localhost'
is the MySQL user that Drupal uses to access all it's databases.
password
is (not) the password that the drupal MySQL user uses. (You don't think that I'd divulge a password, do you?)
/srv/httpd/htdocs/drupal6
is where my Drupal installation resides on my server.
/etc/httpd/httpd.conf
is the path to the Apache configuration file.
/var/named/DB.my.lan
is the path to the BIND configuration file for my internal DNS.
/etc/hosts
is the path to the usual static domain name file.
/etc/mail/local-host-names
is the path to the file in which Sendmail wants to find the names of hosts that can relay email through my server. A select few, to be sure.
/etc/mail/virtusertable
is the path to the file in which Sendmail wants to find the email addresses that I expose to the outside.
lew@my.lan
is my lan-facing email address

Those names helped me decide on these names:

cooker.my.lan
will be the name I call the new site while I'm working on it. Since, while under construction, the site will exist only within my lan, it needs a name that places it logically in the lan.
amemorial.ca
will be the Internet-resolvable fully qualified domain name for the new site, once I put it into production. Note that amemorial.ca isn't really the name of the memorial site; it's simply the name I'll call it here in this article. The real domain is one best left to my late friend's family, and does not need to be publicly outed here.
admin@amemorial.ca
I'll need an email address for the Drupal "administrator" of the new site. This is the address that will issue various site-management emails on behalf of the site's administration
webmaster@amemorial.ca
I'll need a generic webmaster email address, as per various RFCs, so that I can receive and answer website-specific inquiries.
postmaster@amemorial.ca
I'll need a generic postmaster email address, as per various RFCs, so that I can receive and answer email-specific inquiries.
abuse@amemorial.ca
I'll need a generic abuse email address, as per various RFCs, so that I can receive and answer email and website abuse reports.
drupal6_amemorial
will be the name of the MySQL database in which Drupal will build the new site. Since the site doesn't exist in production yet, I will use this name for both the "cooker" databases, and the production databases.

Once I had decided on what to call the various new components, I contacted my domain registrar and arranged to lease the amemorial.ca domainname. This registration process took some time to complete (36 hours, by my estimate), so while I waited, I proceeded to build my "cooker" version of the new website.

Cooker

When I first put together a website, I start off in a "cooker" site. Only hosts within my LAN can access the cooker site, permitting me to work on it without having it exposed to the internet. For expediency's sake, my "cooker" resides on the same server as my production websites; I don't have a standalone website-development environment. Yes, this is poor practice, but, with caution, is practical in small-site environments.
Index

In building these sorts of sites, I start from the "inside", and work "outward", so that I satisfy any dependencies before they become an issue. In this case, that means that I

  1. ensured that I had the proper server software (Drupal, Apache, MySql, ISC Bind, etc.) installed and running correctly,
  2. built the required new Drupal databases in MySQL,
  3. built the Drupal CMS definition that would use those databases,
  4. built the Apache website definition that would invoke Drupal for the site, and
  5. built the DNS alias that would connect my server's lan IP address to the sitename, and finally
  6. build the content of site through web access to the new domain.

Drupal Database definition

The first step in setting up the Drupal databases for my cooker site is to create the database in MySQL. To do this, I used the mysqladmin command (under the global authority of my already defined DbAdmin user), and created the drupal6_amemorial database.

To create the database that this new Drupal site will use, I issued the command:
mysqladmin -u DbAdmin -p create drupal6_amemorial

This database will serve as both the cooker site's database (while I develop the site), and the production site's database (when I'm ready to go live). Since there is no chance of conflict between the "cooker" site and the "production" site, I'm safe to use the same database for both. Otherwise, I'd have name the database something else, and "cloned" it to the production database when I was ready.
The second database step is to ensure that a suitable MySQL "user" exists to access and manage this database, and that the user has the appropriate MySQL permissions set. This we do as DbAdmin through the mysql command, with a single SQL statement.

I used the mysql command (with DbAdmin authority) to
GRANT SELECT, INSERT, UPDATE, DELETE, CREATE, DROP, INDEX, ALTER
ON drupal6_amemorial.*
TO 'drupal'@'localhost' IDENTIFIED BY 'password';

Of course, I set password to some real password value; for this article, I'll just show it as password.

Drupal Site definition

Now that I have the cooker site's Drupal database defined to MySQL, I can proceed on and configure Drupal to use it.

My Drupal installation resides in /srv/httpd/htdocs/drupal6, and all the site configuration files are held in subdirectories of this directory. To hold the site configuration file for my cooker.my.lan Drupal website, I created a /srv/httpd/htdocs/drupal6/sites/cooker.my.lan directory, and populated it with an empty /files subdirectory, and a settings.php file.

In the settings.php file, I coded:
  $db_url = 'mysqli://drupal:password@localhost/drupal6_amemorial';
  $db_prefix = '';
  $update_free_access = FALSE;
  $base_url = 'http://cooker.my.lan';  // NO trailing slash!
  ini_set('arg_separator.output',     '&');
  ini_set('magic_quotes_runtime',     0);
  ini_set('magic_quotes_sybase',      0);
  ini_set('session.cache_expire',     200000);
  ini_set('session.cache_limiter',    'none');
  ini_set('session.cookie_lifetime',  2000000);
  ini_set('session.gc_maxlifetime',   200000);
  ini_set('session.save_handler',     'user');
  ini_set('session.use_cookies',      1);
  ini_set('session.use_only_cookies', 1);
  ini_set('session.use_trans_sid',    0);
  ini_set('url_rewriter.tags',        '');

Here, the $db_url PHP variable names the site's database interface details, including the MySQL access method, the Drupal database user and password, and the Drupal database. The $base_url PHP variable names the leading part of the URL under which all the cooker site's pages will be found. The other PHP variables and function calls set various site performance and behaviour values; I let them default to the settings provided in the Drupal example settings.php file.

Apache VHOST definition

Since I want the webserver to serve up a different, unique set of pages when accessed through it's fully qualified domain name, I had to alter my Apache configuration to define cooker.my.lan as a "Virtual Host".

You see, a webserver can serve up different pages based on the hostname in the URL the web browser is asking for. This isn't the default mode for the Apache webserver, though, and thus needs special configuration.

To tell Apache the configuration for my new website, I edited the /etc/httpd/httpd.conf file, to include the following values:
LoadModule vhost_alias_module lib/httpd/modules/mod_vhost_alias.so

<VirtualHost *:80>
    ServerName  cooker.my.lan
    ServerAdmin lew@my.lan
    DocumentRoot "/srv/httpd/htdocs/drupal6"
    ErrorLog     "/var/log/httpd/error_log"
    CustomLog    "/var/log/httpd/access_log" common
</VirtualHost>

<Directory "/srv/httpd/htdocs/drupal6">
    AllowOverride All
</Directory>

First, Apache needed the vhost_alias_module to parse the request URL, so that Apache could determine the hostname. Without this, Apache would only look at and act apon the page path part of the URL.

Next, I defined the VirtualHost stanza for the site. The crucial parts were the ServerName, which Apache used to match to the hostname in the request URL, and the DocumentRoot, which pointed Apache at the Drupal directory as the root directory for all page paths within the virtual domain.

Finally, for the Drupal CMS that I used for the site, I defined the Apache access permissions for the directory in which the Drupal installation resides. Drupal had it's own permission set, and I wanted those permissions to apply.

Internal DNS definition

I now need my lan's DNS to recognize cooker.my.lan and resolve it to the internal IP address of my webserver.

I run a local DNS using ISC Bind. This provides me with both a caching DNS for my LAN's internet use, and a flexible name service for my LAN. Consequently, I defined cooker.my.lan within my named database file /var/named/DB.my.lan.

In /var/named/DB.my.lan, I have
$ORIGIN .
my.lan               IN SOA  server.my.lan. root.server.my.lan. (
                                2011032000 ; serial
                                28800      ; refresh (8 hours)
                                7200       ; retry (2 hours)
                                604800     ; expire (1 week)
                                86400      ; minimum (1 day)
                                )
                        NS      server.my.lan.
                        A       192.168.255.1
                        MX      10 server.my.lan.
$ORIGIN my.lan.
server                  A       192.168.255.1
                        MX      10 server
cooker                  CNAME   server

This definition built a DNS 'A' record for my server, pointing the server.my.lan at the proper IP address (192.168.255.1). The 'cooker' entry built a 'CNAME' record within my DNS to point cooker.my.lan to the same IP as server.my.lan, making cooker.my.lan an alias for server.my.lan.

Had I not run Bind, I could have done the same thing through the /etc/hosts file. The hosts entry would have been simpler:
192.168.255.1            server.my.lan server cooker.my.lan cooker

This one line is the equivalent of two DNS 'A' records; it points server.my.lan (or server, if referenced from within the my.lan domain) and cooker.my.lan (or cooker) at IP address 192.168.255.1.

With either setting (/etc/hosts or ISC Bind's config), I now had a unique, fully-qualified domain name accessible only within my lan that resolved to the internal IP address of my webserver.

cooker.my.lan Site Management

With all this preparation complete, I could finally access my cooker website, and start configuring it. Since I used a web browser to perform this process, I launched Firefox and pointed it at http://cooker.my.lan/install.php.

To my delight, I saw the first page of the interactive Drupal site configuration process:

The first configuration choice was one of language. I clicked on the "Install Drupal in English" link to start the installation. Drupal accepted this, and proceeded to the Verify Requirements step.

The Drupal installation process quickly streamed through this, and the following two steps ("Set up database" and "Install site"), and finally stopped at the Configure site page.

In the Configure site page, I altered the default Site name value, and provided appropriate values for the rest of the configuration form.

With all the new values in place, I clicked the Save and Continue button at the bottom of the page, and the installation script continued to the end.

Once this installation process completed, the new Drupal site delivered an email into my inbasket:
Account details for Admin at In Memorium (approved)
 From: lew@my.lan
 To: lew@my.lan
 
Admin,

Your account at In Memorium has been activated.

You may now log in by clicking on this link or copying and pasting it in your  
browser:

http://cooker.my.lan/?q=user/reset/1/1303053329/ffacc1885aba7a34e4fff29fdf9df4bq

This is a one-time login, so it can be used only once.

After logging in, you will be redirected to  
http://cooker.my.lan/?q=user/1/edit so you can change your password.

Once you have set your own password, you will be able to log in to  
http://cooker.my.lan/?q=user in the future using:

username: Admin

With this preliminary work behind me, I now took my time and customized the site into the proper memorium for my friend. It took most of a day, but by the time my domain registration was approved, I had the cooker site prepared as if it were amemorial.ca.

Production

So, now that amemorial.ca was mine, I wanted to move the website onto the internet. Like the "cooker" site before, I had to take specific steps to make it available:
  1. Define amemorial.ca as a new Drupal site,
  2. Define amemorial.ca as a new Apache virtual host,
  3. Define the relevant email addresses to my server's Sendmail installation,
  4. Update my external DNS to point amemorial.ca to the proper internet-accessable IP address, and
  5. Establish a local cron job to run amemorial.ca maintenance tasks.
Index

Drupal Site definition

The main value of the "cooker" site is that it allowed me to customize the contents of the drupal6_amemorial databases without requiring an external domain name. However, once I acquired the rights to amemorial.ca and had completed the configuration of the "cooker" website, I had to connect the drupal6_amemorial database to the Drupal configuration for the new website.

So, to hold the site configuration for my amemorial.ca Drupal website, I created a /srv/httpd/htdocs/drupal6/sites/amemorial.ca directory, and populated it with copy of the cooker.my.lan/files subdirectory, and the settings.php file from the cooker.my.lan drupal configuration.

I then altered the sites/amemorial.ca/settings.php file, making the single necessary change to make this file serve amemorial.ca
  $db_url = 'mysqli://drupal:password@localhost/drupal6_amemorial';
  $db_prefix = '';
  $update_free_access = FALSE;
  $base_url = 'http://amemorial.ca';  // WAS 'http://cooker.my.lan'
  ini_set('arg_separator.output',     '&');
  ini_set('magic_quotes_runtime',     0);
  ini_set('magic_quotes_sybase',      0);
  ini_set('session.cache_expire',     200000);
  ini_set('session.cache_limiter',    'none');
  ini_set('session.cookie_lifetime',  2000000);
  ini_set('session.gc_maxlifetime',   200000);
  ini_set('session.save_handler',     'user');
  ini_set('session.use_cookies',      1);
  ini_set('session.use_only_cookies', 1);
  ini_set('session.use_trans_sid',    0);
  ini_set('url_rewriter.tags',        '');

Apache VHOST definition

Like the "cooker" site, I needed Apache to recognize amemorial.ca (and the alternate www.amemorial.ca) and serve up the proper web pages for that domain. This meant that I had to add another Apache VirtualHost to my configuration.

In my /etc/httpd/httpd.conf, I added the appropriate VirtualHost stanza, and restarted my web server.
<VirtualHost *:80>
    ServerName  amemorial.ca
    ServerAlias www.amemorial.ca
    ServerAdmin webmaster@amemorial.ca
    DocumentRoot "/srv/httpd/htdocs/drupal6"
</VirtualHost>

The ServerName names amemorial.ca as the hostname that the VirtualHost would act as. Additionally, the ServerAlias names www.amemorial.ca; this VirtualHost would serve both amemorial.ca and www.amemorial.ca from the same set of webpages.
Apache would use the ServerAdmin email address webmaster@amemorial.ca in any 'native' error messages it would present to the web browser.

With that, my internet-facing web server was ready to serve up pages for the amemorial.ca domain. But, I still had other "internal" facilities to set up before I could go "live".

Sendmail Virtual Domains

The Internet RFCs name several email addresses that an Internet-facing site should have.

  1. If the site provides an email server, the domain must have a postmaster@ email address,
  2. If the site provides an web server, the domain must have a webmaster@ email address, and
  3. If the site provides for internet-facing user services, the domain must have a abuse@ email address,

My amemorial.ca domain would offer specific services to external users using a web server that generated emails, so I had to add all three email addresses to my email server. Additionally, amemorial.ca would use a unique email address (admin@) to send administrative emails from, so I needed sendmail to handle that address as well.

Virtual Host

For this new website, I wanted all incoming email to be sent to the hostname amemorial.ca. A DNS MX record would ensure that email clients know which IP address to send @amemorial.ca email to, but my Sendmail MTA needs to know that it is permitted to receive email addressed to a amemorial.ca recipient. In other words, I needed to tell Sendmail to process emails addressed to the host amemorial.ca.

This took an addition to my /etc/mail/local-host-names file, adding the host amemorial.ca to the list of hostnames that this Sendmail will receive for.

# names of hosts for which we receive email
my.lan
amemorial.ca

Now, Sendmail will service email sent to @my.lan and @amemorial.ca addresses.

Virtual Users

Once sendmail accepts an email for one of the addresses in my new domain, it has pass it on to the proper local user.

I added both the new "virtual" email addresses, and a default error (for email addresses not already specified) to my /etc/mail/virtusertable
admin@amemorial.ca                       lew
abuse@amemorial.ca                       abuse
postmaster@amemorial.ca                  postmaster
webmaster@amemorial.ca                   webmaster
@amemorial.ca                            error:5.7.0:550 No such user here

The local lew, abuse, postmaster and webmaster users are already defined elsewhere; the virtusertable entries here just connect the new @amemorial.ca email addresses to those users. Additionally, the final rule causes Sendmail to reject email addressed to any @amemorial.ca user not already listed in virtusertable; this would help to reduce the amount of spam I'd receive at the new address.

Internet-facing DNS definition

With those changes, I was ready to open the floodgates, and let people connect to the amemorial.ca website. This meant that I could now make the changes to my public DNS to direct amemorial.ca and www.amemorial.ca to my public IP address, and to direct all email for @amemorial.ca addresses to that same address.

So, I asked my public DNS service to open a set of DNS records for domain name amemorial.ca, with an A record pointing amemorial.ca at my IP, a CNAME record pointing www.amemorial.ca at amemorial.ca, and an MX record pointing amemorial.ca at my public IP as well. Once those DNS changes percolated through the Domain Name system, both the website and the email for my memorial site became available to the internet at large.

Drupal Cron job

The last step necessary was to create a cron job that would run every five minutes to retrieve and discard the http://amemorial.ca/cron.php webpage. Drupal uses this task to initiate it's internal scheduling facility.

With that final change, amemorial.ca became a stable, available website to memorialize my late friend.