# How PHP Runs

> How CorePanel executes PHP: one FPM pool per account running as that account's own user, a PHP version chosen per account, which files are treated as PHP, and how the command line resolves.

Source: https://www.corepanel.net/docs/php/how-php-runs/
Last updated: 2026-09-02
Part of the CorePanel documentation — https://www.corepanel.net/docs

---

Every hosting account on a CorePanel server runs PHP in **its own PHP-FPM pool, as its
own system user**. There is no shared web user and no shared interpreter: two accounts on
the same server cannot read each other's files even when both are running the same PHP
version, because the process doing the reading *is* the account.

This page describes that arrangement. For what PHP is allowed to touch once it is
running, see [PHP Isolation and Limits](https://www.corepanel.net/docs/php/isolation-and-limits); for adding
ionCube or any other PHP extension, see
[PHP Extensions and ionCube](https://www.corepanel.net/docs/php/extensions).

## One pool per account

When an account is created, CorePanel writes a single pool file:

```
/etc/opt/remi/php84/php-fpm.d/<account>.conf
```

The pool runs as `<account>:<account>` and listens on a Unix socket at
`/run/php84-php-fpm/<account>.sock`. The web server — [corehttpd](https://www.corepanel.net/docs/web)
— connects to that socket and speaks FastCGI to it. It never executes PHP itself.

That distinction is the whole isolation model:

| | Runs as | Can read |
|---|---|---|
| corehttpd (the web server) | `corehttpd` | Any file, to serve it — but it only ever *streams* bytes |
| The PHP worker | `<account>` | Only what that account owns, and only inside `open_basedir` |

Account files are `0700`/`0600` — owner only. A PHP script therefore has exactly the
reach of the account that owns it, which is what makes shared hosting on CorePanel safe
by construction rather than by configuration.

The pool is **on demand**: no PHP process exists for an idle account. The first request
starts a worker, and it exits again after 10 seconds of inactivity. A server with 200
mostly-idle accounts is not running 200 PHP masters — it is running one master per PHP
version, with workers appearing only where there is traffic.

## The PHP version is per account

Each account runs one PHP version, chosen when the account is created and changeable
afterwards. The versions available are the ones installed on the server — see
[Adding a PHP version](#adding-a-php-version) if the one you need is not there.

> **The version can be changed later**
>
> An account is not stuck on the version it was created with. **Accounts → the account →
> Edit account → PHP version** moves it: the pool is rewritten on the new version, every
> site of the account is re-pointed at it in a single reload, and the old pool is retired
> once nothing uses it. The sites keep serving throughout — see
> [Changing the PHP version](https://www.corepanel.net/docs/accounts/editing#changing-the-php-version) for what the
> switch does and does not cover.
The version is still per **account**, not per domain: an account's aliases, addon domains
and subdomains all run in the one pool, and moving the account moves all of them together.

This is also why an operator installing an extension has to think in versions rather than
in accounts: `php84-php-soap` does nothing for a customer running PHP 8.3 — and an account
moved to 8.4 needs 8.4's extensions installed before it gets there.

## How long each version is supported

PHP branches go out of support on a schedule, and the panel says where each one stands.
Next to every version — in the account creation form, in **Edit account → PHP version**,
and on the dashboard — you will see one of three states:

| Badge | What it means |
|---|---|
| **Supported** | Active support: bug fixes and security fixes. |
| **Security fixes only** | The branch is past active support. Security patches only, with an end date. |
| **End of life** | Unsupported by PHP. No security fixes upstream. |

"End of life" means **end of life upstream**. Remi — the repository CorePanel installs PHP
from — keeps publishing builds of dead branches and backports the occasional patch, but
the PHP project does not, and no packager covers that gap in full. PHP 7.4 has been out of
support since November 2022 and ships on every CorePanel server, because a migrated
account may still need it.

The panel **warns, it does not block**. An account imported from cPanel may genuinely
require 7.4, and you know your customers better than the panel does. What it will not do
is let the decision be made without saying so.

> **The dates come from the server, not the panel**
>
> The support windows travel with the `corepanel-php` package, so `dnf update` refreshes
> them. A server that has not been updated in a while shows versions with **no badge** at
> all rather than a stale one — an absent badge means "this server has not been told",
> never "this version is fine".
### Accounts on unsupported PHP

The dashboard carries the figure that matters operationally: **how many accounts are
running a version that no longer receives security fixes**. Each version links straight to
the accounts on it, so the number turns into a work list rather than an audit you have to
do account by account.

The same card reports a second, louder case: an account pinned to a version **that is not
installed on this server**. That happens when a version is removed while accounts are
still on it. Those sites are not running an old PHP — they are not running PHP at all.

The card is not shown when there is nothing to report, and it is not shown to your
customers: an account holder cannot change their own PHP version, so the notice would be
a worry they cannot act on.

## Adding a PHP version

The versions on a server are not fixed. **Server → PHP versions** lists what is installed,
how many accounts run each, and what else this server can install. Pick one, confirm, and
watch it happen. The screen is **super-administrator only**: this installs software as
root and restarts a service every account on the machine shares, so it is not a reseller's
decision.

![The PHP screen mid-install: a card headed "Installing PHP 8.1" with the phase "Configuring", a progress bar near its end, a note saying CorePanel is now replacing the version's shared pool with its own and that this step prints nothing for a few seconds, and below it a black console showing the package manager's last lines followed by two CorePanel lines marked with an arrow. Underneath, an "Available to install" list with PHP 8.5 badged Supported and PHP 7.4 badged End of life, each with an Install button](https://www.corepanel.net/_astro/php-manager-install.Bu7wK2OH.svg)

Installing a version does not move anything onto it. Accounts change one at a time, so
adding 8.1 for one migrated customer costs the other accounts nothing — you assign it
afterwards in **Accounts → the account → Edit account → PHP version**.

The list of versions on offer is fixed in the server, not read from the repository: it is
what CorePanel is prepared to configure and keep configured, which is a narrower question
than what a repository happens to carry. Every version on it comes with its support dates,
so a branch that is out of support is visible **before** it is chosen — the only moment
that warning is free to act on. An end-of-life version can still be installed, with a
confirmation, because 7.4 is what most cPanel migrations arrive on.

### Watching an install

An install runs a package transaction and takes minutes, so it runs **in the background**
and the panel follows it:

- **The output is the package manager's own**, verbatim. Lines beginning `==>` are
  CorePanel's; everything else is `dnf` speaking for itself. It is collapsed while things
  are going well and **opens by itself if the install fails**, because that is the moment
  it is the only thing that matters. There is a button to copy the whole log for a ticket.
- **You can close the tab.** The install is not tied to your browser: it runs on the
  server and finishes whether or not anybody is watching. Come back and the version is
  either there or the reason it is not is in the server log.
- **There is a quiet stretch near the end**, and the panel says so. Once the packages are
  down, CorePanel replaces the version's shared pool with its own and starts the service;
  on a server with SELinux that step prints nothing for several seconds. It has not hung.
- **One at a time.** The package database takes a lock, so a second install is refused
  rather than left to fail halfway — and the refusal names the version already running,
  including one started from the command line.

An install that fails leaves the error on screen in full. That is deliberate: the point of
installing from CorePanel is not having to open an SSH session to find out what went
wrong.

### From the command line

The same install, without a browser:

```
corepanel php versions
corepanel php install 8.1
corepanel account update acme --php 8.1
```

`corepanel php install` waits for the transaction and prints the output when it ends,
rather than streaming it — it is the same install underneath, so it takes the one slot too.

## Versions installed outside CorePanel

A PHP installed by hand — `dnf install php81-php-fpm` and its friends — arrives with the
distribution's stock `www` pool, which:

- runs as the shared `apache` user rather than as an account,
- starts idle workers whether or not anything uses them, and
- has **neither `open_basedir` nor `disable_functions`** — the one PHP on the host outside
  the isolation model described above.

**CorePanel repairs that by itself.** It checks on every start and once an hour, and any
version still carrying the stock pool is brought under the same per-account pools as every
other version. There is nothing to press and nothing to remember; a version installed at
an SSH prompt is inside the isolation model within the hour.

On a multi-tenant server this is not treated as a preference. A PHP running as a shared
user without `open_basedir` is a hole in the boundary between accounts, and it is closed
whether or not anyone asks.

> **If you were using the stock pool for something**
>
> The `www` pool is replaced, so anything still connecting to `/run/php<version>-fpm.sock`
> stops working — an internal tool, a script, a second web server of your own. Every
> CorePanel site is served by its account's own pool and is unaffected.
>
> If that happens, the replacement is recorded in the server log with the version it
> affected, so the cause is findable rather than mysterious. Point the tool at its own
> php-fpm pool, or at the account pool of the account it belongs to.
A version still marked **Not configured** in the panel therefore means something different
now: not "installed by hand", but "CorePanel tried to configure this and could not". The
server log says why. Treat the badge as a fault to investigate, not a to-do.

## Which files run as PHP

corehttpd sends a file to PHP-FPM based on its **final** extension:

```
.php   .php5   .php7   .php8   .phtml
```

Anything else is served as a static file.

The list is wider than just `.php` for a specific reason. Every cPanel-generated
`.htaccess` declares its handler like this:

```apache
AddHandler application/x-httpd-ea-php81 .php .php8 .phtml
```

So on a cPanel server those files *are* PHP. An account
[imported from cPanel](https://www.corepanel.net/docs/cpanel-import) carries them, and serving one as a static
file would publish its source code — database credentials included. CorePanel executes
what the origin server executed.

Three details are worth knowing:

- **Only the last extension counts.** `backup.php.bak` is a `.bak` file and is served
  statically, not executed. Apache does the opposite — it assigns a handler from *any*
  extension a file carries, and will happily run `backup.php.bak`. This is a deliberate
  divergence, and the safer of the two.
- **`.phps` is not executed.** On Apache it means "render this file as highlighted
  source", so executing it would invert what the extension means.
- **Matching ignores case**, because Apache's does.

### Why the list is not narrower

A site that never had anything but `.php` gets the wider list too, and that is a real
trade rather than a free win. If an application accepts uploads and filters them by
extension, a filter that blocks `.php` and overlooks the rest is a well-known mistake —
and on a wider list its consequence is worse.

The default is what it is because the alternative is worse in a way that is already
happening: an imported site whose `.phtml` files are served as static text publishes its
own source, database credentials included, to anyone who asks for the URL. That is a live
condition on real accounts, not a scenario.

The mitigations that matter are the ordinary ones, and they do not depend on this list:
keep the application patched, and do not let untrusted uploads land inside a document
root. A per-site override exists in the site configuration, but it is not exposed in the
panel today — if you need one, ask support rather than editing the file by hand, because
the web server and the account's pool have to be changed together. If they disagree, PHP
answers *Access denied* instead of serving the page.

## PHP on the command line

There is **no global `php` binary** on a CorePanel host, because there is no single PHP
version for the host to have. Instead, `/usr/local/bin/php` is a small shim (`cpphp`)
that resolves the *invoking account's* version and execs the right Remi binary:

```bash
php -v          # as an account: that account's version
```

That is what makes a cPanel-style cron line work verbatim after a migration:

```
0 3 * * * /usr/local/bin/php /home/example/public_html/cron.php
```

The shim reads a flat, root-written map file rather than asking a daemon, so
[cron jobs](https://www.corepanel.net/docs/accounts/cron-jobs) keep running even when the panel's services are
down. It also points the CLI at the account's private `~/tmp`, exactly like the pool
does — so a script behaves the same whether the web server or cron invoked it.

To run a specific version explicitly, call it by path:

```bash
/opt/remi/php83/root/usr/bin/php -v
```

## Logs

Each account gets its own pair, outside `/home` so a full account quota cannot stop
logging:

```
/var/log/php-fpm/users/<account>/php-access.log
/var/log/php-fpm/users/<account>/php-error.log
```

`php-error.log` is where a PHP fatal error lands, and it is the first place to look when
a page returns a blank 500. Both are rotated by logrotate.

## What CorePanel writes, and what is yours

CorePanel owns exactly one file per account per PHP version — the pool config above. It
does **not** manage `php.ini` and it does not manage the version's `php.d` directory.

That boundary is deliberate: anything you drop into `php.d` is yours and survives panel
updates. What CorePanel will rewrite is the pool file, including on upgrade — the panel
normalizes the settings it owns in every existing account's pool when the service starts,
so a hardening change reaches accounts that already exist and not only new ones. Edits
made by hand inside a CorePanel pool file can be overwritten; put them in `php.d`, or in
a [`.user.ini`](https://www.corepanel.net/docs/php/user-ini) for a single
site.
