education

Homelab Multi-Tenant WordPress Architecture Playbook

L
Lily
⭐ Featured

Homelab Multi-Tenant WordPress Architecture Playbook

Overview

This playbook outlines the architecture for running a multi-tenant WordPress environment on Proxmox VE using Roots.io Bedrock and Sage 10+ stacks.

Table of Contents
  1. Infrastructure Layer (Proxmox VE)
  2. Network Architecture
  3. Storage Strategy
  4. Internal DNS & Domain Naming
  5. Application Stack (Bedrock + Sage)
  6. WordPress VM/LXC Template Creation
  7. Reverse Proxy & SSL
  8. Database Strategy
  9. Media Storage
  10. CI/CD & Automation
  11. Monitoring & Logging
  12. Backup & Disaster Recovery
  13. Operations Handbook

    1. Infrastructure Layer (Proxmox VE)

    LXC vs VM Decision Matrix
    Factor LXC Containers VM Instances
    Resource Overhead Low (shared kernel) Higher (full OS)
    Performance Near-native Slightly reduced
    Isolation Process-level Hardware-level
    Flexibility Limited kernel mods Full OS control
    WordPress Suitability Good for standard installs Better for custom kernels/modules

    Recommendation: Use LXC for standard WordPress tenants, VMs for tenants requiring custom kernels or specific hardware access.

    Proxmox Host Requirements
    • Minimum: 32GB RAM, 8-core CPU, SSD storage
    • Recommended: 64GB+ RAM, 16+ core CPU, NVMe storage
    • Network: Multiple NICs for separation (management, tenant traffic, storage)

      Host Hardening
      • Regular security updates via pve-enterprise or no-subscription repository
      • Fail2Ban for SSH protection
      • SSH key authentication only
      • Regular backups of /etc/pve, /var/lib/vz, and host configuration

        2. Network Architecture

        Network Segmentation
        Proxmox Host
        ā”œā”€ā”€ Management Network (10.0.0.0/24) - Host access, updates
        ā”œā”€ā”€ Tenant Network (10.0.10.0/24) - WordPress tenant traffic
        ā”œā”€ā”€ Storage Network (10.0.20.0/24) - Ceph/NFS/iSCSI storage
        ā”œā”€ā”€ DMZ Network (10.0.30.0/24) - Reverse proxy, public-facing
        └── Backup Network (10.0.40.0/24) - Backup traffic isolation
        
        

        VLAN Configuration
        • VLAN 10: Tenant Network
        • VLAN 20: Storage Network
        • VLAN 30: DMZ Network
        • VLAN 40: Backup Network

          DHCP & DNS
          • Internal DHCP server (dnsmasq or Pi-hole) for tenant networks
          • Internal DNS for service discovery (wordpress-tenant1.internal)
          • Split-horizon DNS for external vs internal resolution

            Firewall Rules
            • Proxmox built-in firewall enabled
            • Default deny, explicit allow principles
            • Specific rules for:
              • Management access (SSH, web GUI) from trusted IPs only
              • Tenant network to reverse proxy (HTTP/HTTPS)
              • Reverse proxy to tenant networks
              • Storage network access for backup/storage services
              • Monitoring system access to all networks

                3. Storage Strategy

                ZFS Pool Layout
                rpool/
                ā”œā”€ā”€ rpool/ROOT/pve-1 (Proxmox OS)
                ā”œā”€ā”€ rpool/data
                │   ā”œā”€ā”€ vm-100-disk-0
                │   ā”œā”€ā”€ vm-101-disk-0
                │   └── ...
                ā”œā”€ā”€ rpool/tenant-data (tenant-specific data)
                ā”œā”€ā”€ rpool/backups (VM/CT backups)
                └── rpool/iso (ISO images for installation)
                
                

                Storage Recommendations
                • Use SSD/NVMe for rpool (OS and VM disks)
                • Consider separate ZIL/SLOG for synchronous write-heavy workloads
                • L2ARC cache for read-heavy workloads if budget allows
                • Regular scrubs and monitoring via zpool status

                  Backup Strategy
                  • Automated snapshots of VM/CT disks before major changes
                  • Regular vzdump backups to external storage
                  • Off-site replication for critical tenant data
                  • Backup verification and restore testing procedures

                    4. Internal DNS & Domain Naming

                    DNS Strategy
                    • DNS Server: Run an internal DNS server (e.g., Pi-hole, PowerDNS, or BIND) on Proxmox
                    • Domain Structure:
                      • .internal for service domains (proxmox.homelab.internal, wordpress.homelab.internal)
                      • Tenant domains mapped to internal IPs (tenant1.homelab.internal, tenant2.homelab.internal)

                        Naming Conventions
                        • Hostnames: tenantX.internal, wp-serviceX.internal
                        • Domain Naming: tenant1.homelab.internal, tenant2.homelab.internal
                        • DNS Records:
                          • A records for LXC/VM IP addresses
                          • CNAME aliases for service aliases (mysql.homelab.internal, redis.homelab.internal)

                            DNS Security
                            • Split-Horizon DNS: External clients get cloud DNS, internal traffic uses internal DNS
                            • DNSSEC implementation for security
                            • DNS over TLS/HTTPS for encrypted queries

                              5. Application Stack (Bedrock + Sage)

                              Bedrock Implementation

                              Directory Structure per Tenant
                              /srv/wordpress/tenant1/
                              ā”œā”€ā”€ .env
                              ā”œā”€ā”€ composer.json
                              ā”œā”€ā”€ composer.lock
                              ā”œā”€ā”€ config/
                              │   ā”œā”€ā”€ application.php
                              │   └── environments/
                              │       ā”œā”€ā”€ development.php
                              │       ā”œā”€ā”€ staging.php
                              │       └── production.php
                              ā”œā”€ā”€ web/
                              │   ā”œā”€ā”€ wp/
                              │   │   ā”œā”€ā”€ index.php (Bedrock bootstraps)
                              │   │   └── ...
                              │   ā”œā”€ā”€ wp-config.php (Bedrock generated)
                              │   └── .htaccess
                              ā”œā”€ā”€ vendor/
                              └── wp-content/
                                  ā”œā”€ā”€ mu-plugins/
                                  ā”œā”€ā”€ plugins/
                                  ā”œā”€ā”€ themes/
                                  │   ā”œā”€ā”€ sage/ (tenant-specific Sage theme)
                                  │   └── ...
                                  └── uploads/
                              
                              

                              Environment Management
                              • .env files for each environment (dev/staging/prod)
                              • Vault for secret management (AWS Secrets Manager, HashiCorp Vault, or SOPS)
                              • Environment-specific Composer dependencies
                              • Automated .env generation from templates

                                Sage 10+ Implementation

                                Theme Structure per Tenant
                                /srv/wordpress/tenant1/web/wp-content/themes/sage/
                                ā”œā”€ā”€ assets/
                                │   ā”œā”€ā”€ build/ (compiled assets)
                                │   ā”œā”€ā”€ scripts/
                                │   ā”œā”€ā”€ styles/
                                │   └── images/
                                ā”œā”€ā”€ composer.json
                                ā”œā”€ā”€ package.json
                                ā”œā”€ā”€ tailwind.config.js
                                ā”œā”€ā”€ webpack.mix.js or vite.config.js
                                ā”œā”€ā”€ resources/
                                │   ā”œā”€ā”€ views/
                                │   ā”œā”€ā”€ fonts/
                                │   └── images/
                                ā”œā”€ā”€ functions.php
                                └── sage.php
                                
                                

                                Build Process
                                • Local development: yarn start or npm run dev
                                • Production builds: yarn run production or npm run build
                                • Asset versioning for cache busting
                                • Critical CSS extraction
                                • Image optimization pipeline

                                  Multi-tenancy Approaches

                                  Approach 1: Isolated Instances (Recommended)
                                  • Each tenant gets separate LXC container/VM
                                  • Separate database, filesystem, and configuration
                                  • Maximum isolation and security
                                  • Higher resource usage but simpler troubleshooting

                                    Approach 2: Shared Core, Separate Content
                                    • Single WordPress codebase
                                    • Separate databases per tenant
                                    • Shared uploads directory with tenant subdirectories
                                    • Requires multi-tenancy plugin or custom code
                                    • Lower resource usage but complex shared state

                                      Approach 3: Multisite Network
                                      • WordPress multisite with domain mapping
                                      • Shared codebase and database
                                      • Separate tables for site-specific data
                                      • Built-in but limited plugin/theme compatibility

                                        Recommendation: Approach 1 (Isolated Instances) for maximum security and flexibility in a homelab environment.


                                        6. WordPress VM/LXC Template Creation

                                        Template Development

                                        LXC Template Creation Script
                                        #!/bin/bash
                                        # create-wordpress-template.sh
                                        
                                        set -euo pipefail
                                        
                                        TEMPLATE_NAME="wordpress-ubuntu-2204-base"
                                        TEMPLATE_STORAGE="local-zfs"
                                        TEMPLATE_VMID=9000
                                        
                                        # Create base container
                                        pct create $TEMPLATE_VMID local:vz/ubuntu-22.04-standard_22.04-1_amd64.tar.gz \
                                          --storage $TEMPLATE_STORAGE \
                                          --hostname $TEMPLATE_NAME \
                                          --cores 2 \
                                          --memory 2048 \
                                          --swap 512 \
                                          --rootfs $TEMPLATE_STORAGE:10 \
                                          --net0 name=eth0,bridge=vmbr0,ip=dhcp \
                                          --unprivileged 1 \
                                          --features nesting=1 \
                                          --password 'changeme' \
                                          --start 1
                                        
                                        # Wait for container to start
                                        sleep 30
                                        
                                        # Install base packages
                                        pct exec $TEMPLATE_VMID -- apt-get update
                                        pct exec $TEMPLATE_VMID -- apt-get install -y \
                                          nginx \
                                          php8.1-fpm \
                                          php8.1-mysql \
                                          php8.1-xml \
                                          php8.1-curl \
                                          php8.1-gd \
                                          php8.1-mbstring \
                                          php8.1-zip \
                                          php8.1-intl \
                                          php8.1-bcmath \
                                          php8.1-imagick \
                                          composer \
                                          git \
                                          unzip \
                                          curl \
                                          wget \
                                          vim \
                                          htop \
                                          fail2ban \
                                          ufw \
                                          certbot \
                                          python3-certbot-nginx
                                        
                                        # Configure PHP-FPM
                                        pct exec $TEMPLATE_VMID -- sed -i 's/;cgi.fix_pathinfo=1/cgi.fix_pathinfo=0/' /etc/php/8.1/fpm/php.ini
                                        pct exec $TEMPLATE_VMID -- sed -i 's/upload_max_filesize = 2M/upload_max_filesize = 64M/' /etc/php/8.1/fpm/php.ini
                                        pct exec $TEMPLATE_VMID -- sed -i 's/post_max_size = 8M/post_max_size = 64M/' /etc/php/8.1/fpm/php.ini
                                        pct exec $TEMPLATE_VMID -- sed -i 's/max_execution_time = 30/max_execution_time = 120/' /etc/php/8.1/fpm/php.ini
                                        pct exec $TEMPLATE_VMID -- sed -i 's/memory_limit = 128M/memory_limit = 256M/' /etc/php/8.1/fpm/php.ini
                                        
                                        # Configure NGINX for WordPress
                                        pct exec $TEMPLATE_VMID -- cat > /etc/nginx/snippets/wordpress.conf << 'EOF'
                                        # WordPress specific configuration
                                        location / {
                                            try_files $uri $uri/ /index.php?$args;
                                        }
                                        
                                        location ~ \.php$ {
                                            include snippets/fastcgi-php.conf;
                                            fastcgi_pass unix:/run/php/php8.1-fpm.sock;
                                            fastcgi_param SCRIPT_FILENAME $document_root$fastcgi_script_name;
                                            include fastcgi_params;
                                            fastcgi_buffers 16 16k;
                                            fastcgi_buffer_size 32k;
                                        }
                                        
                                        location ~* \.(js|css|png|jpg|jpeg|gif|ico|svg|woff|woff2)$ {
                                            expires 30d;
                                            add_header Cache-Control "public, no-transform";
                                        }
                                        
                                        # Deny access to sensitive files
                                        location ~* \.(htaccess|htpasswd|ini|log|sh|inc|bak|sql)$ {
                                            deny all;
                                        }
                                        
                                        location = /wp-config.php {
                                            deny all;
                                        }
                                        
                                        # Disable XML-RPC
                                        location = /xmlrpc.php {
                                            deny all;
                                        }
                                        EOF
                                        
                                        # Configure security hardening
                                        pct exec $TEMPLATE_VMID -- cat > /etc/nginx/snippets/security-headers.conf << 'EOF'
                                        add_header X-Frame-Options "SAMEORIGIN" always;
                                        add_header X-XSS-Protection "1; mode=block" always;
                                        add_header X-Content-Type-Options "nosniff" always;
                                        add_header Referrer-Policy "strict-origin-when-cross-origin" always;
                                        add_header X-Permitted-Cross-Domain-Policies "none" always;
                                        add_header Content-Security-Policy "default-src 'self'; script-src 'self' 'unsafe-inline' 'unsafe-eval'; style-src 'self' 'unsafe-inline'; img-src 'self' data: https:; font-src 'self' data:; connect-src 'self'; frame-ancestors 'self';" always;
                                        EOF
                                        
                                        # Setup fail2ban for WordPress
                                        pct exec $TEMPLATE_VMID -- cat > /etc/fail2ban/jail.d/wordpress.conf << 'EOF'
                                        [wordpress-auth]
                                        enabled = true
                                        filter = wordpress-auth
                                        port = http,https
                                        logpath = /var/log/nginx/*access.log
                                        maxretry = 5
                                        bantime = 3600
                                        findtime = 600
                                        
                                        [nginx-limit-req]
                                        enabled = true
                                        filter = nginx-limit-req
                                        port = http,https
                                        logpath = /var/log/nginx/*error.log
                                        maxretry = 10
                                        bantime = 3600
                                        findtime = 600
                                        EOF
                                        
                                        pct exec $TEMPLATE_VMID -- cat > /etc/fail2ban/filter.d/wordpress-auth.conf << 'EOF'
                                        [Definition]
                                        failregex = ^<HOST> .* "POST /wp-login.php
                                                   ^<HOST> .* "POST /xmlrpc.php
                                        ignoreregex =
                                        EOF
                                        
                                        # Enable services
                                        pct exec $TEMPLATE_VMID -- systemctl enable nginx php8.1-fpm fail2ban
                                        
                                        # Install WP-CLI
                                        pct exec $TEMPLATE_VMID -- curl -o /usr/local/bin/wp https://raw.githubusercontent.com/wp-cli/wp-cli/v2.8.1/utils/wp-cli.phar
                                        pct exec $TEMPLATE_VMID -- chmod +x /usr/local/bin/wp
                                        
                                        # Clean up
                                        pct exec $TEMPLATE_VMID -- apt-get clean
                                        pct exec $TEMPLATE_VMID -- rm -rf /var/lib/apt/lists/* /tmp/* /var/tmp/*
                                        
                                        # Stop container and convert to template
                                        pct stop $TEMPLATE_VMID
                                        pct template $TEMPLATE_VMID
                                        
                                        echo "Template $TEMPLATE_NAME created successfully with VMID $TEMPLATE_VMID"
                                        
                                        

                                        VM Template Creation (Packer)
                                        {
                                          "builders": [{
                                            "type": "proxmox-iso",
                                            "proxmox_url": "https://pve1.internal:8006/api2/json",
                                            "api_token_id": "packer@pve!packer",
                                            "api_token_secret": "{{user `proxmox_token`}}",
                                            "node": "pve1",
                                            "vm_name": "wordpress-ubuntu-2204-template",
                                            "iso_file": "local:iso/ubuntu-22.04.3-live-server-amd64.iso",
                                            "iso_checksum": "sha256:...",
                                            "disk_size": "20G",
                                            "memory": 4096,
                                            "cores": 2,
                                            "ssh_username": "ubuntu",
                                            "ssh_password": "ubuntu",
                                            "ssh_wait_timeout": "10000s",
                                            "network_adapters": [{
                                              "bridge": "vmbr0",
                                              "model": "virtio",
                                              "tag": "10"
                                            }]
                                          }],
                                          "provisioners": [{
                                            "type": "shell",
                                            "inline": [
                                              "sudo apt-get update",
                                              "sudo apt-get install -y nginx php8.1-fpm php8.1-mysql php8.1-xml php8.1-curl php8.1-gd php8.1-mbstring php8.1-zip php8.1-intl php8.1-bcmath php8.1-imagick composer git unzip curl wget vim htop fail2ban ufw certbot python3-certbot-nginx"
                                            ]
                                          }, {
                                            "type": "shell",
                                            "scripts": [
                                              "scripts/configure-php.sh",
                                              "scripts/configure-nginx.sh",
                                              "scripts/harden-system.sh"
                                            ]
                                          }],
                                          "post-processors": [{
                                            "type": "proxmox-template",
                                            "vm_id": 9001
                                          }]
                                        }
                                        
                                        

                                        Template Usage
                                        # Create new tenant from LXC template
                                        pct clone 9000 101 --hostname tenant1 --storage local-zfs --start 1
                                        
                                        # Create new tenant from VM template
                                        qm clone 9001 201 --name tenant1 --full --storage local-zfs
                                        qm start 201
                                        
                                        

                                        7. Reverse Proxy & SSL

                                        Reverse Proxy Options
                                        Feature Nginx Proxy Manager Traefik Caddy
                                        UI Management Excellent Basic None
                                        Docker/LXC Labels Limited Native Limited
                                        Auto SSL Yes Yes Yes
                                        Learning Curve Low Medium Low
                                        Best For Beginners Containers Simplicity

                                        Recommendation: Traefik for dynamic container orchestration integration.

                                        Traefik Configuration

                                        Static Configuration (/etc/traefik/traefik.yml)
                                        entryPoints:
                                          web:
                                            address: ":80"
                                            http:
                                              redirections:
                                                entryPoint:
                                                  to: websecure
                                                  scheme: https
                                          websecure:
                                            address: ":443"
                                        
                                        providers:
                                          file:
                                            directory: /etc/traefik/dynamic
                                            watch: true
                                          docker:
                                            endpoint: "unix:///var/run/docker.sock"
                                            exposedByDefault: false
                                        
                                        certificatesResolvers:
                                          letsencrypt:
                                            acme:
                                              email: admin@homelab.local
                                              storage: /etc/traefik/acme.json
                                              httpChallenge:
                                                entryPoint: web
                                        
                                        api:
                                          dashboard: true
                                          insecure: true
                                        
                                        log:
                                          level: INFO
                                          format: json
                                        
                                        accessLog:
                                          format: json
                                        
                                        

                                        Dynamic Tenant Configuration (/etc/traefik/dynamic/tenant1.yml)
                                        http:
                                          routers:
                                            tenant1:
                                              rule: "Host(`tenant1.homelab.internal`)"
                                              entryPoints:
                                                - websecure
                                              service: tenant1
                                              tls:
                                                certResolver: letsencrypt
                                              middlewares:
                                                - security-headers
                                                - rate-limit
                                          
                                          services:
                                            tenant1:
                                              loadBalancer:
                                                servers:
                                                  - url: "http://10.0.10.101"
                                                passHostHeader: true
                                          
                                          middlewares:
                                            security-headers:
                                              headers:
                                                customRequestHeaders:
                                                  X-Forwarded-Proto: "https"
                                                customResponseHeaders:
                                                  X-Frame-Options: "SAMEORIGIN"
                                                  X-XSS-Protection: "1; mode=block"
                                                  X-Content-Type-Options: "nosniff"
                                                  Referrer-Policy: "strict-origin-when-cross-origin"
                                                  Content-Security-Policy: "default-src 'self'; script-src 'self' 'unsafe-inline' 'unsafe-eval'; style-src 'self' 'unsafe-inline'; img-src 'self' data: https:; font-src 'self' data:; connect-src 'self'; frame-ancestors 'self';"
                                            
                                            rate-limit:
                                              rateLimit:
                                                average: 100
                                                burst: 50
                                        
                                        

                                        SSL/TLS Implementation
                                        • Let's Encrypt: Wildcard certificates for *.homelab.internal
                                        • Individual Certs: For external domains (tenant1.com, tenant2.com)
                                        • DNS-01 Challenge: For wildcard certs (requires Cloudflare/Route53 API)
                                        • Auto Renewal: Traefik handles automatic renewal
                                        • HSTS: Enabled via middleware
                                        • OCSP Stapling: Configured in Traefik

                                          8. Database Strategy

                                          Database Options
                                          Approach Isolation Resource Usage Maintenance Best For
                                          Individual DB per tenant High Higher Distributed Security-critical
                                          Centralized DB with schemas Medium Lower Centralized Cost optimization
                                          Managed DB Service High N/A None No-ops preference

                                          Recommendation: Centralized MariaDB with per-tenant databases/schemas.

                                          Database Host Setup
                                          # Create dedicated LXC for MariaDB
                                          pct create 100 local:vz/ubuntu-22.04-standard_22.04-1_amd64.tar.gz \
                                            --hostname mariadb \
                                            --storage local-zfs \
                                            --cores 4 \
                                            --memory 4096 \
                                            --swap 1024 \
                                            --rootfs local-zfs:50 \
                                            --net0 name=eth0,bridge=vmbr0,ip=10.0.10.100/24,gw=10.0.10.1 \
                                            --start 1
                                          
                                          # Install MariaDB
                                          pct exec 100 -- apt-get update
                                          pct exec 100 -- apt-get install -y mariadb-server
                                          
                                          # Configure for multi-tenant
                                          pct exec 100 -- cat > /etc/mysql/mariadb.conf.d/99-multi-tenant.cnf << 'EOF'
                                          [mysqld]
                                          innodb_buffer_pool_size = 2G
                                          innodb_log_file_size = 256M
                                          innodb_flush_log_at_trx_commit = 2
                                          innodb_flush_method = O_DIRECT
                                          max_connections = 500
                                          query_cache_type = ON
                                          query_cache_size = 64M
                                          thread_cache_size = 50
                                          table_open_cache = 2000
                                          slow_query_log = 1
                                          slow_query_log_file = /var/log/mysql/slow.log
                                          long_query_time = 2
                                          log_queries_not_using_indexes = 1
                                          EOF
                                          
                                          # Create databases per tenant
                                          pct exec 100 -- mysql -e "
                                          CREATE DATABASE wp_tenant1 CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;
                                          CREATE USER 'wp_tenant1'@'%' IDENTIFIED BY 'secure_password';
                                          GRANT ALL PRIVILEGES ON wp_tenant1.* TO 'wp_tenant1'@'%';
                                          FLUSH PRIVILEGES;
                                          "
                                          
                                          

                                          Database Security
                                          • Firewall: Only allow connections from tenant network (10.0.10.0/24)
                                          • SSL: Require SSL connections
                                          • Backups: Automated daily + hourly binlog
                                          • Monitoring: Prometheus mysqld_exporter

                                            9. Media Storage

                                            Storage Options
                                            Option Pros Cons
                                            Local (ZFS) Simple, fast, included in backups Limited by disk, no CDN
                                            MinIO (S3) Scalable, CDN-ready, redundant Additional complexity
                                            Cloud (S3/R2) Managed, global CDN Cost, external dependency

                                            Recommendation: Start with local ZFS, migrate to MinIO when needed.

                                            MinIO Setup (Optional)
                                            # Deploy MinIO as LXC
                                            pct create 102 local:vz/ubuntu-22.04-standard_22.04-1_amd64.tar.gz \
                                              --hostname minio \
                                              --storage local-zfs \
                                              --cores 2 \
                                              --memory 4096 \
                                              --rootfs local-zfs:100 \
                                              --net0 name=eth0,bridge=vmbr0,ip=10.0.20.10/24 \
                                              --start 1
                                            
                                            # Install MinIO
                                            pct exec 102 -- curl -o /usr/local/bin/minio https://dl.min.io/server/minio/release/linux-amd64/minio
                                            pct exec 102 -- chmod +x /usr/local/bin/minio
                                            pct exec 102 -- useradd -r minio-user -s /bin/false
                                            pct exec 102 -- mkdir -p /data/minio
                                            pct exec 102 -- chown minio-user:minio-user /data/minio
                                            
                                            # Systemd service
                                            pct exec 102 -- cat > /etc/systemd/system/minio.service << 'EOF'
                                            [Unit]
                                            Description=MinIO
                                            After=network-online.target
                                            
                                            [Service]
                                            User=minio-user
                                            Group=minio-user
                                            EnvironmentFile=/etc/default/minio
                                            ExecStart=/usr/local/bin/minio server $MINIO_OPTS $MINIO_VOLUMES
                                            Restart=always
                                            
                                            [Install]
                                            WantedBy=multi-user.target
                                            EOF
                                            
                                            # Configuration
                                            pct exec 102 -- cat > /etc/default/minio << 'EOF'
                                            MINIO_ROOT_USER=admin
                                            MINIO_ROOT_PASSWORD=secure_password
                                            MINIO_VOLUMES="/data/minio"
                                            MINIO_OPTS="--console-address :9001"
                                            EOF
                                            
                                            

                                            WordPress Integration
                                            # Install WP Offload Media plugin via WP-CLI
                                            wp plugin install amazon-s3-and-cloudfront --activate --path=/srv/wordpress/tenant1/web
                                            
                                            # Configure via constants in wp-config.php
                                            define('AS3CF_SETTINGS', serialize([
                                                'provider' => 'minio',
                                                'endpoint' => 'http://minio.internal:9000',
                                                'region' => 'us-east-1',
                                                'bucket' => 'tenant1-media',
                                                'key' => 'access_key',
                                                'secret' => 'secret_key',
                                                'use_path_style_endpoint' => true,
                                            ]));
                                            
                                            

                                            10. CI/CD & Automation

                                            GitHub Actions Workflow
                                            # .github/workflows/deploy-wordpress.yml
                                            name: Deploy WordPress Tenant
                                            
                                            on:
                                              push:
                                                branches: [main]
                                                paths:
                                                  - 'tenants/**'
                                              workflow_dispatch:
                                                inputs:
                                                  tenant:
                                                    description: 'Tenant slug to deploy'
                                                    required: true
                                                    type: choice
                                                    options:
                                                      - tenant1
                                                      - tenant2
                                                      - tenant3
                                            
                                            jobs:
                                              build:
                                                runs-on: ubuntu-latest
                                                outputs:
                                                  artifact: ${{ steps.upload.outputs.artifact }}
                                                steps:
                                                  - uses: actions/checkout@v4
                                                  
                                                  - name: Setup Node
                                                    uses: actions/setup-node@v4
                                                    with:
                                                      node-version: '20'
                                                      cache: 'npm'
                                                  
                                                  - name: Install dependencies
                                                    working-directory: tenants/${{ github.event.inputs.tenant || 'tenant1' }}
                                                    run: npm ci
                                                  
                                                  - name: Build assets (Sage 10+)
                                                    working-directory: tenants/${{ github.event.inputs.tenant || 'tenant1' }}
                                                    run: npm run build
                                                  
                                                  - name: Create deployment package
                                                    working-directory: tenants/${{ github.event.inputs.tenant || 'tenant1' }}
                                                    run: |
                                                      mkdir -p ../deploy
                                                      rsync -av --exclude node_modules --exclude .git --exclude '*.log' \
                                                        . ../deploy/${{ github.event.inputs.tenant || 'tenant1' }}/
                                                      cd ../deploy
                                                      tar -czf ${{ github.event.inputs.tenant || 'tenant1' }}.tar.gz ${{ github.event.inputs.tenant || 'tenant1' }}/
                                                  
                                                  - name: Upload artifact
                                                    id: upload
                                                    uses: actions/upload-artifact@v4
                                                    with:
                                                      name: deployment-${{ github.event.inputs.tenant || 'tenant1' }}
                                                      path: tenants/deploy/*.tar.gz
                                            
                                              deploy:
                                                needs: build
                                                runs-on: ubuntu-latest
                                                if: github.ref == 'refs/heads/main' || github.event_name == 'workflow_dispatch'
                                                steps:
                                                  - name: Download artifact
                                                    uses: actions/download-artifact@v4
                                                    with:
                                                      name: deployment-${{ github.event.inputs.tenant || 'tenant1' }}
                                                      path: ./deploy
                                                  
                                                  - name: Deploy to Proxmox
                                                    env:
                                                      SSH_KEY: ${{ secrets.PROXMOX_SSH_KEY }}
                                                      PROXMOX_HOST: ${{ secrets.PROXMOX_HOST }}
                                                      TENANT: ${{ github.event.inputs.tenant || 'tenant1' }}
                                                    run: |
                                                      echo "$SSH_KEY" > key.pem
                                                      chmod 600 key.pem
                                                      
                                                      # Extract and deploy
                                                      tar -xzf deploy/*.tar.gz -C deploy/
                                                      
                                                      # Copy to Proxmox container
                                                      scp -i key.pem -r deploy/${{ github.event.inputs.tenant || 'tenant1' }}/* \
                                                        root@$PROXMOX_HOST:/srv/wordpress/${{ github.event.inputs.tenant || 'tenant1' }}/
                                                      
                                                      # Run post-deploy
                                                      ssh -i key.pem root@$PROXMOX_HOST "
                                                        cd /srv/wordpress/${{ github.event.inputs.tenant || 'tenant1' }}
                                                        composer install --no-dev --optimize-autoloader
                                                        wp cache flush --path=/srv/wordpress/${{ github.event.inputs.tenant || 'tenant1' }}/web
                                                        systemctl reload nginx php8.1-fpm
                                                      "
                                            
                                            

                                            11. Monitoring & Logging

                                            Stack Components
                                            Component Purpose Port
                                            Prometheus Metrics collection 9090
                                            Grafana Dashboards 3000
                                            Node Exporter Host metrics 9100
                                            MySQL Exporter DB metrics 9104
                                            PHP-FPM Exporter App metrics 9253
                                            Blackbox Exporter HTTP probes 9115
                                            Loki Log aggregation 3100
                                            Promtail Log shipping 9080

                                            Prometheus Config (/etc/prometheus/prometheus.yml)
                                            global:
                                              scrape_interval: 15s
                                            
                                            scrape_configs:
                                              - job_name: 'proxmox'
                                                static_configs:
                                                  - targets: ['pve1.internal:9221']
                                              
                                              - job_name: 'node'
                                                static_configs:
                                                  - targets: 
                                                    - 'pve1.internal:9100'
                                                    - '10.0.10.101:9100'
                                                    - '10.0.10.102:9100'
                                              
                                              - job_name: 'mysql'
                                                static_configs:
                                                  - targets: ['10.0.10.100:9104']
                                              
                                              - job_name: 'php-fpm'
                                                static_configs:
                                                  - targets: 
                                                    - '10.0.10.101:9253'
                                                    - '10.0.10.102:9253'
                                              
                                              - job_name: 'blackbox'
                                                metrics_path: /probe
                                                params:
                                                  module: [http_2xx]
                                                static_configs:
                                                  - targets:
                                                    - https://tenant1.homelab.internal
                                                    - https://tenant2.homelab.internal
                                                relabel_configs:
                                                  - source_labels: [__address__]
                                                    target_label: __param_target
                                                  - source_labels: [__param_target]
                                                    target_label: instance
                                                  - target_label: __address__
                                                    replacement: 10.0.30.10:9115
                                            
                                            

                                            Key Dashboards
                                            • Host Overview: CPU, RAM, Disk, Network per Proxmox node
                                            • Container Overview: Per-tenant resource usage
                                            • WordPress Health: Requests/sec, Response time, Error rate, PHP memory
                                            • Database Performance: Queries/sec, Slow queries, Connections, Buffer pool
                                            • Business Metrics: Page views, Load time (LCP), Conversion funnel

                                              12. Backup & Disaster Recovery

                                              Backup Strategy
                                              Layer Frequency Retention Method
                                              Proxmox VM/CT Daily 14 daily, 4 weekly, 3 monthly vzdump to NFS
                                              Application Files Every 6 hours 7 days rsync to MinIO
                                              Database Hourly binlog, Daily dump 30 days mysqldump + xtrabackup
                                              Configuration Hourly 90 days Git repo + etckeeper

                                              Disaster Recovery Plan

                                              RTO: < 4 hours | RPO: < 1 hour

                                              Recovery Procedures
                                              1. Detection: Alert from monitoring (Prometheus/Alertmanager)
                                              2. Assessment: Determine scope (single tenant vs. multi-tenant vs. host)
                                              3. Failover:
                                                • Single tenant: Restore from latest backup to new container
                                                • Host failure: Spin up on DR node, restore from vzdump
                                                • Validation: Smoke tests, DB integrity check, SSL verification
                                                • DNS Cutover: Update internal DNS records
                                                • Post-Mortem: Document timeline, root cause, improvements

                                                  Automated Restore Test
                                                  #!/bin/bash
                                                  # test-restore.sh - Run monthly via cron
                                                  
                                                  TENANT=$1
                                                  BACKUP_DATE=$(date -d "yesterday" +%Y%m%d)
                                                  RESTORE_VMID=9999
                                                  
                                                  # Restore to test container
                                                  pct restore $RESTORE_VMID /mnt/backup/vzdump/lxc/${TENANT}_${BACKUP_DATE}.tar.zst \
                                                    --storage local-zfs --hostname test-${TENANT} --start 1
                                                  
                                                  # Validate
                                                  sleep 30
                                                  TEST_IP=$(pct exec $RESTORE_VMID -- hostname -I | awk '{print $1}')
                                                  curl -f "http://$TEST_IP" || exit 1
                                                  pct exec $RESTORE_VMID -- wp core verify-checksums --path=/srv/wordpress/$TENANT/web
                                                  
                                                  # Cleanup
                                                  pct stop $RESTORE_VMID
                                                  pct destroy $RESTORE_VMID
                                                  echo "Restore test passed for $TENANT"
                                                  
                                                  

                                                  13. Operations Handbook

                                                  Daily Operations
                                                  • Check Grafana dashboards for anomalies
                                                  • Review backup job status in Proxmox
                                                  • Check fail2ban logs for blocked IPs
                                                  • Verify SSL certificate expiration (>30 days)
                                                  • Review resource utilization trends

                                                    Weekly Tasks
                                                    • Apply Proxmox host updates (apt update && apt dist-upgrade)
                                                    • Update container templates with latest security patches
                                                    • Run mysqlcheck --optimize on all tenant databases
                                                    • Check ZFS pool health (zpool status)
                                                    • Test one tenant restore procedure

                                                      Monthly Tasks
                                                      • Full disaster recovery drill
                                                      • Run vulnerability scan (OpenVAS/Lynis)
                                                      • Review and rotate credentials
                                                      • Update WordPress core/plugins/themes
                                                      • Capacity planning review

                                                        Quarterly Tasks
                                                        • Hardware health check (SMART, memtest)
                                                        • Network infrastructure audit
                                                        • Review firewall rules and access controls
                                                        • Penetration testing (internal)
                                                        • Team runbook review and training

                                                          Incident Response Playbooks
                                                          Scenario Detection Response Recovery
                                                          Site Down Blackbox probe fail Check container, nginx, php-fpm, DB Restart services, check logs
                                                          Slow Performance High response time alert Check CPU/RAM/DB slow queries Scale resources, optimize queries
                                                          Security Breach Fail2ban/suricata alert Isolate container, preserve logs Restore from clean backup
                                                          Disk Full Disk usage > 85% Clean logs, expand ZFS, move data Add storage, adjust retention

                                                          Appendix A: Architecture Diagram
                                                          graph TD
                                                              subgraph Internet
                                                                  User[Web Users]
                                                              end
                                                              
                                                              subgraph DMZ[DMZ Network - VLAN 30]
                                                                  Proxy[Traefik Reverse Proxy]
                                                                  CertBot[Certbot/LE Manager]
                                                              end
                                                              
                                                              subgraph Proxmox[Proxmox VE Host]
                                                                  subgraph TenantNet[Tenant Network - VLAN 10]
                                                                      DB[MariaDB LXC :3306]
                                                                      WP1[Tenant1 WordPress LXC]
                                                                      WP2[Tenant2 WordPress LXC]
                                                                      WP3[TenantN WordPress LXC]
                                                                  end
                                                                  
                                                                  subgraph Storage[Storage Network - VLAN 20]
                                                                      ZFS[ZFS Storage Pool]
                                                                      MinIO[MinIO Object Storage]
                                                                  end
                                                                  
                                                                  subgraph Mgmt[Management Network - VLAN 0]
                                                                      PVE[Proxmox Interface]
                                                                      DNS[Pi-hole DNS]
                                                                      Mon[Prometheus/Grafana/Loki]
                                                                      CI[GitHub Actions Runner]
                                                                  end
                                                              end
                                                              
                                                              User -->|HTTPS| Proxy
                                                              Proxy -->|HTTP| WP1 & WP2 & WP3
                                                              WP1 & WP2 & WP3 -->|SQL| DB
                                                              WP1 & WP2 & WP3 -->|Files| ZFS
                                                              WP1 & WP2 & WP3 -.->|Media| MinIO
                                                              PVE -.->|API| CI
                                                              Mon -.->|Scrape| DB & WP1 & WP2 & WP3 & PVE
                                                              DNS -.->|Resolve| Proxy & WP1 & WP2 & WP3
                                                              Proxy -.->|ACME| CertBot
                                                              
                                                              classDef dmz fill:#ff9900,stroke:#333;
                                                              classDef tenant fill:#99ccff,stroke:#333;
                                                              classDef storage fill:#99ff99,stroke:#333;
                                                              classDef mgmt fill:#ffcc99,stroke:#333;
                                                              
                                                              class Proxy,CertBot dmz;
                                                              class WP1,WP2,WP3,DB tenant;
                                                              class ZFS,MinIO storage;
                                                              class PVE,DNS,Mon,CI mgmt;
                                                          
                                                          

                                                          Appendix B: Security Hardening Checklist

                                                          Proxmox Host
                                                          • [ ] Enable Proxmox firewall with default deny
                                                          • [ ] Restrict management network to VPN/VPN-only access
                                                          • [ ] Change default SSH port (22 → 2222)
                                                          • [ ] Enable TOTP 2FA for web GUI
                                                          • [ ] Configure CAPTCHA for GUI login
                                                          • [ ] Set up fail2ban for SSH brute force
                                                          • [ ] Enable auditd for system auditing
                                                          • [ ] Schedule automatic security updates

                                                            Container/VM Hardening
                                                            • [ ] Disable root SSH login (PermitRootLogin no)
                                                            • [ ] Enforce SSH key authentication only
                                                            • [ ] Install fail2ban with WordPress filters
                                                            • [ ] Configure UFW: deny incoming, allow tenant net
                                                            • [ ] Disable password authentication for all users
                                                            • [ ] Enable unattended-upgrades for security patches
                                                            • [ ] Configure logrotate for all services
                                                            • [ ] Restrict PHP: disable_functions = exec,passthru,shell_exec,system,proc_open,popen

                                                              WordPress Security
                                                              • [ ] Enable auto-updates for core (minor only)
                                                              • [ ] Schedule weekly plugin/theme updates
                                                              • [ ] Deploy WAF (Wordfence/Sucuri/ModSecurity)
                                                              • [ ] Limit login attempts (5 per hour per IP)
                                                              • [ ] Enforce 2FA for all admin users
                                                              • [ ] Secure wp-config.php (chmod 600, move outside web root)
                                                              • [ ] Disable file editing: define('DISALLOW_FILE_EDIT', true)
                                                              • [ ] Disable XML-RPC if not needed
                                                              • [ ] Implement Content Security Policy headers
                                                              • [ ] Regular malware scans via WP-CLI

                                                                Appendix C: Key Configuration Files

                                                                /etc/hosts (Internal DNS entries)
                                                                10.0.0.10     pve1.homelab.internal
                                                                10.0.10.100   mariadb.homelab.internal
                                                                10.0.10.101   tenant1.homelab.internal
                                                                10.0.10.102   tenant2.homelab.internal
                                                                10.0.20.10    minio.homelab.internal
                                                                10.0.30.10    traefik.homelab.internal
                                                                10.0.30.11    monitor.homelab.internal
                                                                
                                                                

                                                                .env Template (Bedrock)
                                                                DB_NAME={{ tenant_slug }}
                                                                DB_USER={{ tenant_slug }}_user
                                                                DB_PASSWORD={{ generated_password }}
                                                                DB_HOST=mariadb.homelab.internal
                                                                
                                                                WP_ENV=production
                                                                WP_HOME=https://{{ tenant_domain }}
                                                                WP_SITEURL=https://{{ tenant_domain }}/wp
                                                                
                                                                # Redis for object cache
                                                                WP_REDIS_HOST=redis.homelab.internal
                                                                WP_REDIS_DATABASE=0
                                                                
                                                                # Security keys (auto-generated)
                                                                AUTH_KEY={{ random_64 }}
                                                                SECURE_AUTH_KEY={{ random_64 }}
                                                                LOGGED_IN_KEY={{ random_64 }}
                                                                NONCE_KEY={{ random_64 }}
                                                                AUTH_SALT={{ random_64 }}
                                                                SECURE_AUTH_SALT={{ random_64 }}
                                                                LOGGED_IN_SALT={{ random_64 }}
                                                                NONCE_SALT={{ random_64 }}
                                                                
                                                                

                                                                Playbook Version: 1.0.0
                                                                Last Updated: 2026-08-03
                                                                Maintained by: Homelab Platform Team

                                                                Share:š• Twitterf Facebook