OneLinersCommand workbench
Guides
Services & Applications / Networking & DNS

Configure Apache name-based virtual hosts

Serve multiple domains from isolated document roots, select an intentional default host, validate host matching, and keep separate access and error logs.

45 min8 stepsChanges system stateRevision 2
Save or explore
Save to collectionCreate a collection in the sidebar first.
0 of 8 steps completed
Goal

Host multiple sites on one Apache listener without cross-serving content when a Host header is missing or incorrect.

Supported environments
  • Ubuntu Server 24.04 LTS
  • Apache HTTP Server 2.4.x
Prerequisites
  • Console and recovery access Keep an independent console session and a copy of the current Apache configuration before changing listeners, modules, or access rules.
  • DNS and network plan Resolve every production hostname to the intended public address and list the exact management and client networks that require access.getent ahosts {{domain}} && ip route show
  • Port ownership Confirm that TCP port 80 is free or already belongs to the intended Apache instance.sudo ss -lntp 'sport = :80'
  • Rollback artifact Snapshot the host or create an archive of the active configuration and web roots before the first material change.
Operating boundary

OneLiners never runs these steps or stores secrets. Review placeholders, versions, current state, and change-control requirements before using a command.

Full guide

What you will build

System
  • Two DNS names served from separate deployment-owned document roots with explicit Apache Directory policy and independent access/error logs.
  • A root-owned rejecting default virtual host that returns 404 for missing, unknown, direct-IP, or malicious Host values instead of leaking either tenant.
  • A deterministic vhost map and positive/negative routing test set that proves aliases, fallback behavior, content isolation, log separation, and rollback.
Observable outcome
  • Requests for domain A and B reach only their own markers and logs; an unknown Host receives the rejecting response and never another site's content.
  • Apache's `-S` map identifies exactly one intentional default and one definition for each name/alias, with no duplicate enabled symlink.
  • Document roots are not shared or writable by the Apache runtime, DNS/certificate prerequisites are explicit, and the previous site layout can be restored.

Architecture

How the parts fit together

One Apache TCP/80 listener selects a virtual host from the HTTP Host header. The lexically first enabled definition is the fallback, so a dedicated `000-reject.conf` owns that position and returns 404. Each real site declares canonical ServerName, optional www alias, isolated `/srv/www/<domain>/public`, explicit Directory authorization, disabled indexing/overrides, and separate logs. DNS only routes clients to the server; Apache still performs Host selection.

Rejecting defaultReceives unknown/missing Host values and returns 404 without exposing a tenant document root.
Site A and Site BMap reviewed names and aliases to separate roots and logs on the shared listener.
Deployment accountOwns published content while the www-data runtime reads but cannot modify it.
Vhost mapShows selection order, default host, aliases, and source files before reload.
Routing fixturesUse loopback address overrides and explicit Host values to prove positive and negative behavior independently of DNS.
  1. Verify every A/AAAA record and intended server address, then archive available and enabled site state including symlink selection.
  2. Create separate deployment-owned roots and deterministic markers without granting www-data write access.
  3. Define each site with explicit name, alias, root, Directory access, indexing policy, overrides policy, and log files.
  4. Create and enable a rejecting default, disable the Ubuntu sample, and enable each real site exactly once.
  5. Review configtest and the complete vhost map before reload.
  6. Test A, B, aliases, an unknown Host, and direct-address behavior; inspect per-site logs and ensure the reject path contains no tenant marker.

Assumptions

  • The Apache baseline is healthy, TCP/80 exposure is intentional, and no reverse proxy or CDN rewrites Host without a documented trust model.
  • Both apex and optional www names resolve to the intended address for public use; loopback `--resolve` tests can precede DNS cutover.
  • The two sites are separate security and deployment boundaries even if one team currently owns both.
  • TLS is configured in the next stage with certificates covering every enabled name; this guide proves HTTP routing first.
  • Application writable data and uploads are outside public executable roots and have separate ownership and serving rules.

Key concepts

Name-based virtual host
A site selected from local address/port plus the HTTP Host field rather than a dedicated server address.
Default virtual host
The first matching address/port definition used when no ServerName or ServerAlias matches.
ServerAlias
An additional hostname accepted by one virtual host; every alias should have deliberate DNS and certificate coverage.
DocumentRoot
The filesystem directory mapped to URL paths for a virtual host; Directory authorization is configured separately.
AllowOverride None
Prevents per-directory `.htaccess` files from changing policy, keeping configuration centralized and reviewable.

Before you copy

Values used in this guide

{{domain}}

Representative name used by the shared prerequisite DNS check.

Example: example-a.test
{{domainA}}

Canonical first site name, without scheme or path.

Example: example-a.test
{{domainB}}

Canonical second site name, without scheme or path.

Example: example-b.test
{{deployUser}}

Non-runtime deployment account that owns published content.

Example: webdeploy

Security and production boundaries

  • The Host header is client-controlled. A rejecting default prevents accidental content disclosure but is not an application authorization boundary.
  • Do not make www-data or unrelated tenant deployment accounts owners of another site's files.
  • AllowOverride None prevents hidden `.htaccess` policy, while Options -Indexes avoids directory listing; neither blocks vulnerable application files.
  • Per-site logs can contain credentials in URLs, personal data, and internal paths. Restrict, rotate, redact, and retain them deliberately.
  • Add TLS for all canonical names and aliases before serving authentication or sensitive content.

Stop before continuing if

  • Stop if DNS points to another system, an enabled vhost is undocumented, roots overlap, or the deployment owner is unclear.
  • Do not reload when configtest or `apache2ctl -S` shows duplicate names, duplicate enabled files, or the wrong default.
  • Do not accept routing until both positive hosts, aliases, unknown Host, and direct-address requests have expected distinct results.
  • Do not delete old roots or site definitions until content is backed up and rollback no longer needs them.
01

verification

Verify DNS for every virtual host

read-only

Resolve the apex and www names before changing Apache; mismatched DNS cannot be repaired by a virtual-host declaration.

Why this step matters

DNS evidence prevents an Apache routing change from being blamed for names that actually resolve to another server or address family.

What to understand

Resolve apex and aliases through the production resolver, compare every A/AAAA result with host addresses, TTL, proxy/CDN, and cutover plan.

An IPv6 AAAA record is independently reachable even when IPv4 is correct; test and firewall both.

Use local `--resolve` fixtures for pre-cutover configuration, but do not confuse them with public DNS proof.

System changes

  • No persistent change; reads DNS and local interface state.

Syntax explained

getent ahosts
Resolves through host NSS configuration and displays address families.
ip -brief address
Shows addresses actually assigned to the server.
Command
Fill variables0/2 ready

Values stay on this page and are never sent or saved.

getent ahosts {{domainA}} && getent ahosts {{domainB}} && ip -brief address
Example output / evidence
203.0.113.10 STREAM example-a.test
203.0.113.10 STREAM example-b.test

Checkpoint: Checkpoint: dns

Continue whenEvery intended name/address is documented and either points here or has a scheduled cutover; no stray AAAA remains.

Stop whenA name resolves elsewhere, ownership is unclear, address family is unfiltered, or certificate coverage is unplanned.

If this step fails

Apex resolves correctly but www points elsewhere.

Likely causeStale or independently managed DNS record.

Safe checks
  • getent ahosts www.example-a.test

ResolutionCorrect or deliberately retire the alias before enabling it in Apache.

Security notes

  • DNS ownership is part of site control; protect registrar and DNS credentials with MFA and audit.

Alternatives

  • Test only with `--resolve` until the approved DNS cutover window.

Stop conditions

  • A name resolves elsewhere, ownership is unclear, address family is unfiltered, or certificate coverage is unplanned.
02

command

Archive active site definitions

caution

Preserve both available and enabled site state so the exact symlink selection can be reconstructed.

Why this step matters

Archiving both available files and enabled symlinks preserves not only content but the exact active selection and default order.

What to understand

Store the archive outside web roots, record checksum/listing, and confirm enough storage.

Enabled symlinks determine lexical default and duplicate activation; they are essential rollback state.

Document any configuration included outside the two standard directories separately.

System changes

  • Creates a compressed root-owned archive of sites-available and sites-enabled plus checksum evidence.

Syntax explained

tar -C /etc/apache2
Archives paths relative to the Apache configuration root.
-czf
Creates a gzip-compressed archive file.
sha256sum
Records integrity evidence.
Command
sudo tar -C /etc/apache2 -czf /root/apache-sites-before-vhosts.tgz sites-available sites-enabled && sudo sha256sum /root/apache-sites-before-vhosts.tgz
Example output / evidence
4c1f...  /root/apache-sites-before-vhosts.tgz

Checkpoint: Checkpoint: backup

Continue whenArchive is non-empty, checksum verifies, and listing includes every current file and enabled symlink.

Stop whenArchive misses external includes, checksum fails, storage is insufficient, or current selection is undocumented.

If this step fails

Restored files exist but the active sites differ.

Likely causeSymlink selection/ordering was not preserved or restored.

Safe checks
  • tar -tzf /root/apache-sites-before-vhosts.tgz
  • find /etc/apache2/sites-enabled -type l -ls

ResolutionRestore both directories as one reviewed unit and validate -S before reload.

Security notes

  • Archives expose domain topology and possibly credentials; keep them root-only.

Alternatives

  • Use a version-controlled configuration plus filesystem snapshot when both are tested for symlink restoration.

Stop conditions

  • Archive misses external includes, checksum fails, storage is insufficient, or current selection is undocumented.
03

command

Create isolated document roots

caution

Use one root per site, owned by its deployment account rather than the Apache runtime user; publish deterministic health pages for routing tests.

Why this step matters

Separate roots and a non-runtime deployment owner prevent one site's release process or compromised Apache worker from modifying another tenant's content.

What to understand

Directories use deployment-user ownership and www-data group read/traversal; Apache should not own writable source trees.

Deterministic markers make routing assertions unambiguous before applications introduce caches or redirects.

Keep uploads and mutable state outside public roots with explicit handlers and backup policy.

System changes

  • Creates two `/srv/www/<domain>/public` trees and deterministic index markers with deployment ownership.

Syntax explained

install -d -m 0755
Creates traversable/readable directories with explicit modes.
-o deployUser -g www-data
Separates deployment ownership from runtime group access.
tee index.html
Writes an exact hostname marker for routing tests.
Command
Fill variables0/3 ready

Values stay on this page and are never sent or saved.

sudo install -d -m 0755 -o {{deployUser}} -g www-data /srv/www/{{domainA}}/public /srv/www/{{domainB}}/public && printf '<h1>{{domainA}}</h1>\n' | sudo tee /srv/www/{{domainA}}/public/index.html >/dev/null && printf '<h1>{{domainB}}</h1>\n' | sudo tee /srv/www/{{domainB}}/public/index.html >/dev/null
Example output / evidence
/srv/www/example-a.test/public/index.html
/srv/www/example-b.test/public/index.html

Checkpoint: Checkpoint: roots

Continue whenRoots resolve to distinct paths, markers differ, deployment user owns them, and www-data cannot write.

Stop whenPaths overlap/symlink unexpectedly, content would be overwritten, or runtime/unrelated tenant can write.

If this step fails

Apache cannot read a root despite readable index.

Likely causeA parent directory lacks traversal, Directory policy differs, or mandatory access control denies it.

Safe checks
  • namei -l /srv/www/example-a.test/public/index.html

ResolutionGrant the minimum parent traversal/read path and inspect the site error log.

Security notes

  • Never solve access by recursively setting 777 or making www-data the deployment owner.

Alternatives

  • Deploy immutable version directories and atomically switch a root-owned `current` symlink.

Stop conditions

  • Paths overlap/symlink unexpectedly, content would be overwritten, or runtime/unrelated tenant can write.
04

config

Define the first named host

caution

Declare ServerName, optional alias, document root, explicit directory access, and per-site logs in one file.

Why this step matters

A complete site definition keeps name, alias, root, authorization, override/index policy, and logs together for review and rollback.

What to understand

ServerName is canonical; enable the www alias only when DNS/TLS and redirect/content behavior are deliberate.

Require all granted is scoped to the exact public root, while AllowOverride None centralizes policy and Options -Indexes prevents listing.

Separate logs preserve tenant routing evidence and simplify incident isolation.

System changes

  • Creates the root-owned site A virtual-host configuration under sites-available.

Syntax explained

<VirtualHost *:80>
Attaches this named host to all configured port-80 listener addresses.
ServerName / ServerAlias
Declares canonical and accepted HTTP Host names.
DocumentRoot / Directory
Maps content and authorizes access to the exact path.
Options -Indexes +FollowSymLinks
Disables directory listing while permitting reviewed symlink traversal.
CustomLog combined
Writes site-specific standard access-log fields.
File /etc/apache2/sites-available/{{domainA}}.conf
Configuration
Fill variables0/1 ready

Values stay on this page and are never sent or saved.

printf '<VirtualHost *:80>\n  ServerName {{domainA}}\n  ServerAlias www.{{domainA}}\n  DocumentRoot /srv/www/{{domainA}}/public\n  <Directory /srv/www/{{domainA}}/public>\n    Require all granted\n    AllowOverride None\n    Options -Indexes +FollowSymLinks\n  </Directory>\n  ErrorLog ${APACHE_LOG_DIR}/{{domainA}}-error.log\n  CustomLog ${APACHE_LOG_DIR}/{{domainA}}-access.log combined\n</VirtualHost>\n' | sudo tee /etc/apache2/sites-available/{{domainA}}.conf
Example output / evidence
<VirtualHost *:80>
  ServerName example-a.test
  DocumentRoot /srv/www/example-a.test/public

Checkpoint: Checkpoint: site a

Continue whenThe file contains only domain A names/paths/logs, is root-owned, and has no wildcard aliases or writable policy.

Stop whenAlias, root, Directory path, log path, override, option, or ownership is wrong.

If this step fails

Apache warns that DocumentRoot does not exist.

Likely causeDomain placeholder differs from the created directory or a typo is present.

Safe checks
  • sudo apache2ctl configtest
  • namei -l /srv/www/example-a.test/public

ResolutionCorrect the exact path and keep the site disabled until parser and access checks pass.

Security notes

  • A wildcard ServerAlias can capture unrelated domains; avoid it unless a validated application requires it.

Alternatives

  • Redirect www to the canonical apex in a separate explicit vhost instead of serving both.

Stop conditions

  • Alias, root, Directory path, log path, override, option, or ownership is wrong.
05

config

Define the second named host

caution

Mirror the isolation pattern with the second domain and its own logs; never share a writable document root between unrelated sites.

Why this step matters

Mirroring the reviewed isolation pattern with entirely separate names, root, and logs prevents accidental coupling between the two sites.

What to understand

Do not copy domain A without replacing every name/path/log occurrence; review a unified diff between definitions.

Document root sharing undermines deployment and incident isolation even when pages initially match.

Keep per-site authorization explicit because global Directory policy may change later.

System changes

  • Creates the root-owned site B virtual-host configuration under sites-available.

Syntax explained

ServerName domainB
Selects the second site for its canonical Host.
ServerAlias www.domainB
Optionally selects it for the reviewed alias.
ErrorLog / CustomLog
Separates diagnostic and request evidence from site A.
AllowOverride None
Prevents hidden per-directory policy changes.
File /etc/apache2/sites-available/{{domainB}}.conf
Configuration
Fill variables0/1 ready

Values stay on this page and are never sent or saved.

printf '<VirtualHost *:80>\n  ServerName {{domainB}}\n  ServerAlias www.{{domainB}}\n  DocumentRoot /srv/www/{{domainB}}/public\n  <Directory /srv/www/{{domainB}}/public>\n    Require all granted\n    AllowOverride None\n    Options -Indexes +FollowSymLinks\n  </Directory>\n  ErrorLog ${APACHE_LOG_DIR}/{{domainB}}-error.log\n  CustomLog ${APACHE_LOG_DIR}/{{domainB}}-access.log combined\n</VirtualHost>\n' | sudo tee /etc/apache2/sites-available/{{domainB}}.conf
Example output / evidence
<VirtualHost *:80>
  ServerName example-b.test
  DocumentRoot /srv/www/example-b.test/public

Checkpoint: Checkpoint: site b

Continue whenSite B names, root, and logs are unique and no reference to domain A remains.

Stop whenAny name/path/log is shared unintentionally or the runtime can write site B content.

If this step fails

Site B requests appear in site A logs.

Likely causeThe request matched site A/default because site B name or enablement is wrong.

Safe checks
  • sudo apache2ctl -S
  • sudo tail -n 20 /var/log/apache2/example-a.test-access.log

ResolutionCorrect site B ServerName/enablement and repeat a named loopback request.

Security notes

  • Separate logs and roots reduce accidental disclosure but do not isolate processes for dynamic applications.

Alternatives

  • Use separate hosts/containers when tenant process isolation is required.

Stop conditions

  • Any name/path/log is shared unintentionally or the runtime can write site B content.
06

command

Enable sites and choose the default deliberately

caution

Create a rejecting default so unknown Host values cannot reveal a tenant, disable the sample host, and enable both named definitions exactly once.

Why this step matters

A dedicated lexically first rejecting vhost prevents unknown Host values from falling into tenant A, while each real site is enabled exactly once.

What to understand

The command writes `000-reject.conf`, disables Ubuntu's sample, and enables reject/A/B through normal a2ensite symlinks.

The rejecting host uses no tenant DocumentRoot and returns 404 for every path. It owns only the fallback position.

Avoid manually adding a second 000-domain symlink beside an a2ensite link; duplicate enablement produces misleading maps and behavior.

System changes

  • Creates/enables a rejecting default, disables 000-default, and enables domain A and B exactly once.

Syntax explained

000-reject.conf
Sorts before ordinary domain files and becomes the `*:80` fallback.
Redirect 404 /
Returns 404 for all paths without serving tenant content.
a2dissite 000-default
Removes Ubuntu's sample host selection.
a2ensite
Creates one managed enabled symlink per reviewed site.
Command
Fill variables0/2 ready

Values stay on this page and are never sent or saved.

printf '<VirtualHost *:80>\n  ServerName _default.invalid\n  Redirect 404 /\n</VirtualHost>\n' | sudo tee /etc/apache2/sites-available/000-reject.conf >/dev/null && sudo a2dissite 000-default && sudo a2ensite 000-reject {{domainA}} {{domainB}}
Example output / evidence
Site 000-default disabled.
Enabling site 000-reject.
Enabling site example-a.test.
Enabling site example-b.test.

Checkpoint: Checkpoint: enable

Continue whenOne reject, one domain A, and one domain B symlink exist; reject is the default and no duplicate real site is enabled.

Stop whenSample remains enabled, reject is not first/default, a real site is duplicated, or unknown Host can reach tenant content.

If this step fails

a2ensite says a site is already enabled but duplicates appear.

Likely causeA manually named symlink also targets the same available file.

Safe checks
  • find /etc/apache2/sites-enabled -maxdepth 1 -type l -printf '%f -> %l\n'

ResolutionRemove only the confirmed duplicate symlink, retain the managed site link, and validate -S.

Security notes

  • The fallback must not proxy, redirect to a tenant, or expose filesystem information.

Alternatives

  • Return 421 Misdirected Request through mod_rewrite when operational policy prefers that status and compatibility is tested.

Stop conditions

  • Sample remains enabled, reject is not first/default, a real site is duplicated, or unknown Host can reach tenant content.
07

verification

Inspect Apache's host matching table

read-only

Use configtest and -S to confirm both names, aliases, file origins, and the intended default before reloading.

Why this step matters

The parser and vhost map are authoritative pre-reload evidence for default order, aliases, duplicates, source files, and listener coverage.

What to understand

Review every `*:80` entry and the words `default server`; the rejecting file must own the default.

Each canonical and alias name should map once to the expected source file.

Configtest cannot prove response content, so route fixtures follow after reload.

System changes

  • No persistent change; parses configuration and displays the effective vhost selection table.

Syntax explained

configtest
Parses all enabled Apache configuration.
-S
Dumps address/port defaults, names, aliases, and source lines.
Command
sudo apache2ctl configtest && sudo apache2ctl -S
Example output / evidence
Syntax OK
*:80 example-a.test (/etc/apache2/sites-enabled/000-example-a.test.conf:1)
port 80 namevhost example-b.test

Checkpoint: Checkpoint: validate

Continue whenSyntax OK, reject is default, each site appears once, aliases map correctly, and no sample/duplicate remains.

Stop whenAny warning/error, wrong default, duplicate name, missing alias, or unexpected source file appears.

If this step fails

The intended reject host is not default.

Likely causeAnother enabled filename sorts earlier or binds more specifically.

Safe checks
  • sudo apache2ctl -S
  • ls -la /etc/apache2/sites-enabled

ResolutionCorrect enable order/address overlap and repeat the map; never rely on assumed lexical order.

Security notes

  • Unknown enabled vhosts can expose old applications or admin tools; treat them as blockers.

Alternatives

  • Use DUMP_VHOSTS alongside automated assertions in deployment CI.

Stop conditions

  • Any warning/error, wrong default, duplicate name, missing alias, or unexpected source file appears.
08

verification

Reload and prove content isolation

read-only

Resolve both names to localhost and assert that each returns only its own marker; then inspect the two access logs.

Why this step matters

Positive and negative HTTP fixtures prove actual post-reload Host selection, content isolation, fallback behavior, and per-site logging independently of DNS.

What to understand

Use `--resolve` for each canonical and alias so destination is loopback but Host remains realistic.

Send an explicit unknown Host and a direct-address request; both must receive reject status without either marker.

Inspect both access logs to ensure only the matching site's request was recorded, then review journal for reload errors.

System changes

  • Reloads Apache and creates bounded request plus access/error-log evidence; site content/config remains unchanged.

Syntax explained

systemctl reload
Gracefully applies the validated enabled-site layout.
curl --resolve
Overrides DNS while preserving the requested Host.
--fail
Treats HTTP error responses as command failures for positive fixtures.
-H 'Host: unknown.invalid'
Exercises the rejecting default explicitly.
Command
Fill variables0/2 ready

Values stay on this page and are never sent or saved.

sudo systemctl reload apache2 && curl --fail --resolve '{{domainA}}:80:127.0.0.1' http://{{domainA}}/ && curl --fail --resolve '{{domainB}}:80:127.0.0.1' http://{{domainB}}/ && sudo tail -n 2 /var/log/apache2/{{domainA}}-access.log /var/log/apache2/{{domainB}}-access.log
Example output / evidence
<h1>example-a.test</h1>
<h1>example-b.test</h1>
GET / HTTP/1.1" 200

Checkpoint: Checkpoint: route tests

Continue whenA and B return their own markers/logs, aliases behave deliberately, and unknown/direct-IP requests return only reject status.

Stop whenCross-content appears, unknown Host returns a tenant, logs mix, aliases surprise, reload/journal errors, or either positive fixture fails.

If this step fails

The correct marker appears but in both log files.

Likely causeA global CustomLog duplicates requests or both vhosts share a log path.

Safe checks
  • grep -R --line-number 'CustomLog' /etc/apache2

ResolutionRemove unintended global/duplicate logging or correct per-site paths, then repeat one request at a time.

Security notes

  • Use harmless test paths and avoid placing secrets in URLs that logs will retain.

Alternatives

  • Run the same matrix from an external probe after DNS cutover in addition to local fixtures.

Stop conditions

  • Cross-content appears, unknown Host returns a tenant, logs mix, aliases surprise, reload/journal errors, or either positive fixture fails.

Finish line

Verification checklist

Virtual-host mapsudo apache2ctl -SLists every ServerName once and identifies the intended default.
Host isolationfor d in {{domainA}} {{domainB}}; do curl --fail --silent --resolve "$d:80:127.0.0.1" "http://$d/"; doneEach request returns the marker belonging to that hostname.

Recovery guidance

Common problems and safe checks

Both names return site A.

Likely causeSite B is not enabled, DNS/request Host is wrong, ServerName is misspelled, or the default catches the request.

Safe checks
  • sudo apache2ctl -S
  • curl -v --resolve 'example-b.test:80:127.0.0.1' http://example-b.test/

ResolutionCorrect the exact enabled file and name mapping, then repeat the local Host fixture before DNS changes.

apache2ctl -S lists the same domain twice.

Likely causea2ensite and a manual 000-prefixed symlink both enable the same file, or duplicate configs declare it.

Safe checks
  • find /etc/apache2/sites-enabled -maxdepth 1 -type l -printf '%f -> %l\n'
  • grep -R --line-number 'ServerName example-a.test' /etc/apache2

ResolutionKeep one real-site symlink and the separate rejecting default; remove only the confirmed duplicate after backup.

Unknown Host returns tenant content.

Likely causeThe rejecting file is disabled, sorted after another definition, or does not share the same address/port.

Safe checks
  • sudo apache2ctl -S
  • curl -i -H 'Host: unknown.invalid' http://127.0.0.1/

ResolutionMake the root-owned reject vhost the first enabled definition for `*:80`, validate, reload, and repeat negative fixtures.

Apache returns 403 for the correct site.

Likely causeDirectory Require policy, traversal permissions, root ownership/mode, AppArmor, or wrong DocumentRoot prevents access.

Safe checks
  • namei -l /srv/www/example-a.test/public/index.html
  • sudo tail -n 50 /var/log/apache2/example-a.test-error.log

ResolutionCorrect the minimum traversal/read permission or exact Directory path; do not make the tree world-writable.

After the procedure

Alternatives and next steps

Consider these alternatives

  • Use separate IP addresses or hosts when tenant isolation, compliance, capacity, or failure domains require stronger separation.
  • Use one explicit canonical redirect instead of a www alias when duplicate hostnames should converge.
  • Route applications through reverse-proxy vhosts while retaining the same rejecting default and Host test strategy.

Operate it safely

  • Issue and renew certificates for every canonical name and alias, then redirect HTTP to HTTPS while preserving ACME reachability.
  • Add per-site headers, application/runtime isolation, deployment atomicity, health checks, and structured logging.
  • Monitor unknown-Host rates, per-site status/latency, TLS expiry, log pipeline health, disk usage, and Apache worker saturation.
  • Replace deterministic markers with versioned application releases only after backup, rollback, and external acceptance probes are ready.

Reference

Frequently asked questions

Why not make domain A the default?

Unknown, direct-IP, stale-DNS, and malicious Host requests would receive tenant A content. A rejecting default avoids that cross-serving.

Does DNS decide the virtual host?

DNS chooses an address. Apache chooses the vhost from local address/port and the HTTP Host header.

Should www be a ServerAlias?

Only when DNS and TLS cover it and serving or redirecting it is intentional.

Recovery

Rollback

Disable the new host files and restore the archived enabled-site layout without deleting retained document roots.

  1. Run a2dissite for 000-reject and both new domains, then remove only the rejecting site file created by this guide.
  2. Restore sites-available and sites-enabled from /root/apache-sites-before-vhosts.tgz.
  3. Run apache2ctl configtest and apache2ctl -S before reloading.
  4. Keep /srv/www roots until their content has been backed up and explicitly retired.

Evidence

Sources and review

Verified 2026-07-25Review due 2026-10-23
Apache virtual hostsofficialUbuntu: configure Apache2officialApache HTTP Server 2.4 documentationofficial