Setting up an Isolated Multi-Tenant PHP-FPM and Nginx Environment on Ubuntu 24.04 LTS using Systemd Cgroups limits
Prerequisites and Initial Setup
This guide assumes a fresh Ubuntu 24.04 LTS installation with root or sudo privileges. We’ll be setting up a robust, isolated multi-tenant environment for PHP applications served by Nginx and managed by PHP-FPM. The core of our isolation strategy will leverage Systemd’s control group (cgroup) capabilities to enforce resource limits per tenant.
First, ensure your system is up-to-date:
sudo apt update && sudo apt upgrade -y
Install Nginx, PHP-FPM (we’ll use PHP 8.3 as an example), and necessary PHP extensions. For multi-tenancy, we’ll also install php8.3-fpm and potentially other modules required by your applications.
sudo apt install -y nginx php8.3-fpm php8.3-mysql php8.3-mbstring php8.3-xml php8.3-zip
Verify the installations:
systemctl status nginx systemctl status php8.3-fpm
Tenant Isolation Strategy: Systemd Cgroups
Systemd, through its integration with cgroups, provides a powerful mechanism for resource control. We will create separate Systemd service units for each tenant’s PHP-FPM pool. This allows us to define CPU, memory, and I/O limits granularly for each tenant, preventing resource contention and ensuring fair usage.
Our approach involves:
- Creating dedicated PHP-FPM pool configurations for each tenant.
- Defining custom Systemd service units that manage these tenant-specific PHP-FPM pools.
- Configuring Nginx to route requests to the appropriate tenant’s PHP-FPM pool based on domain or subdomain.
Tenant-Specific PHP-FPM Pool Configuration
For each tenant, we’ll create a distinct PHP-FPM pool. This ensures that processes for one tenant do not interfere with another. Let’s assume our first tenant is `tenant1.example.com`.
First, locate the PHP-FPM pool configuration directory. On Ubuntu 24.04, this is typically `/etc/php/8.3/fpm/pool.d/`.
Create a new pool configuration file for `tenant1`:
[tenant1] ; User and group for the pool user = tenant1 group = tenant1 ; Listen on a Unix socket for better performance and security listen = /run/php/php8.3-fpm-tenant1.sock ; Set process manager settings pm = dynamic pm.max_children = 50 pm.start_servers = 5 pm.min_spare_servers = 2 pm.max_spare_servers = 10 pm.process_idle_timeout = 10s pm.max_requests = 500 ; Resource limits (example values, adjust as needed) ; These are hints for PHP-FPM, but Systemd cgroups will enforce them. ; env[HOSTNAME] = $HOSTNAME ; env[UNIQUE_ID] = $UNIQUE_ID ; Error logging error_log = /var/log/php/php8.3-fpm-tenant1.log ; Log level log_level = notice ; Security settings ; php_admin_value[disable_functions] = exec,passthru,shell_exec,system ; php_admin_flag[enable_dl] = off
Next, create the user and group for this tenant. This user will own the process and its associated files, enhancing security.
sudo groupadd tenant1 sudo useradd -g tenant1 -s /sbin/nologin tenant1
Create the log directory and set appropriate permissions:
sudo mkdir -p /var/log/php sudo chown root:root /var/log/php sudo chmod 755 /var/log/php
Restart PHP-FPM to load the new pool configuration. Note that we are not yet using a custom Systemd service, so this restarts the main service.
sudo systemctl restart php8.3-fpm
Custom Systemd Service Units with Cgroup Limits
To isolate tenants and enforce resource limits, we’ll create custom Systemd service files. This allows us to define cgroup parameters directly within the service definition.
Create a Systemd service file for `tenant1`’s PHP-FPM pool. The standard PHP-FPM service is typically named `php8.3-fpm.service`. We’ll create a drop-in configuration or a completely new service. For better management, a new service is often preferred.
Create the service file:
[Unit] Description=PHP-FPM FPM application server for tenant1 After=network.target [Service] ; User and group for the process User=tenant1 Group=tenant1 ; Specify the configuration file for this pool ExecStart=/usr/sbin/php-fpm8.3 --fpm-config /etc/php/8.3/fpm/pool.d/tenant1.conf ; Cgroup v2 limits (Ubuntu 24.04 uses cgroup v2 by default) ; CPU shares: 1024 is default. 512 means 50% of a single core if no other limits are set. CPUWeight=512 ; Memory limit: 256MB. Use 'M' for megabytes, 'G' for gigabytes. MemoryMax=256M ; IO weight (optional, for disk I/O) ; IOWeight=100 ; Restart policy Restart=on-failure RestartSec=5s ; Standard output/error redirection StandardOutput=journal StandardError=journal ; Security hardening ProtectSystem=full PrivateTmp=true NoNewPrivileges=true CapabilityBoundingSet=CAP_SETGID CAP_SETUID CAP_NET_BIND_SERVICE [Install] WantedBy=multi-user.target
Save this file as `/etc/systemd/system/php-fpm-tenant1.service`. Now, enable and start this new service:
sudo systemctl daemon-reload sudo systemctl enable php-fpm-tenant1.service sudo systemctl start php-fpm-tenant1.service
Verify the service status and check its cgroup limits:
systemctl status php-fpm-tenant1.service systemctl show php-fpm-tenant1.service | grep -E 'CPUWeight|MemoryMax'
You should see the configured `CPUWeight` and `MemoryMax` values. Repeat this process for each tenant, creating a unique `.conf` file in `/etc/php/8.3/fpm/pool.d/` and a corresponding `.service` file in `/etc/systemd/system/`.
Nginx Configuration for Multi-Tenant Routing
Nginx needs to be configured to route incoming requests to the correct PHP-FPM pool based on the requested domain or subdomain. We’ll use the `fastcgi_pass` directive to specify the Unix socket for each tenant’s pool.
Create an Nginx server block configuration for `tenant1.example.com`:
server {
listen 80;
server_name tenant1.example.com;
root /var/www/tenant1/public_html; # Adjust to your tenant's web root
index index.php index.html index.htm;
location / {
try_files $uri $uri/ /index.php?$query_string;
}
location ~ \.php$ {
include snippets/fastcgi-php.conf;
# Pass to the specific tenant's PHP-FPM socket
fastcgi_pass unix:/run/php/php8.3-fpm-tenant1.sock;
fastcgi_param SCRIPT_FILENAME $document_root$fastcgi_script_name;
include fastcgi_params;
}
# Deny access to .htaccess files, if Apache's document root
# concurs with nginx's one
location ~ /\.ht {
deny all;
}
access_log /var/log/nginx/tenant1.access.log;
error_log /var/log/nginx/tenant1.error.log;
}
Ensure the web root directory exists and has the correct ownership:
sudo mkdir -p /var/www/tenant1/public_html sudo chown -R tenant1:tenant1 /var/www/tenant1 sudo chmod -R 755 /var/www/tenant1/public_html
Enable the Nginx site and test the configuration:
sudo ln -s /etc/nginx/sites-available/tenant1.conf /etc/nginx/sites-enabled/ sudo nginx -t sudo systemctl reload nginx
For each additional tenant, create a new server block in Nginx, pointing to their respective PHP-FPM socket (e.g., `php8.3-fpm-tenant2.sock`) and web root.
Advanced Considerations and Best Practices
Resource Monitoring: Regularly monitor resource usage per tenant using tools like systemd-cgtop or by examining cgroup filesystem entries under `/sys/fs/cgroup/`. This helps in tuning the `CPUWeight` and `MemoryMax` limits.
sudo systemd-cgtop
PHP-FPM Process Manager: While `dynamic` is common, consider `ondemand` for very low-traffic tenants to save resources, or `static` for high-traffic, predictable workloads. Adjust `pm.max_children` carefully based on server memory and expected load.
Security: Ensure that the `user` and `group` specified in the PHP-FPM pools and Systemd services are unique per tenant and have minimal privileges. Avoid running PHP-FPM pools as `root`. The `NoNewPrivileges=true` and `CapabilityBoundingSet` directives in Systemd are crucial for security.
PHP Configuration Isolation: For more granular control over PHP settings (e.g., `memory_limit`, `upload_max_filesize`) per tenant, you can create tenant-specific `php.ini` files and point to them via `php_admin_value` in the pool configuration, or by using `PHP_INI_SCAN_DIR` environment variables in the Systemd service. However, for strict isolation, managing these via separate PHP-FPM pools is cleaner.
Systemd Cgroup v1 vs v2: Ubuntu 24.04 defaults to cgroup v2. The directives used (`CPUWeight`, `MemoryMax`) are compatible with v2. If you were on an older system using v1, you might use `CPUQuota`, `CPUShares`, `MemoryLimit` respectively.
Scalability: For a large number of tenants, consider automating the creation of PHP-FPM pools, Systemd services, and Nginx configurations using scripting (Bash, Python) or configuration management tools like Ansible or Puppet.
Error Handling: Centralize Nginx and PHP-FPM logs. Use dedicated log files per tenant as shown, and consider a log aggregation system (e.g., ELK stack, Graylog) for easier analysis.
Conclusion
By combining dedicated PHP-FPM pools, custom Systemd service units with cgroup resource controls, and precise Nginx routing, you can establish a highly isolated and resource-managed multi-tenant environment on Ubuntu 24.04 LTS. This architecture provides the necessary separation and control for hosting multiple PHP applications securely and efficiently.
Leave a Reply
You must be logged in to post a comment.