Implementing Multi-Tenant Web Hosting with Docker: Virtual Host Routing for Containerized LAMP Stacks


2 views

When deploying multiple websites on a single host using Docker, the traditional port-based exposure becomes impractical for web hosting scenarios. The solution lies in combining Docker's isolation capabilities with reverse proxy routing based on virtual hosts.

Here's a complete implementation using Nginx as reverse proxy and Docker containers:


# nginx.conf (host machine)
server {
    listen 80;
    server_name www.somewebsite.com;
    location / {
        proxy_pass http://container1:80;
        proxy_set_header Host $host;
    }
}

server {
    listen 80;
    server_name www.otherwebsite.com;
    location / {
        proxy_pass http://container2:80;
        proxy_set_header Host $host;
    }
}

Each website runs in its own container with identical port mappings, differentiated by hostname routing:


# docker-compose.yml for website A
version: '3'
services:
  webserver:
    image: custom-lamp-image
    container_name: container1
    ports:
      - "8080:80"
    volumes:
      - ./site1:/var/www/html

# docker-compose.yml for website B
version: '3'
services:
  webserver:
    image: custom-lamp-image
    container_name: container2
    ports:
      - "8081:80"
    volumes:
      - ./site2:/var/www/html

The overhead of containerization is minimal (1-3% CPU, negligible memory for isolated processes). Benefits include:

  • Process isolation prevents resource contention
  • Security boundaries between tenants
  • Independent scaling of individual sites
  • Simplified backup/restore via container snapshots

For large-scale hosting, automate container provisioning with this bash script:


#!/bin/bash
DOMAIN=$1
CONTAINER_NAME="site_${DOMAIN//./_}"

docker run -d \
  --name $CONTAINER_NAME \
  -e VIRTUAL_HOST=$DOMAIN \
  -v /sites/$DOMAIN:/var/www/html \
  custom-lamp-image

# Auto-generate Nginx config
echo "server {
    listen 80;
    server_name $DOMAIN;
    location / {
        proxy_pass http://$CONTAINER_NAME:80;
    }
}" > /etc/nginx/conf.d/$DOMAIN.conf

nginx -s reload

For more complex scenarios, consider:

  1. Traefik as reverse proxy with automatic Docker discovery
  2. Kubernetes ingress controllers for large-scale deployments
  3. Apache with mod_proxy in place of Nginx

When managing shared hosting environments, isolation between user websites becomes critical. Traditional approaches using chroot or separate user accounts have limitations in resource control and security. Docker presents an intriguing alternative for creating isolated LAMP environments per website.

The core concept involves:


+-----------------------+
| Host System           |
|  - Reverse Proxy      |
|  - Docker Daemon      |
+-----------------------+
        |  |
        |  +--> [Website1 Container] (LAMP stack)
        |  +--> [Website2 Container] (LAMP stack)
        |  +--> [WebsiteN Container] (LAMP stack)

Here's a practical implementation using Nginx as reverse proxy:


# docker-compose.yml for website containers
version: '3'
services:
  website1:
    image: custom-lamp-image
    environment:
      - VIRTUAL_HOST=www.somewebsite.com
    volumes:
      - ./website1:/var/www/html

  website2:
    image: custom-lamp-image
    environment:
      - VIRTUAL_HOST=www.otherwebsite.com
    volumes:
      - ./website2:/var/www/html

# Nginx reverse proxy configuration
server {
    listen 80;
    server_name www.somewebsite.com;
    
    location / {
        proxy_pass http://website1;
        proxy_set_header Host $host;
        proxy_set_header X-Real-IP $remote_addr;
    }
}

server {
    listen 80;
    server_name www.otherwebsite.com;
    
    location / {
        proxy_pass http://website2;
        proxy_set_header Host $host;
        proxy_set_header X-Real-IP $remote_addr;
    }
}

While Docker adds minimal overhead (1-3% typically), consider:

  • Shared database containers for better resource utilization
  • Bind mounts for persistent storage instead of volumes
  • Resource limits in docker-compose.yml

Container isolation provides:

  • Process isolation between websites
  • Filesystem isolation
  • Network namespace separation
  • Resource constraints per container

For dynamic environments, Traefik automatically discovers containers:


# docker-compose.yml with Traefik labels
services:
  website1:
    image: custom-lamp-image
    labels:
      - "traefik.http.routers.website1.rule=Host(www.somewebsite.com)"
    volumes:
      - ./website1:/var/www/html

Options for MySQL/MariaDB:

  1. Separate DB container per website (maximum isolation)
  2. Shared DB container with separate databases
  3. External managed database service

version: '3.7'

services:
  wordpress1:
    image: wordpress:php7.4-apache
    environment:
      WORDPRESS_DB_HOST: db1
      WORDPRESS_DB_USER: user1
      WORDPRESS_DB_PASSWORD: password1
      WORDPRESS_DB_NAME: wp1
      VIRTUAL_HOST: site1.example.com
    volumes:
      - ./wp-content1:/var/www/html/wp-content

  db1:
    image: mysql:5.7
    environment:
      MYSQL_ROOT_PASSWORD: rootpass
      MYSQL_DATABASE: wp1
      MYSQL_USER: user1
      MYSQL_PASSWORD: password1