Upgrade PHP 7.4 to 8.0 on Ubuntu

March 5, 2021

I host several websites on a Linode VPS, originally provisioned with Laravel Forge. I have long since canceled my Forge subscription, and I do my own maintenance, manually. I may not be the quickest to upgrade to the latest version of anything, but I will do it eventually.

As an indie maker with zero revenue from my side projects, it wasn't making financial sense to pay for Forge. The biggest value Forge brought me was the initial provisioning of the instance. Afterwards, I continued to deploy code manually (how hard can git pull be?), and do my own basic server maintenance. I've been doing this successfully for the better part of the last 2 years.

This guide is about upgrading from PHP 7.4 to 8.0 on Ubuntu 18.04. As of this writing, I have yet to upgrade to Ubuntu 20.04, but it will happen soon(ish).

The instructions are partly based on this excellent article, but I've added many specifics, details, and pitfalls I encountered in my own situation. It wasn't exactly smooth sailing, as you'll see.

Prep

SSH into the instance, then...

# Check PHP version
php -v
PHP 7.4.15 (cli) (built: Feb 23 2021 15:12:05) ( NTS )
Copyright (c) The PHP Group
Zend Engine v3.4.0, Copyright (c) Zend Technologies
    with Zend OPcache v7.4.15, Copyright (c), by Zend Technologies

# List all PHP packages
dpkg -l | grep php | tee packages.txt

# List
ii  php-common                             2:81+ubuntu18.04.1+deb.sury.org+1                                  all          Common files for PHP packages
ii  php-igbinary                           3.2.1+2.0.8-6+ubuntu18.04.1+deb.sury.org+1                         amd64        igbinary PHP serializer
ii  php-memcached                          3.1.5+2.2.0-9+ubuntu18.04.1+deb.sury.org+1                         amd64        memcached extension module for PHP, uses libmemcached
ii  php-msgpack                            2.1.2+0.5.7-6+ubuntu18.04.1+deb.sury.org+1                         amd64        PHP extension for interfacing with MessagePack
ii  php-pear                               1:1.10.12+submodules+notgz+20210212-1+ubuntu18.04.1+deb.sury.org+1 all          PEAR Base System
ii  php7.4-bcmath                          7.4.15-7+ubuntu18.04.1+deb.sury.org+1                              amd64        Bcmath module for PHP
ii  php7.4-cli                             7.4.15-7+ubuntu18.04.1+deb.sury.org+1                              amd64        command-line interpreter for the PHP scripting language
ii  php7.4-common                          7.4.15-7+ubuntu18.04.1+deb.sury.org+1                              amd64        documentation, examples and common module for PHP
ii  php7.4-curl                            7.4.15-7+ubuntu18.04.1+deb.sury.org+1                              amd64        CURL module for PHP
ii  php7.4-dev                             7.4.15-7+ubuntu18.04.1+deb.sury.org+1                              amd64        Files for PHP7.4 module development
ii  php7.4-fpm                             7.4.15-7+ubuntu18.04.1+deb.sury.org+1                              amd64        server-side, HTML-embedded scripting language (FPM-CGI binary)
ii  php7.4-gd                              7.4.15-7+ubuntu18.04.1+deb.sury.org+1                              amd64        GD module for PHP
ii  php7.4-igbinary                        3.2.1+2.0.8-6+ubuntu18.04.1+deb.sury.org+1                         amd64        igbinary PHP serializer
ii  php7.4-imap                            7.4.15-7+ubuntu18.04.1+deb.sury.org+1                              amd64        IMAP module for PHP
ii  php7.4-intl                            7.4.15-7+ubuntu18.04.1+deb.sury.org+1                              amd64        Internationalisation module for PHP
ii  php7.4-json                            7.4.15-7+ubuntu18.04.1+deb.sury.org+1                              amd64        JSON module for PHP
ii  php7.4-mbstring                        7.4.15-7+ubuntu18.04.1+deb.sury.org+1                              amd64        MBSTRING module for PHP
rc  php7.4-memcached                       3.1.5+2.2.0-9+ubuntu18.04.1+deb.sury.org+1                         amd64        memcached extension module for PHP, uses libmemcached
ii  php7.4-msgpack                         2.1.2+0.5.7-6+ubuntu18.04.1+deb.sury.org+1                         amd64        PHP extension for interfacing with MessagePack
ii  php7.4-mysql                           7.4.15-7+ubuntu18.04.1+deb.sury.org+1                              amd64        MySQL module for PHP
ii  php7.4-opcache                         7.4.15-7+ubuntu18.04.1+deb.sury.org+1                              amd64        Zend OpCache module for PHP
ii  php7.4-pgsql                           7.4.15-7+ubuntu18.04.1+deb.sury.org+1                              amd64        PostgreSQL module for PHP
ii  php7.4-readline                        7.4.15-7+ubuntu18.04.1+deb.sury.org+1                              amd64        readline module for PHP
ii  php7.4-soap                            7.4.15-7+ubuntu18.04.1+deb.sury.org+1                              amd64        SOAP module for PHP
ii  php7.4-sqlite3                         7.4.15-7+ubuntu18.04.1+deb.sury.org+1                              amd64        SQLite3 module for PHP
ii  php7.4-xml                             7.4.15-7+ubuntu18.04.1+deb.sury.org+1                              amd64        DOM, SimpleXML, XML, and XSL module for PHP
ii  php7.4-zip                             7.4.15-7+ubuntu18.04.1+deb.sury.org+1                              amd64        Zip module for PHP
ii  php8.0-common                          8.0.2-7+ubuntu18.04.1+deb.sury.org+1                               amd64        documentation, examples and common module for PHP
ii  php8.0-igbinary                        3.2.1+2.0.8-6+ubuntu18.04.1+deb.sury.org+1                         amd64        igbinary PHP serializer
ii  php8.0-memcached                       3.1.5+2.2.0-9+ubuntu18.04.1+deb.sury.org+1                         amd64        memcached extension module for PHP, uses libmemcached
ii  php8.0-msgpack                         2.1.2+0.5.7-6+ubuntu18.04.1+deb.sury.org+1                         amd64        PHP extension for interfacing with MessagePack
ii  pkg-php-tools                          1.35ubuntu1                                                        all          various packaging tools and scripts for PHP packages

Add PPAs

# Add ondrej/php PPA
sudo add-apt-repository ppa:ondrej/php # Press enter when prompted.
# CAVEATS:
# 1. If you are using php-gearman, you need to add ppa:ondrej/pkg-gearman
# 2. If you are using apache2, you are advised to add ppa:ondrej/apache2
# 3. If you are using nginx, you are advised to add ppa:ondrej/nginx-mainline or ppa:ondrej/nginx

sudo add-apt-repository ppa:ondrej/nginx

# Remove a repository (not needed here, listed for reference)
# sudo add-apt-repository --remove ppa:ondrej/nginx

# Check if PPA was added - should see a number of entries
grep ^ /etc/apt/sources.list /etc/apt/sources.list.d/* | grep ondrej/php
grep ^ /etc/apt/sources.list /etc/apt/sources.list.d/* | grep ondrej/nginx

sudo apt-get update

Install PHP 8.0

sudo apt install php8.0-common php8.0-cli -y

# Check version
php -v
PHP 8.0.2 (cli) (built: Feb 23 2021 15:13:59) ( NTS )
Copyright (c) The PHP Group
Zend Engine v4.0.2, Copyright (c) Zend Technologies
    with Zend OPcache v8.0.2, Copyright (c), by Zend Technologies

# Check modules (each will appear on its own line, listed on a single line here for brevity)
php -m
[PHP Modules] calendar Core ctype date exif FFI fileinfo filter ftp gettext hash iconv igbinary json libxml memcached msgpack openssl pcntl pcre PDO Phar posix readline Reflection session shmop sockets sodium SPL standard sysvmsg sysvsem sysvshm tokenizer Zend OPcache zlib
[Zend Modules]
Zend OPcache

# Install additional extensions that were present in 7.4
# Note: no need to install php8.0-json; it's already provided by other packages  
sudo apt install php8.0-{bcmath,curl,dev,fpm,gd,igbinary,imap,intl,mbstring,memcached,msgpack,mysql,opcache,pgsql,readline,soap,sqlite3,xml,zip}

Cleanup

Since I won't be using old versions of PHP (< 7.4), now is a good time to remove them.

# Purge old packages, in my case from PHP 5.6 thru 7.3
sudo apt purge '^php5.6.*'
sudo apt purge '^php7.0.*'
sudo apt purge '^php7.1.*'
sudo apt purge '^php7.2.*'
sudo apt purge '^php7.3.*'

Update Nginx config for each site

This is for Nginx servers only. Sorry, I can't help you with Apache.

Repeat the procedure for each site. There's a way to automate it, probably with sed but I don't do this often enough to warrant the

sudo vi /etc/nginx/sites-enabled/example.com

# Look for the following block
location ~ \.php$ {
    fastcgi_split_path_info ^(.+\.php)(/.+)$;
    fastcgi_pass unix:/var/run/php/php7.4-fpm.sock; # change to php8.0-fpm.sock or even better to php8.0-fpm.sock
    fastcgi_index index.php;
    include fastcgi_params;
}

# Test Nginx config
sudo nginx -t
nginx: the configuration file /etc/nginx/nginx.conf syntax is ok
nginx: configuration file /etc/nginx/nginx.conf test is successful

# Restart PHP FPM & Nginx
sudo service php8.0-fpm restart
sudo service nginx restart

Note The reason I recommend using the generic php-fpm.sock instead of php8.0-fpm.sock is because, on my server at least, php-fpm.sock is actually aliased to php8.0-fpm.sock. Run these commands to find out:

ls -al /var/run/php/php-fpm.sock # /var/run/php/php-fpm.sock -> /etc/alternatives/php-fpm.sock
ls -al /etc/alternatives/php-fpm.sock # /etc/alternatives/php-fpm.sock -> /run/php/php8.0-fpm.sock

I don't know the specifics of why it is so, but I assume it was done as part of the PHP 8 upgrade, which works really well for me as I don't have to worry about changing the Nginx config the next time I upgrade PHP.

Composer install errors?

Let's say that, after upgrading PHP to the shiny new 8.0, you want to run composer install --no-interaction --prefer-dist --optimize-autoloader in your Laravel project, and see the following errors:

Deprecation Notice: Required parameter $path follows optional parameter $schema in phar:///usr/local/bin/composer/vendor/justinrainbow/json-schema/src/JsonSchema/Constraints/UndefinedConstraint.php:62
Deprecation Notice: Required parameter $path follows optional parameter $schema in phar:///usr/local/bin/composer/vendor/justinrainbow/json-schema/src/JsonSchema/Constraints/UndefinedConstraint.php:108
Deprecation Notice: Method ReflectionParameter::getClass() is deprecated in phar:///usr/local/bin/composer/src/Composer/Repository/RepositoryManager.php:130
Deprecation Notice: Method ReflectionParameter::getClass() is deprecated in phar:///usr/local/bin/composer/src/Composer/Repository/RepositoryManager.php:130
Loading composer repositories with package information
Installing dependencies (including require-dev) from lock file
PHP Fatal error:  Uncaught ArgumentCountError: array_merge() does not accept unknown named parameters in phar:///usr/local/bin/composer/src/Composer/DependencyResolver/DefaultPolicy.php:84
Stack trace:
#0 [internal function]: array_merge()
#1 phar:///usr/local/bin/composer/src/Composer/DependencyResolver/DefaultPolicy.php(84): call_user_func_array()
#2 phar:///usr/local/bin/composer/src/Composer/DependencyResolver/Solver.php(387): Composer\DependencyResolver\DefaultPolicy->selectPreferredPackages()
#3 phar:///usr/local/bin/composer/src/Composer/DependencyResolver/Solver.php(740): Composer\DependencyResolver\Solver->selectAndInstall()
#4 phar:///usr/local/bin/composer/src/Composer/DependencyResolver/Solver.php(231): Composer\DependencyResolver\Solver->runSat()
#5 phar:///usr/local/bin/composer/src/Composer/Installer.php(489): Composer\DependencyResolver\Solver->solve()
#6 phar:///usr/local/bin/composer/src/Composer/Installer.php(232): Composer\Installer->doInstall()
#7 phar:///usr/local/bin/composer/src/Composer/Command/InstallCommand.php(122): Composer\Installer->run()
#8 phar:///usr/local/bin/composer/vendor/symfony/console/Command/Command.php(245): Composer\Command\InstallCommand->execute()
#9 phar:///usr/local/bin/composer/vendor/symfony/console/Application.php(835): Symfony\Component\Console\Command\Command->run()
#10 phar:///usr/local/bin/composer/vendor/symfony/console/Application.php(185): Symfony\Component\Console\Application->doRunCommand()
#11 phar:///usr/local/bin/composer/src/Composer/Console/Application.php(281): Symfony\Component\Console\Application->doRun()
#12 phar:///usr/local/bin/composer/vendor/symfony/console/Application.php(117): Composer\Console\Application->doRun()
#13 phar:///usr/local/bin/composer/src/Composer/Console/Application.php(113): Symfony\Component\Console\Application->run()
#14 phar:///usr/local/bin/composer/bin/composer(61): Composer\Console\Application->run()
#15 /usr/local/bin/composer(24): require('...')
#16 {main}
  thrown in phar:///usr/local/bin/composer/src/Composer/DependencyResolver/DefaultPolicy.php on line 84

Fatal error: Uncaught ArgumentCountError: array_merge() does not accept unknown named parameters in phar:///usr/local/bin/composer/src/Composer/DependencyResolver/DefaultPolicy.php:84
Stack trace:
#0 [internal function]: array_merge()
#1 phar:///usr/local/bin/composer/src/Composer/DependencyResolver/DefaultPolicy.php(84): call_user_func_array()
#2 phar:///usr/local/bin/composer/src/Composer/DependencyResolver/Solver.php(387): Composer\DependencyResolver\DefaultPolicy->selectPreferredPackages()
#3 phar:///usr/local/bin/composer/src/Composer/DependencyResolver/Solver.php(740): Composer\DependencyResolver\Solver->selectAndInstall()
#4 phar:///usr/local/bin/composer/src/Composer/DependencyResolver/Solver.php(231): Composer\DependencyResolver\Solver->runSat()
#5 phar:///usr/local/bin/composer/src/Composer/Installer.php(489): Composer\DependencyResolver\Solver->solve()
#6 phar:///usr/local/bin/composer/src/Composer/Installer.php(232): Composer\Installer->doInstall()
#7 phar:///usr/local/bin/composer/src/Composer/Command/InstallCommand.php(122): Composer\Installer->run()
#8 phar:///usr/local/bin/composer/vendor/symfony/console/Command/Command.php(245): Composer\Command\InstallCommand->execute()
#9 phar:///usr/local/bin/composer/vendor/symfony/console/Application.php(835): Symfony\Component\Console\Command\Command->run()
#10 phar:///usr/local/bin/composer/vendor/symfony/console/Application.php(185): Symfony\Component\Console\Application->doRunCommand()
#11 phar:///usr/local/bin/composer/src/Composer/Console/Application.php(281): Symfony\Component\Console\Application->doRun()
#12 phar:///usr/local/bin/composer/vendor/symfony/console/Application.php(117): Composer\Console\Application->doRun()
#13 phar:///usr/local/bin/composer/src/Composer/Console/Application.php(113): Symfony\Component\Console\Application->run()
#14 phar:///usr/local/bin/composer/bin/composer(61): Composer\Console\Application->run()
#15 /usr/local/bin/composer(24): require('...')
#16 {main}
  thrown in phar:///usr/local/bin/composer/src/Composer/DependencyResolver/DefaultPolicy.php on line 84

Oops, apparently I completely forgot that my server still runs Composer 1.0, while my local environment was upgraded to 2.0 a long time ago. Time to upgrade the server too.

Upgrade Composer from 1.0 to 2.0

This article partially covers the procedure, but here are my own steps:

# Check version
composer --version
Composer version 1.10.1 2020-03-13 20:34:27

# Upgrade Composer to 2.0
sudo composer self-update

# Downgrade Composer to 1.0 (in case you need it)
# sudo composer self-update --rollback # return to version 1.10.1

# Check version
composer --version
Composer version 2.0.11 2021-02-24 14:57:23

composer install should once again work without issues.

500 Internal Server Error

Everything looks fine and dandy, right? Not so fast. There's always a final wrench in the proverbial gears. You may not encounter this specific issue, but it's absolutely worth documenting.

When I loaded one of my main Laravel sites in the browser, I was presented with a nice blank page (production client reporting is off as it should be), but with a 500 Internal Server Error in the dev console. So let's tail the Nginx app log to see what is going on, using tail -f /var/log/nginx/example.com-error.log:

2021/03/05 06:42:09 [error] 836#836: *1 FastCGI sent in stderr: "PHP message: PHP Fatal error:  Uncaught ErrorException: file_put_contents(/home/forge/example.com/storage/framework/views/2e31adb7dfd4e14cc6108d8b49272e43adaa7371.php): Failed to open stream: Permission denied in /home/forge/example.com/vendor/laravel/framework/src/Illuminate/Filesystem/Filesystem.php:135
Stack trace:
#0 [internal function]: Illuminate\Foundation\Bootstrap\HandleExceptions->handleError()
#1 /home/forge/example.com/vendor/laravel/framework/src/Illuminate/Filesystem/Filesystem.php(135): file_put_contents()
#2 /home/forge/example.com/vendor/laravel/framework/src/Illuminate/View/Compilers/BladeCompiler.php(150): Illuminate\Filesystem\Filesystem->put()
#3 /home/forge/example.com/vendor/laravel/framework/src/Illuminate/View/Engines/CompilerEngine.php(51): Illuminate\View\Compilers\BladeCompiler->compile()
#4 /home/forge/example.com/vendor/facade/ignition/src/Views/Engines/CompilerEngine.php(37): Illuminate\View\Engines\CompilerEngine->get()
#5 /home/forge/example.com/vendor/laravel/fram...PHP message: PHP Fatal error:  Uncaught ErrorException: file_put_contents(/home/forge/example.com/storage/framework/views/2e31adb7dfd4e14cc6108d8b49272e43adaa7371.php): Failed to open stream: Permission denied in /home/forge/example.com/vendor/laravel/framework/src/Illuminate/Filesystem/Filesystem.php:135
Stack trace:
#0 [internal function]: Illuminate\Foundation\Bootstrap\HandleExceptions->handleError()
#1 /home/forge/example.com/vendor/laravel/framework/src/Illuminate/Filesystem/Filesystem.php(135): file_put_contents()
#2 /home/forge/example.com/vendor/laravel/framework/src/Illuminate/View/Compilers/BladeCompiler.php(150): Illuminate\Filesystem\Filesystem->put()
#3 /home/forge/example.com/vendor/laravel/framework/src/Illuminate/View/Engines/CompilerEngine.php(51): Illuminate\View\Compilers\BladeCompiler->compile()
#4 /home/forge/example.com/vendor/facade/ignition/src/Views/Engines/CompilerEngine.php(37): Illuminate\View\Engines\CompilerEngine->get()

At first glance it looks like Laravel doesn't have permissions to write to the storage/ folder.

After some digging, I realized that ownership and permissions for the storage/ folder have changed for some reason. I seem to recall a similar situation from a couple of years back.

An easy way to find if permissions are out of whack is to compare with another site that still works. Right away I noticed that the sub-folders in storage/ had 755 permissions instead of 775.

# Before 755
ls -al /home/forge/example.com/storage/framework/
total 76
drwxr-xr-x 6 forge forge  4096 Mar  5 07:00 .
drwxr-xr-x 6 forge forge  4096 Aug 23  2019 ..
drwxr-xr-x 3 forge forge  4096 Aug 23  2019 cache
-rwxr-xr-x 1 forge forge   103 Aug 23  2019 .gitignore
drwxr-xr-x 2 forge forge 40960 Mar  5 06:16 sessions
drwxr-xr-x 2 forge forge  4096 Aug 23  2019 testing
drwxr-xr-x 2 forge forge 12288 Mar  5 06:37 views

# Recursively fix ownership and permissions 
sudo chown -R forge:www-data storage
sudo chmod -R ug+w storage

# After 775
ls -al /home/forge/example.com/storage/framework/
total 48
drwxrwxr-x 6 forge forge  4096 Jan 19  2020 .
drwxrwxr-x 5 forge forge  4096 Jan 19  2020 ..
drwxrwxr-x 3 forge forge  4096 Jan 19  2020 cache
-rwxrwxr-x 1 forge forge   103 Jan 19  2020 .gitignore
drwxr-xr-x 2 forge forge 40960 Mar  5 06:16 sessions
drwxrwxr-x 2 forge forge  4096 Jan 19  2020 testing
drwxr-xr-x 2 forge forge 12288 Mar  5 06:37 views

php artisan cache:clear
composer dumpautoload
sudo service nginx restart
# OK

The end

This concludes the PHP 7.4 -> 8.0 upgrade. All systems are green. Lessons were learned.

PHP Linux
Liked this article? Share it on your favorite platform.
Picture of me