Peter Davies

Ramblings

Virtualmin with Nginx and PHP 7 for Magento 2

Magento 2 runs noticeably better on PHP 7 than on 5.6, and Magento publish a ready-made Nginx config for it. I still wanted Virtualmin on the box to manage domains, users, DNS and mail. This is how I put the three together on Debian 8 (Jessie), mid-2016, for Magento 2.1.

The order matters: get PHP 7 in place before you create any virtual servers, because Virtualmin writes the PHP handler into each site's config at creation time.

Virtualmin with Nginx

Install Virtualmin with the normal install.sh, then follow Virtualmin's own Nginx guide. In short:

service apache2 stop
update-rc.d apache2 disable
apt-get install nginx webmin-virtualmin-nginx webmin-virtualmin-nginx-ssl

In Virtualmin, under System Settings, Features and Plugins, turn on "Nginx website" and "Nginx SSL website" and turn off the Apache features. Remove Debian's stock site so it doesn't answer for every IP on the box:

rm /etc/nginx/sites-enabled/default

PHP 7.0 from Dotdeb

Jessie only has PHP 5.6. Dotdeb packaged PHP 7.0 for it:

echo "deb http://packages.dotdeb.org jessie all" > /etc/apt/sources.list.d/dotdeb.list
wget https://www.dotdeb.org/dotdeb.gpg
apt-key add dotdeb.gpg
apt-get update

apt-get install php7.0-fpm php7.0-cli php7.0-mysql php7.0-gd php7.0-curl \
  php7.0-mcrypt php7.0-intl php7.0-xsl php7.0-mbstring php7.0-zip \
  php7.0-soap php7.0-json

That covers the extensions the Magento 2.1 installer checks for. FPM listens on /run/php/php7.0-fpm.sock.

Raise the PHP limits for the CLI and FPM. Magento's own guidance is at least 768M for memory_limit; set it in both /etc/php/7.0/fpm/php.ini and /etc/php/7.0/cli/php.ini, then service php7.0-fpm restart.

Make Virtualmin use the FPM socket

Virtualmin's Nginx module expects to run a php-cgi process per domain and points fastcgi_pass at that. I wanted every site on the FPM socket instead. The template lives in the module's Perl, around line 254 of virtual_feature.pl:

nano /usr/share/webmin/virtualmin-nginx/virtual_feature.pl

Replace the block that builds the fastcgi_pass directive with:

{ 'name' => 'fastcgi_pass',
  'words' => [ 'unix:/run/php/php7.0-fpm.sock' ],
},

Then in Virtualmin run System Settings, Re-Check Configuration so it notices PHP.

Two things to know about this. A Webmin update replaces the file, so check it after every upgrade. And one shared FPM pool means every site runs PHP as www-data. That suits a box dedicated to one store; on a shared server give each domain its own pool running as its own user.

MariaDB

Magento 2.1 wants MySQL 5.6 or equivalent; Jessie's default is 5.5. I used MariaDB 10.1 from MariaDB's repository:

apt-get install software-properties-common
apt-key adv --recv-keys --keyserver keyserver.ubuntu.com 0xcbcb082a1bb943db
echo "deb [arch=amd64,i386] http://ftp.osuosl.org/pub/mariadb/repo/10.1/debian jessie main" > /etc/apt/sources.list.d/mariadb.list
apt-get update
apt-get install mariadb-server

Any mirror from MariaDB's repository configuration tool will do for the deb line.

The Nginx server block

Create the virtual server in Virtualmin as normal, then edit the file it writes under /etc/nginx/sites-available/. The upstream goes in its own file so that a second site doesn't try to define it again (Nginx refuses to start on a duplicate upstream):

# /etc/nginx/conf.d/php7-fpm.conf
upstream fastcgi_backend {
    server unix:/run/php/php7.0-fpm.sock;
}

The server block, cut down to what matters. Virtualmin also writes a run of fastcgi_param lines into it; they can stay.

server {
    server_name example.com www.example.com;
    listen 192.0.2.10:80;
    listen 192.0.2.10:443 ssl;

    ssl_certificate /home/example/ssl.cert;
    ssl_certificate_key /home/example/ssl.key;

    access_log /var/log/virtualmin/example.com_access_log;
    error_log /var/log/virtualmin/example.com_error_log;

    set $MAGE_ROOT /home/example/public_html;
    set $MAGE_MODE developer;
    include /etc/nginx/magento.conf;
}

Magento's Nginx config

Magento 2 ships nginx.conf.sample in its root directory. Copy it rather than writing your own:

cp /home/example/public_html/nginx.conf.sample /etc/nginx/magento.conf
nginx -t && service nginx reload

It's worth reading through once. It sets the document root to $MAGE_ROOT/pub, so the application code isn't web-accessible at all. It denies the sensitive media folders (customer, downloadable, import) and cron.php. It only passes a short list of front controllers to PHP, so a stray .php file elsewhere in the tree won't execute. And it handles static file versioning, which is the part people get wrong when they write their own:

location /static/ {
    if (!-f $request_filename) {
        rewrite ^/static/(version\d*/)?(.*)$ /static.php?resource=$2 last;
    }
}

Magento puts a version number in static URLs so browsers can cache them for a year. That rewrite strips it, and if the file hasn't been generated yet static.php builds it on demand.

$MAGE_MODE is passed through to PHP. Leave it on developer while building, then switch to production once the code is deployed (bin/magento deploy:mode:set production) and change the variable to match, so the long expiry headers in the sample config apply.

Sample data

On a test build it's useful to have a populated catalogue. From the Magento root, as the domain's user:

bin/magento sampledata:deploy
bin/magento setup:upgrade

Then reboot the server once to make sure Nginx, PHP-FPM and MariaDB all come back on their own.

Today: Dotdeb is no longer maintained, PHP 7.0 and Debian 8 are long out of support, and current Magento 2.4 needs PHP 8 and OpenSearch. The shape of the setup still applies; take PHP from Ondřej Surý's repository or the distribution instead.