FastCGI page caching can let Nginx answer an anonymous request without running PHP. Verify both cache hits and exclusions before relying on it: private or personalized responses must not be served from a shared anonymous cache.
Scope: We tested an existing WordPress FastCGI cache in aaPanel Free 8.0.7 with Nginx and PHP 8.5. This guide verifies the existing setup; it does not claim we installed a new cache or fully audited every authenticated workflow.
1. Identify the cache layer
Page caching stores a rendered HTTP response. OPcache stores compiled PHP bytecode; Redis object caching stores application objects. They solve different problems. A page-cache HIT may avoid PHP, so it cannot be used to measure PHP or SQL tuning alone.
Inspect the website's Nginx configuration and included PHP handler. Our WordPress site included enable-php-85-wpfastcgi.conf and used a WORDPRESS FastCGI cache zone.
2. Read the rules before testing
Our existing rules bypassed POST requests, nonempty query strings, WordPress administrative/login-related paths and cookies matching WordPress login or password-protected content. These rules must be checked against the actual applications on the site.
The handler used both:
The first controls whether an existing cached response is used; the second controls whether the upstream response is stored. A correct private-response policy needs both aspects considered. Do not copy an incomplete location block or override application Set-Cookie behavior blindly.
3. Inspect cache headers
Our configuration exposed Nginx-Cache with the upstream cache status. Your site may use another header or expose none.
Code:
curl -sS -D - -o /dev/null https://example.com/
Replace the hostname. Read the HTTP status and cache header. HIT means a stored response was served. MISS means the cache did not supply an entry; an eligible response may subsequently be stored. BYPASS means the rules skipped reading the cache. Absence of a header does not prove caching is disabled.
4. Repeat an anonymous request
We sent five sequential requests to the same lab homepage over loopback HTTPS with certificate validation. All five returned HTTP 200 and HIT; median time to first byte was approximately 58ms.
The page was already cached. We did not claim to demonstrate a fresh MISS-to-HIT transition, clear the whole cache or measure maximum throughput. Five requests are a small diagnostic sample.
The query-string request, synthetic login-cookie request and login page each returned HTTP 200 and BYPASS in our lab. The synthetic cookie contains no real login token; it tests the configured cookie-name rule only. It does not authenticate a user or prove actual account privacy end to end.
AdminsForum formatted transcript of the original checks, not a screenshot.
6. Test actual personalized workflows before rollout
With a controlled test account, verify that logged-in pages, shopping carts, checkout, account settings and password-protected content are excluded. Test two separate sessions and inspect rendered content, not just a fast response. Review authorization, cookies, cache keys and application-specific paths.
Our short check did not perform these authenticated workflows. Treat them as required verification before enabling shared page caching for a real personalized application.
7. Verify freshness and controlled purging
After updating a public test page, confirm when anonymous visitors receive the new content. Purge the relevant cache entry using your supported administration workflow and verify the result. Do not expose an unrestricted public purge endpoint.
Our existing configuration restricted its purge location to loopback. We did not weaken that restriction or perform a global purge for this guide. Cache lifetime and invalidation should match how frequently your pages change.
8. Troubleshoot a persistent BYPASS or stale page
Check whether a query string, cookie, request method or path matches an exclusion. Inspect the effective Nginx configuration and logs. If a public page bypasses unexpectedly, identify the cause rather than deleting all exclusions.
For stale content, check the correct hostname, cache key, purge method and any additional CDN or browser cache. A browser refresh alone may leave server caches untouched.
Back up before changing rules, validate Nginx syntax, reload and repeat both anonymous and private-response tests. Restore the original rules if personalized content is cached or application behavior changes.
A controlled load test helps compare one workload at several concurrency levels. It does not prove how many visitors an entire website can support. This guide records a bounded external-client test and explains the measurements still needed for a production decision.
Environment: two-CPU shared Ubuntu VPS, aaPanel Free 8.0.7, Nginx, WordPress and PHP 8.5. The tested FPM pool had a maximum of eight children and OPcache enabled. Existing FastCGI cache rules bypassed query-string requests. The generator ran on a separate Windows machine.
1. Choose a safe target and scope
Use a lab server you control. Choose a harmless GET endpoint and a finite request count. Exclude actions that create accounts, orders, mail or database writes. Agree a maintenance window for production testing and monitor other services sharing the machine.
Our target was the WordPress homepage with a unique test query on each request. We verified the query rule returned Nginx-Cache: BYPASS before the run. This measures an uncached homepage path, not checkout, search, login or an entire browsing session.
2. Record the baseline and configuration
Write down PHP version, FPM worker limits, cache states, database version and application version/plugins. Keep those constant between phases. Record server memory, swap and CPU before testing; arrange an interval resource sampler if you need to locate saturation.
This run did not collect a simultaneous server resource trace. We therefore report response results without claiming whether PHP, CPU or SQL limited throughput.
3. Set explicit limits and stop conditions
Our client used 40 requests per phase at concurrency 2, then 4, then 8. Each curl request had a 15-second maximum time, used normal HTTPS certificate validation and performed no retries. Later phases would stop if any request failed or the sample p95 time to first byte exceeded five seconds.
A short request count bounds the total work but can miss sustained bottlenecks. Avoid unbounded loops and high concurrency simply to generate a dramatic result. Watch availability and stop manually if important services degrade.
4. Understand the client measurements
A single request example is:
This is a Windows example: NUL discards the body. Use /dev/null on Linux. Replace the hostname and make each test query unique if your verified rules use query strings to bypass caching. Do not disable certificate checks.
Time to first byte includes client-side connection and network effects as well as server waiting. Total time includes body transfer. An HTTP 200 alone does not prove the content is correct; separately inspect a sample response for application errors.
We used a bounded client worker pool to run the finite requests concurrently, waited for each phase to complete, then calculated its results. These are closed-loop tests: workers submit another request after the previous one finishes, rather than maintaining a fixed independent arrival rate.
5. Read the actual results AdminsForum formatted transcript of the original client test, not a screenshot.
Concurrency 2: 40/40 HTTP 200; median TTFB 295ms; sample p95 334ms; phase elapsed 8.46 seconds; about 4.73 completed requests/second.
Concurrency 4: 40/40 HTTP 200; median TTFB 334ms; sample p95 431ms; phase elapsed 4.91 seconds; about 8.15 completed requests/second.
Concurrency 8: 40/40 HTTP 200; median TTFB 377ms; sample p95 609ms; phase elapsed 2.88 seconds; about 13.87 completed requests/second.
All 120 requests completed with HTTP 200 and no client request failure. Concurrency increased the observed completion rate and latency. The phases were short, were not repeated in randomized order, and did not isolate cache-warmth or resource effects. Do not interpret the numbers as a guaranteed sustained rate or maximum visitors.
The sample p95 is a nearest-rank estimate from forty observations, not a precise long-term tail metric. Network variation and shared-server activity can affect it.
6. Add resource evidence before tuning
For a repeat test, sample CPU utilization, iowait, free memory, swap activity, FPM queues and active database work throughout the phases. Private FPM status and slow logs can help locate queueing. Keep diagnostic endpoints private and redact logs before sharing.
If latency rises with CPU saturation, more FPM workers may worsen contention. If queries dominate, inspect SQL plans. If the client itself is saturated, the generator may be the limit. These conclusions need measurements; this run does not establish them.
7. Repeat a representative workload
Use the same test after one targeted change. Include realistic page mixes, authenticated workflows under controlled accounts, writes in a disposable environment and sustained runs appropriate to your service. Measure errors and content correctness alongside latency.
Test anonymous cache HIT and uncached behavior separately. A fast cached homepage cannot prove that private or dynamic pages perform well. Never increase a pool limit solely because a cached benchmark looks good.
8. Preserve results and restore experimental changes
Keep raw request outcomes, timing definitions and configuration snapshots. Remove only your own disposable fixtures when no longer needed. Retain rollback copies for settings changed during earlier tests. This load run itself changed no server configuration or website content.
Use the baseline guide, the measured PHP-FPM pool guide and the database index test as a repeatable series. Present the workload and its limits whenever quoting performance numbers.
Nginx tuning should start with measurements, not a list of larger limits. This guide checks worker settings and gzip behavior, demonstrates a real compression tradeoff, and explains when to restore a change.
Tested: aaPanel Free 8.0.7, Nginx 1.30.5 and Ubuntu 22.04.5 on the shared two-CPU lab VPS. No production configuration was changed.
1. Inspect the active configuration
Open App Store → Installed → Nginx → Setting and inspect its configuration. Paths and interface labels can differ by release. Our global file was /www/server/nginx/conf/nginx.conf; website configurations were included separately.
A configuration dump can help identify includes:
Code:
/www/server/nginx/sbin/nginx -T
Keep the output private because it can reveal hostnames, paths and configuration details. Identify which file owns the directive before editing it.
2. Review workers and connection limits
Our server already used worker_processes auto and worker_connections 51200. We did not increase either. More permitted connections do not mean more requests per second: upstream connections, open-file limits, PHP workers, database work and CPU also constrain throughput.
Inspect actual worker count, active connections, system file limits and errors during representative traffic. Raise a limit only if it is the observed bottleneck and the server has resources to support it. Avoid treating a high worker_connections value as a visitor-capacity guarantee.
Keepalive settings also need workload evidence. Shorter idle time can reduce held connections but increase connection setup; longer idle time can retain resources. We left the existing 60-second setting unchanged.
3. Back up before a compression experiment
Make a private copy of the active file. Our baseline already had gzip enabled, a 1KiB minimum length, compression level 5 and gzip_vary on. Its gzip_types included text, JavaScript, CSS, JSON and several image types.
Inspect response headers and actual transfer sizes before changing MIME types. Many image formats contain compressed data, but the effect of an additional gzip layer depends on the particular file.
4. Test text and image responses
We created an original 24,500-byte text fixture in the lab website and used an existing 3,113-byte WordPress PNG. We requested gzip explicitly over loopback HTTPS with normal certificate validation.
A basic header check is:
Use your own hostname and a safe test file. Inspect Content-Encoding, Vary and HTTP status. To compare raw encoded bytes, save the response body without automatic decompression. curl --compressed instead requests supported compression and decompresses the body, so do not confuse decoded size with wire size.
Our test compared the baseline against removing JPEG/GIF/PNG from gzip_types. It did not change the compression level or worker limits.
AdminsForum formatted transcript of the experiment, not a screenshot.
5. Interpret the evidence
Text: gzip was used in both phases; the body transferred was 156 bytes and decoded to 24,500 bytes.
PNG before: gzip body 2,487 bytes, decoded to 3,113 bytes.
PNG after: no gzip; body transferred 3,113 bytes.
All responses returned HTTP 200, and decoded content hashes matched.
Removing image compression increased transfer size for this particular small PNG. We did not measure CPU savings, so we did not claim the change improved performance. These body sizes exclude headers and transport overhead; one file does not represent an entire image library.
Validation passed for the experiment. Because we had no demonstrated overall benefit, we restored the original configuration, validated it again and reloaded Nginx. A successful reload is not an application test: check important pages, TLS, redirects, upstream errors and logs afterwards.
Do not keep an optimization solely because it looks conventional. Record the tradeoff and test enough representative content to support a decision.
7. Apply this method to a real high-traffic site
Collect response latency, transfer sizes, CPU, connection counts and upstream timing over comparable windows. Separate cached and uncached requests. Change one variable at a time and repeat the same workload. Keep a rollback copy and stop if errors or resource pressure increase.
A compression change cannot repair slow SQL or exhausted PHP workers. Use the database and PHP-FPM guides alongside this test, then investigate the bottleneck actually observed.
A slow website does not automatically need a larger database buffer or more connections. First identify the query, inspect its execution plan and measure a targeted change. This guide demonstrates that workflow in a separate MariaDB lab database.
Tested environment: aaPanel Free 8.0.7, Ubuntu 22.04.5 and MariaDB 10.11.10. MySQL and MariaDB releases differ; verify your installed version before copying configuration advice. We used 50,000 synthetic rows and changed no global database settings.
1. Identify the database and establish a baseline
Open aaPanel → Databases and confirm which service and database the website uses. Inspect application logs and slow queries rather than assuming the homepage represents all workloads. Administrative diagnostics require suitable database permissions; keep credentials out of commands, screenshots and public tutorials.
Code:
SELECT VERSION();
SHOW VARIABLES WHERE Variable_name IN
('innodb_buffer_pool_size','max_connections','slow_query_log','long_query_time');
SHOW GLOBAL STATUS WHERE Variable_name IN
('Threads_connected','Max_used_connections','Innodb_buffer_pool_reads','Innodb_buffer_pool_read_requests');
Our lab had a 384MiB InnoDB buffer pool, max_connections 200, slow logging enabled and a 3-second threshold. Maximum used connections was 3 at that observation. These are observations, not recommended settings for every server.
Counters may accumulate since startup. Take interval snapshots under representative traffic; a one-time cache ratio is not enough to justify allocating more RAM.
2. Find the actual slow query
If slow logging is enabled, review its configured path through an administrator session. Logs can contain query text and user data: keep access restricted and do not upload raw logs to a public thread. A 3-second threshold will miss many shorter queries; choose a diagnostic threshold deliberately and monitor storage overhead.
Find frequently repeated queries as well as individually expensive ones. Record execution count, latency and examined rows. A fast query executed thousands of times may still consume significant resources.
3. Create a separate practice database
Use a disposable database, never a production table, for this example. Our new database was af_performance_index_lab_20261014. In MariaDB 10.11, the sequence table below provides synthetic integers; this syntax is not a portable MySQL recipe.
Code:
CREATE DATABASE af_performance_index_lab_20261014;
USE af_performance_index_lab_20261014;
CREATE TABLE events (
id INT PRIMARY KEY,
account_id INT NOT NULL,
status VARCHAR(16) NOT NULL,
payload VARCHAR(100) NOT NULL
) ENGINE=InnoDB;
INSERT INTO events
SELECT seq, MOD(seq,1000),
IF(MOD(seq,2)=0,'active','closed'), REPEAT('x',100)
FROM seq_1_to_50000;
Check that your chosen database name does not already exist. This example creates only synthetic data; adapt names and versions for your own isolated lab.
4. Inspect the query plan before changing anything
Code:
EXPLAIN SELECT SQL_NO_CACHE COUNT(*)
FROM events WHERE account_id=321 AND status='closed';
SQL_NO_CACHE is used here to exclude MariaDB's query-result cache; it does not empty the InnoDB buffer pool. Do not copy this clause into an incompatible database release.
Our plan showed type ALL and an estimated 50,000 rows. EXPLAIN estimates are not measured execution counts, but this plan indicates a full table scan to evaluate the filters.
The query returned 50 matching rows. Record the result so that a faster but incorrect rewrite is not accepted.
5. Test a targeted composite index
Code:
ALTER TABLE events
ADD INDEX idx_account_status(account_id,status);
ANALYZE TABLE events;
EXPLAIN SELECT SQL_NO_CACHE COUNT(*)
FROM events WHERE account_id=321 AND status='closed';
The new plan used idx_account_status, type ref and an estimated 50 rows. It also reported Using index: this count query can be satisfied from the index entries needed for the filters.
Index design depends on the actual predicates, sorting and joins. Column order matters for other queries using only part of a composite index. Avoid creating an index for every column: indexes use storage and add work to writes.
On a large production table, building an index can consume time, space and locks. Check version-specific online DDL behavior, back up and plan the change before applying it to real data.
6. Compare the same query
We used MariaDB session profiling for this lab: one warmup followed by ten measured executions in each phase. The same database session setting and query were used before and after. Warm data remained possible in the buffer pool; this is not a cold-storage benchmark.
Code:
SET profiling=1;
SELECT SQL_NO_CACHE COUNT(*)
FROM events WHERE account_id=321 AND status='closed';
SHOW PROFILES;
Repeat the SELECT for your sample and inspect its reported duration. Session profiling is version-specific; use the diagnostic tools available in your actual database release. Disable temporary profiling when finished.
AdminsForum formatted transcript of the original lab test, not a terminal screenshot.
The median of ten measured executions was about 12.72ms before and 0.188ms after. Both phases returned 50 rows. This supports this index for this synthetic query; it does not promise the same improvement for every website or establish production capacity.
7. Tune resources only after reviewing evidence
For InnoDB buffer sizing, account for the working data set and sustained physical reads, while reserving memory for PHP-FPM, Nginx, OS, mail and other services. Dedicated database-server percentage rules are unsuitable for every shared aaPanel VPS.
For max_connections, inspect concurrent connections, application pooling and per-connection memory. Raising the limit can worsen overload. Our observed maximum of 3 did not justify increasing the limit of 200.
Change one setting at a time, retain its original value, validate the database configuration and repeat representative workloads. Monitor swap, OOM events, errors and query latency. A restart can temporarily alter cache warmth and distort comparisons.
8. Verify the website and retain a rollback plan
Test real reads and writes, not just this SELECT. Compare execution plans and results, then monitor normal traffic. For our disposable table, the index can be removed with:
Code:
ALTER TABLE events DROP INDEX idx_account_status;
Do not remove an existing production index without checking its other uses. We retained this isolated lab database and its index for repeatable demonstrations; no application database was modified.
Continue with PHP-FPM memory limits to balance the shared server, rather than tuning each service as though it owns all RAM.
OPcache stores compiled PHP bytecode so PHP-FPM can reuse it instead of compiling the same scripts for every request. This guide measures that effect on a small WordPress lab and explains how to verify the serving runtime.
Test environment
aaPanel Free 8.0.7, Ubuntu 22.04.5, PHP 8.5.11, Nginx and WordPress. OPcache was already enabled before the experiment. We temporarily disabled it for the comparison and left it enabled afterwards. No production website was used.
1. Confirm the PHP version serving the website
Open Website → your site and inspect its PHP version. Our site used PHP 8.5 through /tmp/php-cgi-85.sock. A command-line PHP result alone cannot prove what PHP-FPM is doing: CLI and FPM may load different configuration files.
Read Check PHP OPcache: CLI vs PHP-FPM first if those results disagree.
2. Back up the active configuration
In App Store → Installed → PHP-8.5 → Setting, inspect the configuration and available extensions. Interface labels depend on the panel release. Confirm the active FPM php.ini before editing; our FPM runtime loaded /www/server/php/85/etc/php.ini, while CLI loaded php-cli.ini.
Keep a backup and working SSH access. A setting change affects the websites sharing that PHP version. Schedule a suitable maintenance window for a busy server.
3. Use a measured starting configuration
Our retained lab settings were:
The memory value is MiB. The file setting controls the script-cache table size; the actual key capacity may differ from the number configured. Check the runtime rather than assuming they are identical.
Timestamp validation allows changed files to be detected; the revalidation interval controls how frequently checks occur. Disabling validation requires a reliable deployment invalidation process. We kept it enabled and did not change JIT settings.
Do not increase these values merely because traffic increased. Look for cache exhaustion, restarts and the application’s script count, while reserving RAM for other services.
4. Validate and reload the correct pool
Our SSH checks were:
Validation passed before each reload. Syntax success does not prove the application works: check important pages and error logs afterwards. Restore the backup and reload if the change causes failures.
5. Verify OPcache through PHP-FPM privately
Our diagnostic script used opcache_get_status(false) and php_ini_loaded_file() through the private Unix socket. It was outside the website root. Passing false excludes the detailed cached-script path listing.
The enabled phase reported FPM SAPI fpm-fcgi, PHP 8.5.11, 532 cached scripts, about 29MiB used and 99MiB free, with zero out-of-memory and hash restarts at that observation. These values describe this small workload, not the needs of every WordPress installation.
Do not publish phpinfo or cache status on an unprotected URL: configuration and paths can disclose server details. Remove temporary diagnostics when finished.
6. Separate page caching from bytecode caching
A full-page cache HIT may avoid PHP entirely. We used unique query strings and verified Nginx-Cache: BYPASS under our existing rules. Query strings do not universally bypass caches; check your own configuration and response headers.
Each phase used 20 warmup requests followed by 20 measured requests at concurrency 2. Requests used loopback HTTPS with normal certificate validation. This isolates much of the network variation but does not represent a remote visitor’s full browser experience.
AdminsForum formatted transcript of the original experiment, not a terminal screenshot.
7. Interpret the results
OPcache off: 20/20 HTTP 200; median time to first byte 490ms; small-sample p95 555ms.
OPcache on: 20/20 HTTP 200; median time to first byte 168ms; small-sample p95 190ms.
This workload was faster with OPcache enabled. Twenty requests give limited tail-latency evidence and do not establish maximum visitor capacity. Repeat tests on expensive pages and representative workloads before making production promises.
Runtime hit/miss counters can include warmup and other requests since the cache started; do not treat a cumulative hit ratio as the measured phase alone.
8. Monitor and revisit sizing
Inspect memory used/free/wasted, cached-script count, misses and restart counters over a representative period. An exhausted cache may need additional memory or key capacity; frequent deployments and invalidation also affect results. Check PHP errors and memory pressure alongside cache statistics.
OPcache does not replace SQL indexes, object caching or page caching. Use measured PHP-FPM worker limits to control concurrent process memory separately.
PHP-FPM worker limits should fit the memory and workload of the PHP applications on your server. Measure warmed workers, reserve resources for the database and other services, then compare a controlled workload before increasing concurrency.
Tested: aaPanel Free 8.0.7, Ubuntu 22.04.5, two CPUs and 3.8Gi RAM. WordPress uses the PHP 8.5 FPM socket /tmp/php-cgi-85.sock. We backed up the pool configuration and tested a smaller dynamic pool. This is a lab sizing example, not a universal high-traffic configuration.
1. Establish a baseline and confirm the right PHP version
Start with Measure Website Performance Before Tuning. In aaPanel, confirm the website's selected PHP version. The default command-line php can be a different version from the PHP-FPM pool serving the site.
Our actual Nginx handler used PHP 8.5 through a Unix socket. We left its connection address and permissions unchanged. Review every site sharing the PHP version before changing a global pool: the settings can affect more than one website.
2. Warm the workload and separate cache hits from PHP work
A cached anonymous response may never reach PHP. In our lab, the existing Nginx FastCGI rules bypass cache for requests with a query string. We verified the response header Nginx-Cache: BYPASS for the measured WordPress requests.
We sent 20 warmup requests, then measured 20 requests at concurrency 2 in each phase. Requests used the same lab hostname over loopback HTTPS with normal certificate validation. This removes most external network variation but does not simulate remote users or a full browser.
Do not assume adding a query string bypasses every cache. Inspect your own rules and headers. Test representative pages, including the expensive workflows your users actually perform.
Identify the master for the correct PHP version and measure its children. RSS includes shared pages; adding every RSS value can double-count shared memory. On Linux, /proc/PID/smaps_rollup includes proportional set size (PSS), which distributes shared pages between processes. Reading these files may require administrator privileges.
After the cache-bypassed warmup, our sampled workers had a maximum RSS around 51–53MiB. Other pages, plugins and traffic can use more. These are sampled values, not guaranteed per-worker maximums. Reserve extra headroom rather than basing the entire pool on the smallest idle worker.
4. Choose a memory budget before choosing a worker count
Reserve memory for the OS, Nginx, database, Redis, mail and any other PHP versions. Estimate a ceiling:
This is an upper-bound planning estimate, not a throughput recommendation. Shared memory, request spikes and application behavior still need measurement. CPU capacity, database limits and acceptable queue time can require a smaller count.
For this shared 3.8Gi lab we used a conservative 1GiB PHP planning budget and a 100MiB per-worker allowance, larger than this light sample. That suggests roughly ten workers before additional headroom; we tested a limit of eight. Do not copy eight simply because your server also has 4GB RAM.
5. Open the native aaPanel worker settings
Choose App Store → Installed → PHP-8.5 → Setting → Optimization. In our release, the form includes Running mode, max_children, start_servers, min_spare_servers and max_spare_servers.
The native PHP Optimization screen shows the tested dynamic-pool values.
Previously the pool used max_children 80, start_servers 10, min_spare_servers 10 and max_spare_servers 30. The form's RAM presets and approximate per-worker memory guidance cannot replace your application's measurements.
What each setting does
pm.max_children: the pool's simultaneous request-worker limit. Requests can queue when all workers are busy.
pm.start_servers: initial children for dynamic mode.
pm.min_spare_servers: desired minimum idle children in dynamic mode.
pm.max_spare_servers: desired maximum idle children in dynamic mode.
static mode: keeps a fixed worker count; plan its persistent memory use.
ondemand mode: spawns workers when needed; startup behavior and idle settings differ from dynamic mode.
Raising workers cannot make slow SQL or external APIs fast, and can overload shared resources. Lowering workers can reduce idle memory but increase queueing under traffic.
6. Back up, validate and reload
We backed up /www/server/php/85/etc/php-fpm.conf before editing it through SSH, then ran:
Configuration validation passed. The native screen was inspected afterwards and reflected the saved values. For a UI change, use its Save action and verify the resulting configuration and service state; our test applied the changes through SSH rather than claiming we clicked Save in the UI.
A configuration test proves syntax, not application behavior. Keep the backup and a working SSH recovery route. Restore it and reload if meaningful requests fail or monitoring shows unacceptable queueing/resource use.
7. Compare the same workload honestly
After 20 warmup requests, each measured phase used 20 requests at concurrency 2:
Before: 20/20 HTTP 200; median TTFB 173 ms; sample p95 189 ms; 12 sampled children; aggregate child PSS about 113MiB.
After: 20/20 HTTP 200; median TTFB 180 ms; sample p95 196 ms; 4 sampled children; aggregate child PSS about 72MiB.
The smaller pool used less aggregate child memory in this snapshot. It did not make this workload faster. The small latency difference can include normal variability, and 20 requests provide limited tail-latency evidence. We did not establish maximum throughput or prove eight workers fits production peak traffic.
8. Monitor queues and real traffic before a production decision
Inspect PHP-FPM status, listen-queue behavior, slow requests, memory pressure, CPU saturation and database activity under a representative bounded load. Keep status endpoints private. Increase a limit only when evidence shows workers are the bottleneck and the server has resources for them.
Do not use PHP memory_limit multiplied by worker count as an exact process-memory measurement. Keep application errors and OOM events in view, and test expensive pages as well as the homepage.
Reference:Official PHP-FPM configuration documentation. AdminsForum performance series. Original pool configuration retained at /www/backup/af-performance-20261014/php85-fpm-before.conf in the lab; do not publish your own private configuration values.
How to install WordPress in CloudPanel (one-click)
CloudPanel has a built-in WordPress installer: it creates the site user, the database and a fully configured WordPress install in one step, with no setup wizard. Here's the whole process on CloudPanel 2.5.4 (Ubuntu 24.04).
You'll need: a domain whose DNS A record points to your server (needed for SSL afterwards).
Step 1: Sites → Add Site → Create a WordPress Site
Open Sites, click + Add Site and choose Create a WordPress Site.
Step 2: Domain and site title
Enter the domain name and a Site Title. The title is just WordPress's site name; you can change it later in WordPress settings.
Step 3: Site user and WordPress admin
Site User: the Linux user that owns this site's files.
Admin User Name: your WordPress login. Don't use "admin"; it's the first name attackers try.
CloudPanel generates strong passwords for both. Click Create.
Step 4: Copy your credentials (shown once!)
CloudPanel shows everything on one page: the site user and password, the database name, user and password, and further down the WordPress admin login. Copy them into a password manager now; they're only shown once.
Step 5: The site is ready
Back in the sites list, the WordPress site has its own site user, separate from your other sites.
Visit the domain: WordPress is already installed and configured.
Step 6: Log in to WordPress
Go to
Code:
https://yourdomain.com/wp-admin
and log in with the admin user and password from Step 4.
Step 7: Add free SSL
Open the site's SSL/TLS tab and issue a Let's Encrypt certificate. See our guide How to install a free Let's Encrypt SSL certificate in CloudPanel.
Recommended tweaks
Upload limits: if you'll upload big themes, plugins or media, raise upload_max_filesize and post_max_size in the site's Settings → PHP Settings.
Real cron: replace WordPress's built-in cron with a real cron job every 5 minutes (covered in our cron, logs and Varnish guide).
Varnish Cache: great for blogs and content sites; CloudPanel's CLP Varnish Cache plugin clears the cache when you publish.
Backups: set up remote backups in the Admin Area before going live.
How to install a free Let's Encrypt SSL certificate in CloudPanel
Every new CloudPanel site starts with a self-signed certificate, so HTTPS works but browsers show a warning. This guide replaces it with a free, trusted Let's Encrypt certificate that CloudPanel renews automatically. Tested on CloudPanel 2.5.4, Ubuntu 24.04.
Before you start
Let's Encrypt checks that you control the domain by connecting to it over port 80. So:
The domain's DNS A record must already point at your server. Check it from your computer:
Code:
dig +short example.com
It should print your server's IP.
Port 80 must be open (it is in CloudPanel's default firewall; also check your cloud provider's firewall).
Using Cloudflare? Set the record to DNS only (grey cloud) while the certificate is issued.
Step 1: Open the site's SSL/TLS tab
Open Sites, click Manage on your site and go to SSL/TLS. You'll see the self-signed certificate.
Step 2: Actions → New Let's Encrypt Certificate
Click Actions, then New Let's Encrypt Certificate. (If you bought a certificate elsewhere, Import Certificate is the option for that.)
Step 3: Confirm the domain names
CloudPanel fills in the site's domain. Add any other names that point to this site, such as the www version, then click Create and Install. If you see "A DNS record pointing to this server is required", fix DNS first.
Step 4: Done
A few seconds later: Certificate has been installed.
The Let's Encrypt certificate is now the installed one. It's valid for 90 days, and CloudPanel renews it automatically before it expires, so there's nothing to remember.
Step 5: Repeat for every site
Do the same for each site you add. Here's our WordPress site with its certificate installed the same way:
If you use Cloudflare, you can now switch the proxy back on (orange cloud) and set SSL/TLS mode to Full (strict).
Command-line alternative
As root, clpctl issues the same certificate:
"A DNS record pointing to this server is required": the A record is missing or points elsewhere, or Cloudflare's proxy is on. Fix DNS, wait for it to update, try again.
Validation fails: port 80 is blocked by your provider's firewall or security group.
www doesn't work: add a DNS record for www too, and include it in the certificate's domain names.
Need help? Ask in CloudPanel Domains, DNS & SSL Support.
This guide walks through adding a PHP site in CloudPanel, from the Add Site button to tuning PHP and Nginx. It's tested on CloudPanel 2.5.4 on Ubuntu 24.04, and every screenshot is from that server.
You'll need: CloudPanel installed and your admin login, plus a domain (or subdomain) you control. Pointing its DNS A record at the server now saves time later when you add SSL.
Step 1: Open Sites → Add Site
Log in to CloudPanel, open Sites and click + Add Site.
Step 2: Choose "Create a PHP Site"
CloudPanel asks what kind of site you want. WordPress has its own one-click option (see our WordPress guide); for any other PHP code choose Create a PHP Site.
Step 3: Pick the application
The Application menu writes the right Nginx configuration for you. Generic works for most PHP code. For frameworks and shops pick the matching template (Laravel, Symfony, Magento, WooCommerce and others) so the rewrite rules are correct from the start.
Step 4: Domain, PHP version and site user
Domain Name: the domain or subdomain for this site.
PHP Version: any version from 7.1 to 8.5. We used 8.5, the newest.
Site User: a real Linux user that will own the files and run the site's PHP processes. Each site gets its own, which keeps sites isolated from each other.
Site User Password: CloudPanel generates a strong one. Save it in your password manager before you click Create.
Step 5: The site is created
A few seconds later the site appears in the list with its site user and app type. Click Manage to open it.
Step 6: Check the root directory
The Settings tab shows the domain and its Root Directory. Frameworks like Laravel serve from a
Code:
public
subfolder, so change it here if your app needs that.
Underneath is the full path where your code goes:
Code:
/home/SITE_USER/htdocs/YOUR_DOMAIN
Upload your files there over SFTP or with the File Manager.
Step 7: Site user password and SSH keys
Further down you can generate a new password for the site user, or paste your public SSH key to log in without a password.
Step 8: Tune PHP for this site
In PHP Settings you can switch the PHP version at any time and set memory_limit, max_execution_time, max_input_time, max_input_vars, post_max_size and upload_max_filesize for this site only, without editing any config files.
Step 9 (optional): The Nginx vhost
The Vhost tab holds the site's full Nginx configuration. CloudPanel fills in the placeholders for certificates, root and logs. You rarely need it, but it's there for custom redirects, headers or rules, and the config is tested before it's saved.
Prefer the command line?
As root, clpctl can create the same site, for example:
CloudPanel system requirements (2026): OS, CPU, RAM, disk and ports
Before you install CloudPanel, check that your server meets these requirements. They're taken from the official docs for CloudPanel 2.5.4 and checked against our own test install on Ubuntu 24.04.
Minimum requirements at a glance
Operating system: Ubuntu 26.04, 24.04 or 22.04 LTS; Debian 13 or 12.
Architecture: x86 (x86_64) or ARM64.
CPU: 1 core or more.
Memory: 2 GB RAM or more.
Disk: 10 GB or more.
Server type: a virtual machine or dedicated server. LXC, LXD and OpenVZ containers are not supported.
Access: root access to a clean, freshly installed server.
Supported operating systems
Ubuntu 26.04 LTS (Resolute Raccoon)
Ubuntu 24.04 LTS (Noble Numbat): the one we use in our guides
Ubuntu 22.04 LTS (Jammy Jellyfish)
Debian 13 (Trixie)
Debian 12 (Bookworm)
Older releases such as Ubuntu 20.04 and Debian 11 aren't on the current list. Other distributions (AlmaLinux, Rocky Linux, CentOS) are not supported.
Here's our test server before installing: Ubuntu 24.04.5 LTS, 4 cores, 7.8 GB RAM and a 73 GB disk.
What gets installed (by OS) Ubuntu 24.04, Ubuntu 22.04 and Debian 12: Nginx 1.30, PHP 7.1 to 8.5, MySQL 8.4 or 8.0 / MariaDB 11.8, 11.4 or 10.11, Redis 7, Varnish 7.5, ProFTPD 1.3, Node.js 12 to 24, Python 3.12. Ubuntu 26.04 and Debian 13: Nginx 1.30, PHP 7.1 to 8.5, MySQL 8.4 / MariaDB 12.3 or 11.8, Redis 8, Varnish 7.7, ProFTPD 1.3, Node.js 12 to 24, Python 3.14.
You pick the database engine when you run the installer, so decide between MySQL and MariaDB before you start.
How much RAM do you really need?
2 GB is the official minimum, and it's a real one: CloudPanel installs a full stack, including a PHP-FPM service for every PHP version, MySQL/MariaDB, Redis and Varnish. On our 8 GB server with two sites, a few hours after install, 1.95 GB was in use, mostly MySQL's buffer pool (~970 MB), which is sized from the server's RAM.
Our practical guidance (not an official figure):
2 GB: fine for one or two small sites or a test server.
4 GB: a comfortable start for several WordPress or PHP sites.
8 GB or more: busy shops, many sites, or heavy Varnish caching.
Disk: 10 GB is enough to install, but leave room for your sites, databases, logs and local backup files; 25 GB or more is more realistic.
Network and ports
CloudPanel's default firewall rules allow these ports. Make sure your cloud provider's firewall / security group allows them too:
22 SSH/SFTP
80 HTTP (also needed for Let's Encrypt validation)
443 HTTPS
8443 the CloudPanel interface
Important: right after installation, anyone who reaches port 8443 can create the admin user. If you can, allow 8443 only from your own IP before you install, and create the admin user immediately.
You'll also want a public IPv4 address and, for real sites, a domain with an A record pointing to the server (needed for Let's Encrypt SSL).
Supported cloud providers
CloudPanel has dedicated install guides for Amazon Web Services, Google Compute Engine, Microsoft Azure, Oracle Cloud, DigitalOcean, Hetzner Cloud, Vultr, Hostinger and Donweb Cloud. Any other VPS or dedicated server that meets the requirements works with the generic installer.
Pre-install checklist
A fresh server with a supported OS, no other web server, database or panel installed.
Root SSH access.
2 GB+ RAM, 10 GB+ disk, x86 or ARM64.
Ports 22, 80, 443 and 8443 open (8443 ideally only for your IP).
Download the installer and verify its SHA-256 checksum. Copy the current command and checksum from the official install docs, as it changes with each release.
This is what a passed checksum looks like on our server:
If the system update installed a new kernel, the installer may show a pending kernel upgrade notice. That's normal; reboot when convenient.
After install, open
Code:
https://YOUR_SERVER_IP:8443
, accept the self-signed certificate and create your admin user.