← All articles

How to Host Multiple Projects on One Server with Subdomains

A field guide to sharing one server across static sites and web apps: DNS, Apache virtual hosts, loopback proxies, TLS certificates and operations, illustrated with the live 40i.net setup.

A subdomain is a routing name, not an application boundary. DNS sends it to an IP; Apache selects a site from the requested hostname; the selected virtual host serves files or proxies to a local process. On 40i.net, the root domain serves the static wiki from /var/www/40i-wiki, site-builder.40i.net proxies to Rails on 127.0.0.1:3000, and other subdomains have separate static roots or application backends. This guide follows that arrangement from DNS to deployment.

How does one request find the right project?

A request passes through four independent choices: DNS resolves the hostname to an IP; TLS presents a certificate; Apache selects a virtual host by hostname; that host serves files or forwards the request to an application port. These layers are easy to mix up during an outage. DNS does not start a process or issue its certificate, and Apache does not keep an application running. Inspect each layer separately.

Request path through one server
Browser
  │  https://app.example.com
  ▼
DNS A / wildcard ──► SERVER_IP
  │
  ▼
Apache :443 ── TLS certificate + Host virtual host
  ├── DocumentRoot ──► static site files
  └── ProxyPass ─────► 127.0.0.1:3001 (app process)

Which DNS records should point to the server?

For a few named projects, create one A record per subdomain and point each to the server’s public IPv4 address. A wildcard record such as *.example.com can direct otherwise unmatched subdomains to the same address; the root name still needs its own record. More specific DNS records take precedence. A wildcard is a convenience for name resolution, not a rule that creates websites. Preserve MX, SPF, DKIM and DMARC records when editing web DNS.

Why keep Apache instead of switching to nginx?

This is a decision about the server already running the projects, not a claim that Apache is universally better. The 40i.net host runs Apache 2.4; nginx is not installed. ISPConfig manages this host’s Apache virtual-host files and ACME challenge mapping, while the existing sites, logs and certificate workflow already depend on that configuration. ISPConfig supports both Apache and nginx, so the panel alone does not force the choice. For this server, keeping Apache avoids migrating generated virtual hosts and TLS handling or adding another proxy layer, with little practical gain for the current workload. nginx is a sound option for a new server or a measured need; switch only with a migration plan for every site, port 80/443 owner, certificate renewal path and rollback.

How ISPConfig and the web server fit together
ISPConfig control panel
  └── generates/manages virtual hosts and ACME mapping
       │
       ▼
Apache 2.4 (active on 40i.net)
  ├── static sites
  └── reverse proxy ──► app on 127.0.0.1:PORT

nginx: not installed on this host

How does Apache select the virtual host?

Apache name-based hosting compares the requested Host header with ServerName and ServerAlias among virtual hosts listening on the destination port. Give every host an explicit ServerName; use ServerAlias only for additional names that should show the same project. A broad *.example.com alias can claim a subdomain intended for another application. If no host matches, Apache uses the first virtual host for that address and port, so define a deliberate default instead of exposing an arbitrary project.

When should Apache serve files instead of proxying?

For an exported static site, assign a dedicated DocumentRoot and a DirectoryIndex. No app process or application port is needed. The root wiki at 40i.net uses this model; Transcriber and Profile Analyzer also have static public pages. Put each published site in its own directory and give Apache read access only. Do not place environment files, credentials, backups or repository metadata in the document root. The virtual host separates URL routing; filesystem permissions limit what the server can read.

How should Apache proxy a web application?

Run the app on its own loopback port and let Apache accept public HTTP and HTTPS. Bind the backend to 127.0.0.1 so external clients cannot bypass Apache’s TLS and host routing. Preserve the original Host header and forward the HTTPS scheme so the framework generates correct redirects, cookies and links. On 40i.net, site-builder.40i.net proxies to Rails on port 3000. Other projects use separate static roots or loopback backends. WebSocket routes need a dedicated proxy rule before the catch-all HTTP rule.

Illustrative HTTPS host for a local application backend
<VirtualHost *:443>
    ServerName app.example.com
    SSLEngine on
    SSLCertificateFile /etc/letsencrypt/live/app.example.com/fullchain.pem
    SSLCertificateKeyFile /etc/letsencrypt/live/app.example.com/privkey.pem

    ProxyPreserveHost On
    RequestHeader set X-Forwarded-Proto "https"
    RequestHeader set X-Forwarded-Port "443"
    ProxyPass / http://127.0.0.1:3001/
    ProxyPassReverse / http://127.0.0.1:3001/
</VirtualHost>

How do systemd and application settings fit in?

Use a separate systemd service for each long-running web process and worker. Set its Unix user, working directory, environment file and restart behavior. Keep secrets in a protected environment or secret store, outside Git and the public document root. This Rails deployment runs its web process and background jobs as separate services. After changing a unit file, run systemctl daemon-reload and restart that service; after changing environment values, restart the application so it reads them. Check the service journal before investigating Apache.

How does an app know which hostname it serves?

A reverse proxy normally preserves the public Host header, but the application still needs an explicit list of allowed hosts and a canonical URL. Here, APP_HOST sets the Rails host, APP_BASE_DOMAIN sets the tenant-subdomain base, and RAILS_ALLOWED_HOSTS controls accepted request hosts. Tenant routing must also reserve names such as www and admin so they are not mistaken for tenants. After moving a host, test redirects, sign-in and password-reset links, cookies, WebSockets and canonical URLs; those often reveal a stale hostname.

Does a wildcard DNS record provide wildcard HTTPS?

No. DNS controls where a name resolves; TLS certificates require separate domain validation. Let’s Encrypt HTTP-01 fetches a token under /.well-known/acme-challenge/ over port 80. DNS-01 validates a TXT record and is required for wildcard certificates. Exact-name certificates work well for a small, stable list; DNS-01 wildcard certificates fit frequently changing subdomains when the DNS provider supports safe automation. A SAN certificate that lists many exact names must be updated without dropping any hostname still in service.

Keep the HTTP-01 challenge path available before redirecting other traffic
Alias /.well-known/acme-challenge/ /path/served/by/certbot/

RewriteEngine On
RewriteCond %{REQUEST_URI} !^/\.well-known/acme-challenge/
RewriteRule ^ https://app.example.com%{REQUEST_URI} [R=301,L,NE]

How does certificate renewal work on 40i.net?

The 40i.net host uses an ISPConfig ACME webroot for HTTP-01 validation. Its certificate-sync script collects active website and tenant names, asks Certbot to expand the SAN certificate through the webroot, then reloads Apache. site-builder.40i.net has its own certificate. The challenge URL must remain reachable on port 80; a catch-all redirect or proxy can break validation if it hides the token file. After changing DNS, a webroot or certificate names, inspect the certificate list and run a renewal dry-run.

What does a safe new-subdomain rollout look like?

Reserve a unique name and check it against tenant subdomains and existing vhosts. Add a DNS record or confirm the wildcard points to the intended server. Choose either a dedicated static directory or an unused loopback port. Configure the exact Apache host, app host allow-list, canonical URL and certificate. Validate the Apache configuration before reload, then check the app locally and test the public HTTPS name. Keep the vhost, service unit and deployment command in version control or an operations repository so rollback has a known target.

How can you find which layer failed?

Start with dig +short app.example.com for DNS and apachectl -S for virtual-host matching. Run apachectl configtest before reloading. Check systemctl status and journalctl for the app process. A curl request with the intended Host header to 127.0.0.1:PORT tests the backend without public DNS; openssl s_client with the server name shows the certificate Apache presents. A 502 usually points to an unavailable upstream or wrong port; a certificate mismatch points to TLS or the selected host; an unrelated project page often means no host matched.

When is one server no longer enough?

Subdomains separate routing, not CPU, memory, disks, databases or the machine’s failure domain. A busy app can slow its neighbours, and one host outage takes every project on it offline. A shared database may bottleneck even when application processes use different ports. Monitor resources and test backup restoration. Add service limits when useful. Move a project to a separate server or a load-balanced setup when availability, capacity or stronger isolation calls for it; prepare DNS and valid certificates at the destination before switching traffic.

Related projects

Primary technical documentation