Skip to main content

Deploy PHP Apps

PHP is a popular programming language for web development. PHP is served in production server using PHP-FPM daemon run globally for whole server.

PHP environment setup

The default PHP version is 8.3, which is the default provided from the OS.

To change PHP version used in PHP-FPM to other versions, add this to the deployment system.

features:
- php latest

You can also use a fixed PHP version: php 7.4, php 8.1, php 8.2, php 8.3.

Unfortunately you can't use custom PHP version other than provided because it's tied to system daemon. We always update the list to the latest supported version or latest major version starting from PHP 7.4.

Support for PHP extensions is varies but you can request a ticket to be included, provided the extension is provided officially by PHP.

When PHP version is changed, it's also changing the php and composer version it used.

Alternatively, you can call the alternative php version using php81, php80, php56, etc.

Composer install

Composer is already installed globally and follows what PHP version you've activated.

To call composer with other PHP version, run php81 $(which composer) install.

Installed PHP Extensions

PHP are installed as system wide. It's installed as in these commands:

yum install php-{74,81,82,83}-php-{bcmath,cli,common,devel,fpm,gd,imap,intl,mbstring,mysqlnd,opcache,pdo,pecl-mongodb,pecl-redis,pecl-zip,pgsql,process,sodium,soap,xml}

Here's the list of extensions available:

bcmathbz2calendarCorectypecurl
calendardatedomexiffileinfofilter
gdgettexthashiconvigbinaryimap
intljsonlibxmlmbstringmongodbmsgpack
mysqlimysqlndopensslpcntlpcrePDO
pdo_mysqlpdo_pgsqlPharpdo_sqlitepgsqlposix
randomreadlineredisReflectionsessionshmop
SimpleXMLsoapsocketssodiumSPLsqlite3
standardsysvmsgsysvsemsysvshmtokenizerxml
xmlreaderxmlwriterxslZend OPcachezipzlib

Custom extensions can be added using separate sysdaemon mentioned below.

PHP INI configuration

The PHP INI configuration is useful to tweak the PHP behavior such at upload size limits. While you can't directly change the PHP INI located in system files, you can create .user.ini into PHP root folder (check root in NGINX e.g. ~/public_html/.user.ini or ~/public_html/public/.user.ini) and tweak the config there.

An example of PHP INI configuration is:

~/public_html/.user.ini
upload_max_filesize = 32M
post_max_size = 32M

See the list of available PHP INI configuration in official PHP documentation. To see default values or if your change has been in effect, use phpinfo().

info

PHP INI refresh on 5 minutes. You have to wait for changes to pick up.

You should place in document root, like public_html/public if your system is CI4 / Laravel.

Also, you can't change configs with PHP_INI_SYSTEM level unless you're using method below.

PHP Error Logging

The error logs can be seen in PHP Error Logs in Check -> Check Logs.

You won't see error details in your website because we use production default settings.

If you want to see the error details directly in website, change this setting in .user.ini:

~/public_html/.user.ini
display_errors = On
display_startup_errors = On

If you made changes to these parameters but not see any errors in the website yet, then it's maybe overrided in frameworks settings. For example, Laravel uses APP_DEBUG=true to enable debug mode.

Restarting PHP

PHP doesn't need restart. Changing PHP files instantly changes the running server code.

The PHP-FPM instance itself is running as ondemand and goes inactive after 15 minutes of no traffic.

NGINX Setup

NGINX can be configured to serve PHP files. PHP files are served by the PHP-FPM server. This works by writing fastcgi_pass directive in the NGINX configuration, which points to underlying PHP-FPM server proxy for given host.

The minimum configuration to enable PHP is:

nginx:
fastcgi: on

fastcgi Options

The fastcgi option has three options: on, off, always. The difference between three options:

fastcgi Options off

location ~ \.php$ {
return 404;
fastcgi_pass ....;
}

This essentially disables PHP support in NGINX because it directly return 404 without being forwarded to fastcgi. This is the default value for fastcgi.

fastcgi Options on

location ~ \.php$ {
try_files $uri =404;
fastcgi_pass ......;
}

This detects all URLs that ends with .php and forwards it to the PHP-FPM server. However, if the file is not found, it will return 404 without being forwarded to fastcgi.

fastcgi Options always

location ~ \.php(/|$) {
fastcgi_pass ......;
}

This detects all URLs that ends with .php or contains .php/ in the path and forwards it to the PHP-FPM server. The path after .php is sent as additional path in $_SERVER['PATH_INFO'].

Multiple Directory Setup

If you have multiple directory setup, it's important to write fastcgi: on where the directory should also serve PHP files, for example:

nginx:
fastcgi: on
locations:
- match: /admin/
root: public_app/public
fastcgi: on

The second fastcgi: on will make sure PHP files inside /admin/ directory are also served by PHP-FPM.

Rewrite root directory

nginx:
root: public_html/public

The default root directory is public_html, where app files are extracted from recipes. Some modern frameworks like Laravel and CodeIgniter put static files inside public folder to avoid leaking bare *.php files be accessed maliciously and creates RCE attack.

So when your app requires this behavior, you need to change the root folder to public_html/public.

info

Not to be confused with root: inside nginx:, this setting is placed outside of nginx: because it will also tell Virtualmin to use this folder as e.g. SSL verification requests.

Reroute PHP Files

There's two kind of reroutes: To hide .php extension or reroute all to top index.php file.

To reroute to top index.php file:

nginx:
locations:
- match: /
try_files: $uri $uri/ /index.php$is_args$args
fastcgi: on

If you use frameworks, they also likes to handle custom routes. The try_files: configuration here is to instruct NGINX to try to serve the root /index.php file in case no static files found in the given request URL.

To hide .php extension for files, you can use this:

nginx:
locations:
- match: /
try_files: $uri $uri/ $uri.php
fastcgi: on

Multiple website in a domain

root: public_html
nginx:
locations:
- match: ~ ^/(app|auth|api|web)/
root: public_app/public
fastcgi: on
try_files: $uri $uri/ /index.php$is_args$args
- match: /uploads/
alias: public_app/storage/app/public/
- match: /
try_files: $uri $uri/ /index.php$is_args$args
fastcgi: on

The example setup above is a typical setup for combining a WordPress (the landing page) in ~/public_html and a Laravel app in ~/public_app. Let's see what happens when we do requests from browser:

  1. /: Resolves to /public_html/index.php and loads the landing page at /.
  2. /about: Resolves to /public_html/index.php and loads the landing page at /about.
  3. /api/oauth: Resolves to /public_app/index.php and loads the Laravel app at /api/oauth.
  4. /web/login: Resolves to /public_app/index.php and loads the Laravel app at /web/login.
  5. /uploads/image.png: Resolves to /public_app/storage/app/public/image.png and loads the image (if exist).

Let's see another approach to combine multiple websites in a domain.

root: public_html
nginx:
locations:
- match: /app/
alias: public_app/public
fastcgi: on
try_files: $uri $uri/ @app
- match: "@app"
/app/(.*)$ /app/index.php last
- match: /uploads/
alias: public_app/storage/app/public/
- match: /
try_files: $uri $uri/ /index.php$is_args$args
fastcgi: on

The example above puts the whole laravel app inside /app/ subfolder. Let's see how it works:

  1. /: Resolves to /public_html/index.php and loads the landing page at /.
  2. /about: Resolves to /public_html/index.php and loads the landing page at /about.
  3. /app/api/oauth: Resolves to /public_app/index.php and loads the Laravel app at /api/oauth.
  4. /app/web/login: Resolves to /public_app/index.php and loads the Laravel app at /web/login.
  5. /web/login: Resolves to /public_html/index.php and loads the landing page at /web/login (404 error).
  6. /uploads/image.png: Resolves to /public_app/storage/app/public/image.png and loads the image (if exist).

You can read more about putting Laravel in subfolder using NGINX in this StackOverflow answer.

Troubleshooting

Some error codes you might be encounter when developing PHP apps.

403 (Forbidden)

A 403 error from NGINX means NGINX is unable to see if there's any file at given path.

If you're accessing a folder (the url ends with /) it means there's no index.html or index.php in the folder.

If you sure if the file exists, please check it's permission. NGINX needs to read from group perspective. So try chmod -R 0750 ~/public_html. Common cause is Laravel Storage is set Private. A private laravel storage is set as 600 which NGINX won't be able to read.

404 (Not Found)

A 404 error from NGINX means There's no PHP or any static file found for given path.

Please note that files in Linux are case-sensitive, unlike Windows, e.g. api.php is different with Api.php same thing applies with folder names.

500 (Blank Error)

A 500 error with empty body means your PHP apps has a critical error but won't emit any error message because the default production settings is used.

You need to identify the exact reason by reading PHP Error Logs.

502 (Bad Gateway)

A 502 error from NGINX means PHP configuration is corrupted and need to renitialized.

You can fix it by switching its PHP version. The configuration should can reheal itself.

features:
- php 7.4
- php 8.2

Custom PHP Daemon

You can also create your own System daemon in userspace for your own background service. For instance, you might want to install your own PHP with additional extensions we don't cover.

This requires to enable Docker Feature. A feature only available to Kit Plan and Later. The script below init the systemctl daemon:

mkdir -p $HOME/.local/share/systemd/user
cat << EOF > $HOME/.local/share/systemd/user/custom-php.service
[Unit]
Description=FPM
Documentation=Custom PHP FPM
After=network.target

[Service]
Type=simple
WorkingDirectory=$HOME
ExecStart=/opt/remi/php74/root/usr/sbin/php-fpm -c $HOME/etc/php7.4 -y $HOME/etc/php-fpm.conf -F
TimeoutStopSec=5
Restart=always

[Install]
WantedBy=default.target
EOF

cat << EOF > $HOME/etc/php-fpm.conf
error_log = $HOME/logs/php-fpm.log
[wordpress_site]
listen = $HOME/etc/php.sock
listen.owner = $USER
listen.group = $USER
pm = ondemand
pm.max_children = 10
pm.process_idle_timeout = 10s
EOF

systemctl enable custom-php --user
systemctl start custom-php --user

Then bind like this:

features:
- php etc/php.sock