A CloudLinux Alternative: Per-Account Limits with cgroup v2 on Ubuntu 24.04
On a shared hosting server it takes a single misbehaving site (a runaway plugin, a bot crawling everything, a hacked site sending spam) to slow down everyone else. CloudLinux fixes this by giving every account its own CPU, memory, process and disk limits, but it costs around $18 a month per server and needs AlmaLinux or a derivative. On Ubuntu 24.04 you can get the same limits with tools the system already has: systemd and cgroup v2. This guide shows how to do it by hand for one account, what's missing compared with CloudLinux, and how Koapanel does it with its Isolation & limits module, included in every plan.
What CloudLinux does, briefly
CloudLinux adds a set of features built for shared hosting:
- LVE: per-account limits for CPU, memory, processes, disk speed (I/O and IOPS), number of files and entry processes, i.e. how many of the account's PHP requests can run at the same time;
- CageFS: every account sees a file system of its own, without the other customers and without sensitive system files;
- MySQL Governor: slows down, inside the database, the account that overdoes it;
- PHP Selector and selectors for Ruby, Python and Node;
- diagnostic tools such as X-Ray for slow sites.
On a server hosting many customers these are useful. But today the Linux kernel does most of this work by itself.
cgroup v2: the mechanism that's already there
cgroups (control groups) are the kernel feature that groups processes and limits their resources. Ubuntu 24.04 uses version 2, and systemd exposes it simply: every service runs in its own cgroup, and slices group several services under a shared limit.
The properties that matter for hosting:
| systemd property | What it limits | CloudLinux equivalent |
|---|---|---|
CPUQuota=100% |
CPU time (100% = one core) | SPEED |
MemoryMax=1G |
memory | PMEM |
TasksMax=200 |
processes and threads | NPROC |
IOReadBandwidthMax, IOWriteBandwidthMax |
MB/s on a disk | IO |
IOReadIOPSMax, IOWriteIOPSMax |
operations per second | IOPS |
Entry processes are missing from the list, but PHP has a simple equivalent: the maximum number of processes of the account's PHP-FPM pool (pm.max_children).
The shared PHP-FPM problem
On a regular Ubuntu server every PHP-FPM pool runs under one service, php8.3-fpm.service. Even if each site has its own Linux user, the PHP processes of all customers sit in the same cgroup: limiting that service would limit everyone together.
The fix is to give every account a PHP-FPM of its own, as a separate service inside its slice.
Limits for one account, by hand
Take an account customer1, with its sites in /var/www/customer1 and the Linux user customer1.
1. The slice with the limits
# /etc/systemd/system/customer1.slice
[Unit]
Description=Hosting account customer1
[Slice]
CPUQuota=100%
MemoryMax=1G
TasksMax=200
IOReadBandwidthMax=/dev/vda 50M
IOWriteBandwidthMax=/dev/vda 50M
The device (/dev/vda in the example) is the disk holding the sites: check it with df /var/www.
2. A dedicated PHP-FPM pool
; /etc/php/8.3/fpm/customer1.conf
[global]
pid = /run/php/php-fpm-customer1.pid
error_log = /var/log/php-fpm-customer1.log
[customer1]
user = customer1
group = customer1
listen = /run/php/php-fpm-customer1.sock
listen.owner = www-data
listen.group = www-data
pm = ondemand
pm.max_children = 10
pm.process_idle_timeout = 30s
php_admin_value[open_basedir] = /var/www/customer1:/tmp
pm.max_children = 10 acts as entry processes: at most ten PHP pages of the account at the same time, the rest wait in the queue instead of grabbing CPU and memory.
3. The service, inside the slice and isolated
# /etc/systemd/system/php-fpm-customer1.service
[Unit]
Description=PHP-FPM for customer1
After=network.target
[Service]
Type=notify
Slice=customer1.slice
ExecStart=/usr/sbin/php-fpm8.3 --nodaemonize --fpm-config /etc/php/8.3/fpm/customer1.conf
ExecReload=/bin/kill -USR2 $MAINPID
# isolation, the heart of CageFS:
ProtectSystem=strict
ProtectHome=yes
PrivateTmp=yes
ReadWritePaths=/var/www/customer1 /run/php /var/log
InaccessiblePaths=-/var/www/customer2 -/var/www/customer3
[Install]
WantedBy=multi-user.target
ProtectSystem=strict makes the system read-only, PrivateTmp=yes gives the account its own /tmp, InaccessiblePaths hides the other customers' folders.
4. Enable and check
sudo systemctl daemon-reload
sudo systemctl enable --now php-fpm-customer1.service
systemctl status customer1.slice
sudo systemd-cgtop
Then point fastcgi_pass in the nginx configuration of customer1's sites to the new socket unix:/run/php/php-fpm-customer1.sock and reload nginx. systemd-cgtop shows the CPU, memory and I/O of every slice live: if one of the account's sites overdoes it, only customer1.slice slows down.
To change a limit without restarting: sudo systemctl set-property customer1.slice CPUQuota=200%.
What's left out
The hand-made version works, but on a real server several pieces are missing:
- cron and WP-CLI run outside the slice, so outside the limits, unless you start them with
systemd-run --slice=customer1.slice; - each site's Redis cache, if any, has to go into the account's slice;
- the database: cgroups can't see how much MariaDB works for each account. You can cap connections per user (
MAX_USER_CONNECTIONS), but not CPU like MySQL Governor; - graphs: knowing who hit the limits, when and how often means reading the cgroup counters (
memory.events,cpu.stat) and keeping them; - maintenance: one service and one slice per account, to create, update and delete along with the accounts, for every PHP version.
With ten accounts it's manageable; with a hundred you need a tool.
How Koapanel does it
Koapanel has the Isolation & limits module, optional and included in every plan, which does all of the above for every account, with the sites online:
- per-account limits for CPU, memory, processes, I/O, IOPS, concurrent PHP requests, files and database connections, in the package, on the single account or as server defaults; also an overall cap per reseller;
- isolated PHP like CageFS: every account sees only its own sites, the system read-only and a private
/tmp; cron, WP-CLI and the Redis cache count against the limits too; - an optional database limiter, like MySQL Governor;
- X-Ray for slow sites: for every PHP page that is too slow it names the guilty plugin, theme or file;
- an hourly mail limit per account, against compromised accounts sending spam;
- graphs over 24 hours and 30 days, email alerts when an account hits its limits, a ranking and a report every Monday;
- limit import from cPanel on CloudLinux with the SSH migration;
- in the WHMCS module the limits can be sold as configurable options.
It doesn't modify the kernel and has no Ruby, Python or Node selectors: it covers PHP and WordPress, i.e. almost all shared hosting. Install and uninstall it in one click; uninstalled, the server goes back as it was. The point-by-point comparison with CloudLinux and the savings are on the CloudLinux alternative page; details are in the manual, section Isolation & limits.
FAQ
Does cgroup v2 slow the server down?
Not measurably: the kernel accounts every process's resources anyway. The real cost is the memory of the separate PHP-FPMs, about 10 MB per account; with PHP on demand only recently visited accounts use it.
Does it work on Debian?
The mechanisms (systemd and cgroup v2) do, with Debian 12 or later. Koapanel itself runs on Ubuntu 24.04 LTS only.
Can I move from CloudLinux to Koapanel and keep the limits?
Yes: the SSH migration from a cPanel server with CloudLinux reads the LVE limits of every account, and an administrator imports them in one click. See migrate from cPanel.
Does the memory limit take the site down?
When an account reaches its limit, the PHP process that overdoes it is stopped and that page returns an error; other pages and other accounts carry on. That's why realistic limits and a look at the graphs in the first days are worth it.
Try Koapanel
Look around the public demo or install it on a fresh Ubuntu 24.04, free for up to 3 sites:
curl -fsSL https://get.koapanel.app | sudo bash
The module installs from Server › Isolation & limits, or right away with the installer: curl -fsSL https://get.koapanel.app | sudo KOAPANEL_ISOLATION=1 bash.