NGINX + PHP

From Wiki³

Icon Introduction

Icon Install

Beforehand be sure to determine weather the web server will be using MySQL (ie. MariaDB) or PostgreSQL.

Begin by installing NGINX, PHP and other required utilities.

# pikaur -S apache-tools composer curl minify nginx php-fpm sassc wget

Install all of the required PHP extensions.

# pikaur -S php-gd php-geoip php-imagick php-intl php-memcache php-odbc php-sqlite php-sodium xdebug

Next create the environment for the web server.

# sudo mkdir -p /nginx/conf.d /nginx/https /nginx/logs /nginx/sql /nginx/ssl /nginx/vhosts.d
 
# sudo chown -R http:http /nginx
 
# sudo chmod -R 770 /nginx
 
# sudo chmod 750 /nginx/sql
 
# sudo gpasswd -a username http

Set the default shell for http to Bash.

# sudo chsh -s /bin/bash http

Icon PostgreSQL

Using postgresql as a back-end will require the following setup and configuration.

# pikaur -S postgresql php-pgsql
 
# sudo chown postgres:postgres /nginx/sql
 
# sudo gpasswd -a username postgres

Swap over to the postgresql user account.

# sudo -iu postgres

Run the database initialization.

# initdb --locale en_US.UTF-8 -E UTF8 -D '/nginx/sql/data'

Return to the normal user account.

# exit

Modify the systemd service file to reflect the new data directory.

# sudo systemctl edit postgresql.service
 
filename: postgresql.service
Environment=PGROOT=/nginx/sql
PIDFile=/nginx/sql/postmaster.pid

Start and enable the systemd service.

# sudo systemctl enable --now postgresql.service

Swap back over to the postgresql user account.

# sudo -iu postgres

Create a new postgres user account.

# createuser -P --interactive
Enter name of role to add: username
Enter password for new role: ********
Enter it again: ********
Shall the new role be a superuser? (y/n) n
Shall the new role be allowed to create databases? (y/n) y
Shall the new role be allowed to create more new roles? (y/n) n

Icon MariaDB

Using mariadb as a back-end will require the following setup and configuration.

# pikaur -S mariadb
 
# sudo chown mysql:mysql /nginx/sql

Give the current logged in user access.

# sudo gpasswd -a username mysql

Create and initialize the data directory.

# mariadb-install-db --user=mysql --basedir=/usr --datadir=/nginx/sql
 
# sudoedit /etc/my.cnf.d/server.cnf
 
filename: /etc/my.cnf.d/server.cnf
[mysqld]
datadir=/nginx/sql

Start and enable the MySQL service.

# sudo systemctl enable --now mariadb.service

Secure the installation and set the root password.

# sudo mysql_secure_installation
IconThe default mysql root password is none

Connect to mysql using the root account and the password you previously set.

# sudo mysql -u root -p

Add a new mysql user account.

# MariaDB [(none)]> GRANT ALL PRIVILEGES ON *.* TO 'kyau'@'localhost' \
  IDENTIFIED BY 'user_password' WITH GRANT OPTION;

Icon Configuration

Icon PHP

First remove the default pool.

# sudo rm /etc/php/php-fpm.d/www.conf

Create the defaults for all pools.

# sudoedit /etc/php/php-fpm.d/defaults.inc
 
filename: /etc/php/php-fpm.d/defaults.inc
user = http
group = http
listen = /run/php-fpm/php-fpm-$pool.sock
listen.owner = http
listen.group = http
; process configuration
pm = dynamic
pm.max_children = 5
pm.start_servers = 2
pm.min_spare_servers = 1
pm.max_spare_servers = 3
; php.ini changes
php_admin_flag[expose_php] = off
php_admin_flag[log_errors] = on
php_admin_flag[short_open_tag] = on
php_admin_value[date.timezone] = America/Los_Angeles
php_admin_value[error_log] = /nginx/logs/$pool/php.log
php_admin_value[memory_limit] = 256M
php_admin_value[post_max_size] = 2048M
php_admin_value[session.save_path] = /tmp
php_admin_value[upload_max_filesize] = 2048M

Enable all third party PHP extensions that were installed.

# sudo find . -type f -name '*.ini' -exec sed -i -e 's/^;extension/extension/g' \
  -e 's/^;zend_extension/zend_extension/g' -e 's/^;xdebug/xdebug/g' {} +

Enable global PHP extensions.

# sudoedit /etc/php/conf.d/defaults.ini
 
filename: /etc/php/conf.d/defaults.ini
extension=bz2
extension=exif
extension=gd
extension=gettext
extension=gmp
extension=iconv
extension=intl
extension=sodium
extension=mysqli
extension=odbc
extension=pdo_mysql
extension=pdo_odbc
extension=pdo_sqlite
extension=sockets
extension=sqlite3
; opcache
zend_extension=opcache
opcache.enable = 1
opcache.interned_strings_buffer = 8
opcache.max_accelerated_files = 10000
opcache.memory_consumption = 128
opcache.save_comments = 1
opcache.revalidate_freq = 1

Create a php-fpm pool for the domain being setup (use a different pool for each site/domain).

# sudoedit /etc/php/php-fpm.d/domain_com.conf
 
filename: /etc/php/php-fpm.d/domain_com.conf
; $KYAULabs: domain_com.conf,v 1.0.0 2021/05/01 12:36:14 kyau Exp $

[domain_com]
include = /etc/php/php-fpm.d/defaults.inc
env[HOSTNAME] = domain.com
env[PATH] = /usr/local/bin:/usr/bin:/bin
env[TMP] = /tmp
env[TMPDIR] = /tmp
env[TEMP] = /tmp

; vim: ft=dosini sts=4 sw=4 ts=4 noet :
 
IconOne can temporarily disable sites/domains by renaming the config to .conf.disable and reloading the php-fpm service

Be sure to set the file permissions properly.

# sudo chmod 644 /etc/php/conf.d/defaults.ini /etc/php/php-fpm.d/*

Start and enable the php-fpm service.

# sudo systemctl enable --now php-fpm.service

Icon NGINX

Create a blank configuration file.

# sudo install -g http -m 660 -o http /dev/null /nginx/conf.d/nginx.conf

Copy the MIME types file.

# sudo install -g http -m 660 -o http /etc/nginx/mime.types /nginx/conf.d/mime.types

Remove the default config in nginx.conf and replace it with an include (to the new config location).

# sudoedit /etc/nginx/nginx.conf
 
filename: /etc/nginx/nginx.conf
include /nginx/conf.d/nginx.conf;

Create the nginx config file.

# sudoedit /nginx/conf.d/nginx.conf
 
filename: /nginx/conf.d/nginx.conf
# $KYAULabs: nginx.conf,v 1.1.7 2021/05/03 18:14:27 kyau Exp $

# Help / Additional Info {{{
# always test configuration before reload!
# $ sudo nginx -t
# reload the configuration by using reload not restart!
# $ sudo systemctl reload nginx
# https://www.nginx.com/resources/wiki/start/topics/tutorials/config_pitfalls/
# }}}

# enables the use of “just-in-time compilation” for the regular expressions
# known by the time of configuration parsing
pcre_jit on;
# user and group credentials used by worker processes
user http http;
# number of worker processes (auto will autodetect number of CPU cores)
worker_processes auto;
# binds worker processes automatically to available CPUs
worker_cpu_affinity auto;
# number of file descriptors used for nginx
worker_rlimit_nofile 65535;

events {
# worker process will accept one/all (off/on) connection(s) at a time
multi_accept on;
# maximum number of simultaneous connections that can be opened by a worker
worker_connections 4096;
}

http {
# mime types
include /nginx/conf.d/mime.types;
# to boost I/O on HDD we can disable access logs
access_log off;
# read and send using multi-threading, without blocking a worker process
aio threads;
# hide index pages
autoindex off;
# add to 'Content-Type' response header
charset utf-8;
# request timed out -- default 60
client_body_timeout 10;
# sets the maximum allowed size of the client request body -- default 1
client_max_body_size 16m;
# default mime type
default_type text/plain;
# enable gzipping of responses
gzip on;
# disables gzipping of responses for msie6 and below
gzip_disable "msie6";
# minimum length of a response that will be gzipped -- default 20
gzip_min_length 1024;
# gzip compression level -- default 1
gzip_comp_level 6;
gzip_vary on;
gzip_proxied expired no-cache no-store private auth;
# text/html is always compressed
gzip_types
text/css
text/javascript
text/xml
text/x-component
application/javascript
application/x-javascript
application/json
application/xml
application/rss+xml
application/atom+xml
font/truetype
font/opentype
application/vnd.ms-fontobject
image/svg+xml;
# files that will be used as an index, checked in the specified order
index index.php index.html index.htm index.txt;
# set the global path for njs
js_path "/nginx/njs";
# import the variables njs module
js_import variables.js;
# enables keep-alive connections with all browsers
keepalive_disable none;
# keep-alive client connections stay active for -- default 75
keepalive_timeout 30s;
# specifies log format
log_format main
'$remote_addr - $remote_user [$time_local] '
'"$request" $status $body_bytes_sent '
'"$http_referer" "$http_user_agent"';
# disables logging of errors about not found files into error_log
log_not_found off;
# cache open file descriptors, directories and file lookup errors
open_file_cache max=10240 inactive=20s;
open_file_cache_valid 30s;
open_file_cache_min_uses 2;
open_file_cache_errors on;
# allow the server to close connection on non responding client, this will
# free up memory
reset_timedout_connection on;
# if client stops responding, free up memory -- default 60
send_timeout 8;
# copies data between one FD and other from within the kernel faster than
# read() + write()
sendfile on;
# bucket size for the server names hash tables
server_names_hash_bucket_size 128;
# disables emitting nginx version on error pages and in the "server"
# response header field
server_tokens off;
# send headers in one piece, it is better than sending them one by one
tcp_nopush on;
# don't buffer data sent, good for small data bursts in real time
tcp_nodelay on;
# hash table maximum size -- default 1024
types_hash_max_size 4096;

# redirect all non-encrypted (http) traffic to encrypted (https)
server {
server_name _;
listen *:80 default_server;
listen [::]:80 default_server;
return 301 https://$host$request_uri;
}

# include domain configuration files
include /nginx/vhosts.d/*.conf;
}

# vim: ft=nginx sts=4 sw=4 ts=4 noet :

Create a vhost defaults config.

# sudoedit /nginx/conf.d/vhost_defaults
 
filename: /nginx/conf.d/vhost_defaults
# $KYAULabs: vhost_defaults,v 1.0.6 2021/05/05 05:22:38 kyau Exp $

js_set $domain njs_domain($server_name);
js_set $pdomain nfs_pdomain($domain);
js_set $subdomain njs_subdomain($server_name);

access_log /nginx/logs/$pdomain/$subdomain-access.log;
error_log /nginx/logs/$pdomain/$subdomain-error.log;

ssl_certificate /etc/letsencrypt/live/$domain/fullchain.pem;
ssl_certificate_key /etc/letsencrypt/live/$domain/privkey.pem;
ssl_trusted_certificate /etc/letsencrypt/live/$domain/chain.pem;

include /nginx/conf.d/ssl.conf;

# security settings
add_header Strict-Transport-Security "max-age=31536000; includeSubDomains; preload" always;
add_header X-Frame-Options DENY;
add_header X-Content-Type-Options nosniff;
add_header X-XSS-Protection "1; mode=block";

root /nginx/https/$domain/$subdomain;

# vim: ft=nginx sts=4 sw=4 ts=4 noet :

Create a URI defaults config.

# sudoedit /nginx/conf.d/uri_defaults
filename: /nginx/conf.d/uri_defaults
# $KYAULabs: uri_defaults,v 1.0.2 2021/05/05 17:55:10 kyau Exp $

# remove `robots.txt` and all favicons from the logs
location ~* "^/(favicon\.\S{3,5}|robots\.txt)$" {
access_log off;
allow all;
log_not_found off;
}

# disable dot (hidden) files while allowing `.well-known`
location ~* /\.(?!well-known).* {
access_log off;
deny all;
log_not_found off;
}

# deny access to any sensitive material
location ~* (?:#.*#|\.(?:bak|conf|dist|fla|in[ci]|log|orig|phps|psd|sass|scss|sh|sql|sw[op])|~)$ {
access_log on;
deny all;
log_not_found on;
}

# asset/media cache
location ~* \.(?:css(\.map)?|js(\.map)?|jpe?g|png|gif|ico|cur|heic|webp|tiff?|mp3|m4a|aac|ogg|midi?|wav|mp4|mov|webm|mpe?g|avi|ogv|flv|wmv)$ {
access_log off;
expires 7d;
}

# fonts/svg cache and access
location ~* \.(?:svgz?|ttf|ttc|otf|eot|woff2?)$ {
access_log off;
add_header Access-Control-Allow-Origin "*";
expires 7d;
}

# html processing
location ~* \.html$ {
try_files $uri $uri/ /index.html =404;
}

# php scripts
location ~* [^/]\.php(/|$) {
try_files $fastcgi_script_name =404;
include /nginx/conf.d/fastcgi_params;
fastcgi_buffers 8 16k;
fastcgi_buffer_size 32k;
fastcgi_index index.php;
fastcgi_split_path_info ^(.+?\.php)(/.*)$;
fastcgi_pass unix:/run/php-fpm/php-fpm-$pdomain.sock;
}

# http error pages
error_page 404 /404;
error_page 500 502 503 504 /50x;
location = /404 {
root /nginx/https/error;
index 404.php;
}
location = /50x {
root /nginx/https/error;
index 50x.php;
}

# vim: ft=nginx sts=4 sw=4 ts=4 noet :

Create the variables njs script.

# sudoedit /nginx/njs/variables.js
 
filename: /nginx/njs/variables.js
/* $KYAULabs: variables.js,v 1.0.0 2021/05/05 14:51:39 kyau Exp $
*/


function njs_subdomain(server_name) {
return server_name.split('.')[0];
}

function njs_domain(server_name) {
domain = server_name;
if (server_name != null) {
var parts = server_name.split('.').reverse();
if (parts != null && parts.length > 1) {
domain = parts[1] + '.' + parts[0];

//special-case TLDs
//if (server_name.toLowerCase().indexOf('.co.uk') != -1 && parts.length > 2) {
// domain = parts[2] + '.' + domain;
//}

}
}
return domain;
}

function njs_pdomain(domain) {
return domain.replace('.', '_');
}

/*
vim: ft=javascript sts=4 sw=4 ts=4 noet :
*/

FastCGI

Create a fastcgi_params config file (PHP environmental variable defaults).

# sudoedit /nginx/conf.d/fastcgi_params
 
filename: /nginx/conf.d/fastcgi_params
# $KYAULabs: fastcgi_params,v 1.0.5 2021/05/03 17:31:37 kyau Exp $

fastcgi_param QUERY_STRING $query_string;
fastcgi_param REQUEST_METHOD $request_method;
fastcgi_param CONTENT_TYPE $content_type;
fastcgi_param CONTENT_LENGTH $content_length;

fastcgi_param SCRIPT_FILENAME $request_filename;
fastcgi_param SCRIPT_NAME $fastcgi_script_name;
fastcgi_param REQUEST_URI $request_uri;
fastcgi_param DOCUMENT_URI $document_uri;
fastcgi_param DOCUMENT_ROOT $document_root;
fastcgi_param SERVER_PROTOCOL $server_protocol;
fastcgi_param REQUEST_SCHEME $scheme;
fastcgi_param HTTPS $https if_not_empty;

fastcgi_param GATEWAY_INTERFACE CGI/1.1;
#fastcgi_param SERVER_SOFTWARE nginx/$nginx_version;
fastcgi_param SERVER_SOFTWARE nginx;

fastcgi_param REMOTE_ADDR $remote_addr;
fastcgi_param REMOTE_PORT $remote_port;
fastcgi_param SERVER_ADDR $server_addr;
fastcgi_param SERVER_PORT $server_port;
fastcgi_param SERVER_NAME $server_name;

# PHP only, required if PHP was built with --enable-force-cgi-redirect
fastcgi_param REDIRECT_STATUS 200;

# Mitigate https://httpoxy.org/ vulnerabilities
fastcgi_param HTTP_PROXY "";

# vim: ft=nginx sts=4 sw=4 ts=4 noet :

SSL/TLS

Create an SSL config file (SSL/TLS hardening/defaults).

# sudoedit /nginx/conf.d/ssl.conf
 
filename: /nginx/conf.d/ssl.conf
# $KYAULabs: ssl.conf,v 1.0.3 2021/05/03 18:00:56 kyau Exp $

## SSL/TLS (https://cipherlist.dev/)
ssl_dhparam /nginx/ssl/dhparam4096.pem; # openssl dhparam -out dhparam4096.pem 4096
ssl_protocols TLSv1.3; # Requires nginx >= 1.13.0
ssl_ciphers EECDH+CHACHA20:EECDH+AES;
ssl_ecdh_curve X25519; # Requires nginx >= 1.1.0
ssl_session_cache shared:SSL:10m;
ssl_session_tickets off; # Requires nginx >= 1.5.9
ssl_session_timeout 10m;
ssl_stapling on; # Requires nginx >= 1.3.7
ssl_stapling_verify on; # Requires nginx >= 1.3.7
ssl_prefer_server_ciphers on;
resolver 1.1.1.1 1.0.0.1 [2606:4700:4700::1111] [2606:4700:4700::1001] valid=60s; # Change if you run your own DNS servers
resolver_timeout 2s;

# vim: ft=nginx sts=4 sw=4 ts=4 noet :

Create the dhparam as indicated above.

# sudo -u http openssl dhparam -out /nginx/ssl/dhparam4096.pem 4096

Set permissions properly.

# sudo chmod 660 /nginx/ssl/dhparam4096.pem

Virtual Hosts

Virtual Hosts are created in domain config files keeping all subdomains in a single file. The following template can be used, simply replace domain_com and domain.com with the actual domain name.

# sudoedit /nginx/vhosts.d/domain_com.conf
 
filename: /nginx/vhosts.d/domain_com.conf
# $KYAULabs: domain_com.conf,v 1.1.5 2021/05/05 18:06:34 kyau Exp $

## Redirect all WWW to Non-WWW (SSL)
server {
listen *:443 ssl http2;
listen [::]:443 ssl http2;
server_name www.domain.com;

include /nginx/conf.d/vhost_defaults;

return 301 https://$domain$request_uri;
}

## domain.com
server {
listen *:443 ssl http2;
listen [::]:443 ssl http2;
server_name domain.com;

include /nginx/conf.d/vhost_defaults;

location / {
try_files $uri $uri/ @rewrite;
}

location @rewrite {
#rewrite ^/directory/$ /script.php?x=directory last;
}

#location ^~ /app/ {
#proxy_pass http://127.0.0.1:8080/;
#proxy_http_version 1.1;
#proxy_set_header Connection "upgrade";
#proxy_set_header Upgrade $http_upgrade;
#proxy_set_header X-Forwarded-For $remote_addr;
#proxy_set_header X-Forwarded-Proto $scheme;
# by defaults nginx times out connections in one minute
#proxy_read_timeout 1d;
}


include /nginx/conf.d/uri_defaults;
}

## api.domain.com
server {
listen *:443 ssl http2;
listen [::]:443 ssl http2;
server_name api.domain.com;

include /nginx/conf.d/vhost_defaults;

location / {
try_files $uri $uri/ @rewrite;
}

location @rewrite {
#rewrite ^/directory/$ /script.php?x=directory last;
}

include /nginx/conf.d/uri_defaults;
}

# vim: ft=nginx sts=4 sw=4 ts=4 noet :