ngx_php Menjalankan PHP dari Nginx Tanpa PHP-FPM

6 min read

Disclaimer
Saya bekerja di AWS, semua opini adalah dari saya pribadi. (I work for AWS, my opinions are my own.)
ngx_php Menjalankan PHP langsung dari Nginx tanpa PHP-FPM
ngx_php Menjalankan PHP langsung dari Nginx tanpa PHP-FPM

TeknoCerdas.com – Salam cerdas untuk kita semua. Untuk menjalankan PHP pada Nginx umumnya digunakan PHP-FPM sebagai FastCGI process manager. ngx_php adalah sebuah modul Nginx untuk menjalankan PHP secara langsung. Sebuah alternatif baru dalam menjalankan PHP dengan Nginx. Dengan modul ngx_php maka pengguna dapat melakukan embed script PHP langsung di Nginx. Sehingga tanpa PHP-FPM pun kode PHP dapat berjalan pada Nginx.

Solusi yang dihadirkan oleh ngx_php sebagai modul Nginx, mirip dengan apa yang dilakukan mod_php pada Apache server. mod_php adalah cara paling populer menjalan PHP dengan Apache selama puluhan tahun. Modul ngx_php mencoba menawarkan cara yang sama pada Nginx.

Secara teori harusnya modul ngx_php pada Nginx lebih cepat dari mod_php karena penggunaan arsitektur event-driven pada Nginx dibandingkan penggunaan thread pada Apache.

Daftar Isi

Persiapan Menajalankan ngx_php

Pada artikel ini TeknoCerdas mengasumsikan bahwa anda telah memahami beberapa hal berikut.

  • Memiliki pemahaman dasar tentang Docker
  • Memiliki pemahaman dasar tentang PHP
  • Memiliki pemahaman dasar tentang Nginx

Jika anda tidak memiliki pengetahuan tentang beberapa hal diatas, tetap saja lanjutkan membaca. Karena mungkin ada informasi baru yang berguna bagi anda.

Instalasi ngx_php Lewat Docker

Ada banyak cara melakukan instalasi ngx_php namun pada artikel ini TeknoCerdas akan menggunakan Docker untuk menjalankan ngx_php. Instalasi ngx_php dilakukan dengan melakukan kompilasi Nginx dari kode sumber.

Versi yang akan digunakan.

  • OS Ubuntu 20.04
  • Nginx v1.19.2
  • PHP v7.4
  • ngx_php v0.0.24 untuk PHP 7

Buat sebuah direktori untuk pengerjaan instalasi ini. Sebagai contoh TeknoCerdas akan menggunakan nama direktori ngx_php-module.

$ mkdir ngx-php-module
$ cd ngx-php-module

Kemudian buat Dockerfile yang berisi kompilasi Nginx dengan modul ngx_php. Berikut ini adalah isi dari Dockerfile.

$ vi Dockerfile
FROM ubuntu:20.04

ARG DEBIAN_FRONTEND=noninteractive

RUN apt-get update -yqq && apt-get install -yqq software-properties-common > /dev/null
RUN LC_ALL=C.UTF-8 add-apt-repository ppa:ondrej/php > /dev/null
RUN apt-get update -yqq > /dev/null && \
    apt-get install -yqq wget git unzip libxml2-dev cmake make systemtap-sdt-dev \
                    zlibc zlib1g zlib1g-dev libpcre3 libpcre3-dev libargon2-0-dev libsodium-dev \
                    php7.4 php7.4-common php7.4-dev libphp7.4-embed php7.4-mysql nginx > /dev/null

ADD ./ ./

ENV NGINX_VERSION=1.19.2

RUN git clone -b v0.0.24 --single-branch --depth 1 https://github.com/rryqszq4/ngx_php7.git > /dev/null

RUN wget -q http://nginx.org/download/nginx-${NGINX_VERSION}.tar.gz && \
    tar -zxf nginx-${NGINX_VERSION}.tar.gz && \
    cd nginx-${NGINX_VERSION} && \
    export PHP_LIB=/usr/lib && \
    ./configure --user=www --group=www \
            --prefix=/nginx \
            --with-ld-opt="-Wl,-rpath,$PHP_LIB" \
            --add-module=/ngx_php7/third_party/ngx_devel_kit \
            --add-module=/ngx_php7 > /dev/null && \
    make > /dev/null && make install > /dev/null

CMD /nginx/sbin/nginx -c /deploy/nginx_default.conf

Pertama kita akan melakukan build sebuah docker image dari Dockerfile yang sudah dibuat sebelumnya. Image hasil build akan diberi nama ngx-php dengan tag versi 7 yang menandakan PHP 7. Base image yang digunakan adalah dari Ubuntu 20.04.

$ sudo docker build -t ngx-php:7

Setelah proses build selesai, cek daftar Docker image yang ada pada local komputer.

$ sudo docker image ls
REPOSITORY          TAG                 IMAGE ID            CREATED             SIZE
ngx-php             7                   be17ca455a9a        12 hours ago        693MB
ubuntu              20.04               9140108b62dc        11 days ago         72.9MB

Dapat terlihat bahwa image untuk ngx_php sudah berhasil dibuat.

Membuat Konfigurasi ngx_php

Setelah modul ngx_php terinstal pada Nginx akan ada beberapa directive baru yang bisa digunakan untuk menjalankan PHP. Pada contoh ini TeknoCerdas akan membuat empat routing untuk pemrosesan file PHP.

  • Routing /text akan mengembalikan sebuah pesan “Hello World!” dalam bentuk text/plain.
  • Routing /json akan mengembalikan sebuah string JSON dengan isi { "message": "Hello World!"} dalam bentuk application/json.
  • Routing /html akan mengembalikan tanggal dan jam hari ini dalam bentuk text/html.
  • Routing /info akan mengembalikan halaman hasil dari fungsi phpinfo() dalam bentuk text/html.

Membuat nginx_default.conf

Masih dalam direktori kerja yang sekarang, buat sebuah direktori baru bernama deploy.

$ mkdir deploy

Buat sebuah file nginx_default.conf untuk konfigurasi Nginx dan ngx_php dalam menjalankan PHP secara langsung.

$ vi deploy/nginx_default.conf
user www-data;
worker_cpu_affinity auto;
worker_processes  auto;
error_log /dev/stdout error;
timer_resolution 1s;
daemon off;
pid /run/nginx.pid;
pcre_jit on;

events {
    worker_connections 100000;
    multi_accept off;
}

http {
    #include       /etc/nginx/mime.types;
    access_log off;
    server_tokens off;
    msie_padding off;

    sendfile off;
    tcp_nopush off;
    tcp_nodelay on;
    keepalive_timeout 65s;
    keepalive_disable none;
    keepalive_requests 100000;

    php_ini_path /deploy/php.ini;

    server {
        listen       *:8080 backlog=65535 reuseport;

        root /;
        index  index.html;

        php_keepalive 256;

        location = /text {
            content_by_php_block {
                ngx_header_set('Content-Type', 'text/plain');
                echo 'Hello, World!';
            }
        }

        location = /json {
            content_by_php_block {
                ngx_header_set('Content-Type', 'application/json');
                echo json_encode(['message' => 'Hello, World!']);
            }
        }

        location = /html {
            content_by_php_block {
                require '/deploy/today.php';
            }
        }

        location = /info {
            content_by_php_block {
                ngx_header_set('Content-Type', 'text/html; charset=UTF8');
                phpinfo();
            }
        }
    }
}

Dapat terlihat pada contoh konfigurasi Nginx diatas. Empat routing yang disebutkan sebelumnya menggunakan directive baru yaitu content_by_php_block. Ini adalah satu diantara banyak directive yang disediakan oleh modul ngx_php.

Fungsi dari directive baru tersebut adalah untuk melakukan interpretasi atau eksekusi kode PHP yang ada dalam blok {...}. Sebagai contoh pada routing /json, pada blok tersebut dipanggil sebuah fungsi dari PHP yaitu json_encode().

Intinya kode PHP yang valid akan dapat dieksekusi oleh blok directive content_by_php_block. Fungsi ngx_header_set() adalah fungsi yang disediakan oleh ngx_php untuk melakukan modifikasi HTTP header.

Membuat php.ini

Konfigurasi ini sebenarnya opsional, jika konfigurasi ini tidak dibuat maka PHP akan menggunakan default value.

Masih pada direktori kerja yang sama, sekarang buat sebuah file bernama php.ini yang berisi konfigurasi untuk PHP runtime. Konfigurasi ini akan dibaca oleh ngx_php menggunakan directive php_ini_path.

$ vi deploy/php.ini
[PHP]
engine = On
short_open_tag = Off
precision = 14
output_buffering = 4096
zlib.output_compression = Off
implicit_flush = Off
unserialize_callback_func =
serialize_precision = -1
disable_functions =
disable_classes =
realpath_cache_size = 4096k
realpath_cache_ttl = 600
zend.enable_gc = On
expose_php = Off
max_execution_time = 30
max_input_time = 60
memory_limit = 128M
error_reporting = E_ALL & ~E_DEPRECATED & ~E_STRICT
display_errors = Off
display_startup_errors = Off
log_errors = On
log_errors_max_len = 1024
ignore_repeated_errors = Off
ignore_repeated_source = Off
report_memleaks = On
html_errors = On
variables_order = "GPCS"
request_order = "GP"
register_argc_argv = Off
auto_globals_jit = On
post_max_size = 8M
auto_prepend_file =
auto_append_file =
default_mimetype = "text/html"
default_charset = "UTF-8"
doc_root =
user_dir =
enable_dl = Off
 cgi.fix_pathinfo=0
file_uploads = On
upload_max_filesize = 2M
max_file_uploads = 20
allow_url_fopen = On
allow_url_include = Off
default_socket_timeout = 60

[CLI Server]
cli_server.color = On

[Date]
date.timezone = UTC

[filter]
[iconv]
[intl]
[sqlite3]
[Pcre]
[Pdo]
[Pdo_mysql]
pdo_mysql.default_socket=

[Phar]
[mail function]
SMTP = localhost
smtp_port = 25
mail.add_x_header = On

[ODBC]
odbc.allow_persistent = On
odbc.check_persistent = On
odbc.max_persistent = -1
odbc.max_links = -1
odbc.defaultlrl = 4096
odbc.defaultbinmode = 1

[Interbase]
ibase.allow_persistent = 1
ibase.max_persistent = -1
ibase.max_links = -1
ibase.timestampformat = "%Y-%m-%d %H:%M:%S"
ibase.dateformat = "%Y-%m-%d"
ibase.timeformat = "%H:%M:%S"
[MySQLi]
mysqli.max_persistent = -1
mysqli.allow_persistent = On
mysqli.max_links = -1
mysqli.cache_size = 2000
mysqli.default_port = 3306
mysqli.default_socket =
mysqli.default_host =
mysqli.default_user =
mysqli.default_pw =
mysqli.reconnect = Off
[mysqlnd]
mysqlnd.collect_statistics = Off
mysqlnd.collect_memory_statistics = Off
mysqlnd.mempool_default_size = 32768

[OCI8]

[PostgreSQL]
pgsql.allow_persistent = On
pgsql.auto_reset_persistent = Off
pgsql.max_persistent = -1
pgsql.max_links = -1
pgsql.ignore_notice = 0
pgsql.log_notice = 0
[bcmath]
bcmath.scale = 0

[browscap]

[Session]
session.save_handler = files
session.use_strict_mode = 0
session.use_cookies = 1
session.use_only_cookies = 1
session.name = PHPSESSID
session.auto_start = 0
session.cookie_lifetime = 0
session.cookie_path = /
session.cookie_domain =
session.cookie_httponly =
session.serialize_handler = php
session.gc_probability = 0
session.gc_divisor = 1000
session.gc_maxlifetime = 1440
session.referer_check =
session.cache_limiter = nocache
session.cache_expire = 180
session.use_trans_sid = 0
session.sid_length = 26
session.trans_sid_tags = "a=href,area=href,frame=src,form="
session.sid_bits_per_character = 5

[Assertion]
zend.assertions = -1

[COM]
[mbstring]
[gd]
[exif]

[Tidy]
tidy.clean_output = Off

[soap]
soap.wsdl_cache_enabled=1
soap.wsdl_cache_dir="/tmp"
soap.wsdl_cache_ttl=86400
soap.wsdl_cache_limit = 5

[sysvshm]
[ldap]
ldap.max_links = -1

[dba]

[opcache]
opcache.enable=1
opcache.enable_cli=1
opcache.validate_timestamps=0
opcache.save_comments=0
opcache.enable_file_override=1
opcache.huge_code_pages=1

[curl]
[openssl]

Membuat file today.php

File today.php digunakan untuk mendemonstrasikan penggunakan include atau require pada directive content_php_by_block. Dimana pada routing /html ngx_php akan memproses file today.php menggunakan require.

File akan menampilkan sebuah halaman HTML yang berisi tanggal dan jam hari ini yang digenerate secara dinamis oleh PHP.

$ vi deploy/today.php
<?php 
ngx_header_set('Content-Type', 'text/html; charset=UTF8');
?><!DOCTYPE html>
<html>
<body>
    <h2>Today is <?= date('Y-m-d H:i:s') ?></h2>
</body>
</html>

Melakukan Tes ngx_php

Pastikan masih berada pada direktori kerja yang sama. Direktori deploy/ akan dimount ke dalam container dengan lokasi /deploy/.

$ sudo docker run -v $(pwd)/deploy/:/deploy/ --rm --name ngx-test -d -p 8080:8080 ngx-php:7

Pastikan container telah berjalan dengan perintah ps.

$ sudo docker ps
CONTAINER ID        IMAGE               COMMAND                  CREATED             STATUS              PORTS                    NAMES
775bbaa3c361        ngx-php:7           "/bin/sh -c '/nginx/…"   47 minutes ago      Up 40 minutes       0.0.0.0:8080->8080/tcp   ngx-test

Sekarang container yang berisi Nginx dengan modul ngx_php sudah berjalan. Lakukan tes satu-per-satu untuk setiap routing yang dibuat. Dimulai dengan routing /text.

$ curl -s -D /dev/stdout localhost:8080/text
HTTP/1.1 200 OK
Server: nginx
Date: Wed, 07 Oct 2020 02:48:49 GMT
Content-Type: text/plain
Content-Length: 13
Connection: keep-alive

Hello, World!

Hasilnya sesuai yang diinginkan. HTTP header Content-Type dan HTTP body sesusai.

Lanjut ke routing yang kedua yaitu /json dimana harusnya Content-Type berisi application/json dan HTTP body berisi JSON object.

$ curl -s -D /dev/stdout localhost:8080/json
HTTP/1.1 200 OK
Server: nginx
Date: Wed, 07 Oct 2020 02:51:18 GMT
Content-Type: application/json
Content-Length: 27
Connection: keep-alive

{"message":"Hello, World!"}

Bagus. Hasilnya juga sesuai. Berikutnya adalah routing /html. Content-type yang dikembalikan harusya adalah text/html dan berisi halaman HTML dari tanggal dan jam sekarang.

$ curl -s -D /dev/stdout localhost:8080/html
HTTP/1.1 200 OK
Server: nginx
Date: Wed, 07 Oct 2020 02:53:27 GMT
Content-Type: text/html; charset=UTF8
Content-Length: 88
Connection: keep-alive

<!DOCTYPE html>
<html>
<body>
    <h2>Today is 2020-10-07 02:53:27</h2>
</body>
</html>

Dapat dilihat bahwa hasilnya sesuai dengan apa yang ditulis pada file today.php. Routing terakhir yaitu /info berisi tentang informasi PHP yang digunakan. Pada routing ini gunakan web browser untuk mendapatkan tampilan yang mudah dilihat.

Buka halaman localhost:8080/info pada web browser, tampilan harusnya mirip dengan gambar dibawah.

Menjalankan phpinfo dari ngx_php
Menjalankan phpinfo() dari ngx_php

Kekurangan ngx_php

Menurut dokumentasi resmi dari ngx_php terdapat beberapa kekurangan dibandingkan dengan metode tradisional dalam menjalankan PHP.

  • Global variabel tidak bekerja semestinya ($_GET, $_POST dll)
  • Static variabel pada class tidak aman digunakan.
  • Kode dengan desain Singleton tidak aman digunakan.
  • I/O pada PHP berfungsi normal tapi akan memperlambat Nginx.

Jika melakukan pengembangan menggunakan ngx_php maka harus memikirkan dari awal kekurangan diatas. Sebagai contoh kode yang bergantung pada $_GET harus diubah atau memasang bootstrap file khusus untuk melakukan parsing mandiri query string. Hasilnya gunakan untuk melakukan override variabel $_GET.