Embedding PHP 7 Directly into Nginx with ngx_php7

ngx_php7 is a high-performance, non-blocking Nginx module that embeds PHP 7 directly into the web server process—similar in architecture and intent to ngx_lua. It enables inline PHP execution at various Nginx request-processing phases without relying on external processes like PHP-FPM, mod_php, or CGI gateways.

Developed as a open-source extension (GitHub: rryqszq4/ngx_php7), it targets developers seeking low-latency, stateless, and I/O-efficient PHP-driven routing, header manipulation, request inspection, and lightweight service orchestration—all within Nginx’s event loop.

Key Design Principles

  • Non-blocking by default: Asynchronous socket operations (ngx_socket_connect, ngx_socket_recv) use yield to suspend/resume coroutines without blocking workers.
  • No shared state per request: Global variables and static class properties are unsafe across requests; each invocation runs in an isolated PHP context.
  • Zero-process overhead: Eliminates inter-process communication, serialization, and process spawning inherent in traditional PHP SAPIs.
  • Minimalist integration: Does not replace PHP-FPM—it complements it. Use it for edge logic (authz, redirects, logging) and delegate heavy workloads to backend services.

System Requirements

  • Linux only (no macOS or Windows support)
  • PHP 7.0–7.4, compiled with --enable-embed
  • Nginx 1.4.7–1.17.8
  • Requires ngx_devel_kit as a dependency

Installation Options

Source Compilation

# Build PHP with embed support
wget https://www.php.net/distributions/php-7.4.33.tar.gz
tar xzf php-7.4.33.tar.gz
cd php-7.4.33
./configure --prefix=/opt/php-embed --enable-embed=shared
make -j$(nproc) && sudo make install

# Build Nginx with ngx_php7
git clone https://github.com/rryqszq4/ngx_php7.git
wget https://nginx.org/download/nginx-1.20.2.tar.gz
tar xzf nginx-1.20.2.tar.gz
cd nginx-1.20.2

export PHP_CONFIG=/opt/php-embed/bin/php-config
export PHP_INC=/opt/php-embed/include/php
export PHP_LIB=/opt/php-embed/lib

./configure \
  --prefix=/opt/nginx \
  --with-ld-opt="-Wl,-rpath,$PHP_LIB" \
  --add-module=../ngx_php7/third_party/ngx_devel_kit \
  --add-module=../ngx_php7 \
  --user=www-data --group=www-data

make -j$(nproc) && sudo make install

RPM-Based Systems (CentOS/RHEL 7+)

yum install -y https://extras.getpagespeed.com/release-el7-latest.rpm
yum install -y epel-release
yum install -y nginx-module-php7

# In /etc/nginx/nginx.conf, add before 'events {':
load_module modules/ndk_http_module.so;
load_module modules/ngx_http_php_module.so;

Docker Quickstart

git clone https://github.com/rryqszq4/ngx_php7.git
cd ngx_php7/docker
docker build -t nginx-php7 .

# Run with custom config mounted
docker run -d -p 8080:80 \
  -v $(pwd)/examples/default.conf:/etc/nginx/conf.d/default.conf \
  --name php7-server nginx-php7

Configuration Overview

The following minimal nginx.conf demonstrates core capabilities:

worker_processes auto;

events {
    worker_connections 10240;
}

http {
    include       mime.types;
    default_type  application/json;

    # Load PHP configuration
    php_ini_path /opt/php-embed/etc/php.ini;

    server {
        listen 80;
        server_name _;

        # Log request URI using embedded PHP
        location = /uri {
            content_by_php_block {
                $uri = \$_SERVER['REQUEST_URI'] ?? '';
                echo json_encode(['uri' => $uri, 'timestamp' => time()]);
            }
        }

        # Parse query string safely (replaces $_GET)
        location = /query {
            content_by_php_block {
                $params = ngx_query_args();
                http_response_code(200);
                header('Content-Type: application/json');
                echo json_encode($params);
            }
        }

        # Handle POST data (replaces $_POST)
        location = /post {
            client_max_body_size 16k;
            content_by_php_block {
                $body = ngx_post_args();
                echo json_encode([
                    'received' => !empty($body),
                    'keys' => array_keys($body)
                ]);
            }
        }

        # Non-blocking HTTP client call
        location = /stock {
            content_by_php_block {
                $sock = ngx_socket_create();
                if (yield ngx_socket_connect($sock, 'hq.sinajs.cn', 80)) {
                    $req = "GET /list=s_sh000001 HTTP/1.1\r\n" .
                           "Host: hq.sinajs.cn\r\n" .
                           "Connection: close\r\n\r\n";
                    yield ngx_socket_send($sock, $req, strlen($req));

                    $resp = '';
                    yield ngx_socket_recv($sock, $resp, 4096);
                    echo $resp;
                    yield ngx_socket_close($sock);
                } else {
                    echo "Connection failed";
                }
            }
        }

        # Set response headers dynamically
        location = /headers {
            content_by_php_block {
                ngx_header_set('X-Powered-By', 'ngx_php7/0.0.21');
                ngx_header_set('Cache-Control', 'no-cache');
                echo '{"status":"ok"}';
            }
        }
    }
}

Request Lifecycle Hooks

PHP code can be injected at every major Nginx phase:

Directive Phase Description
init_worker_by_php_block Worker startup Runs once per worker process (e.g., init connection pools)
rewrite_by_php_block Rewrite Modify URI or internal redirect before routing
access_by_php_block Access Implement auth, rate limiting, IP filtering
content_by_php_block Content Generate full response body (most common)
header_filter_by_php_block Header filter Modify outgoing response headers
log_by_php_block Log Custom logging after request completion

Core PHP APIs

Request Inspection

  • ngx_request_method() → e.g., "GET"
  • ngx_request_uri() → raw URI path
  • ngx_request_headers() → associative array of all request headers
  • ngx_request_remote_addr() → client IP (respecting X-Forwarded-For requires menual parsing)

Input Handling

  • ngx_query_args() → parsed query string (URL-decoded, no superglobals)
  • ngx_post_args() → form-encoded or multipart POST body
  • ngx_var_get('host') → fetch Nginx variable values (e.g., $host, $request_time)

Response Control

  • ngx_header_set('Content-Type', 'text/plain')
  • ngx_redirect('/login', 302)
  • ngx_exit(403) → terminate immediately with status
  • ngx_log_error(NGX_LOG_WARN, 'Custom warning')

Async I/O Primitives

  • yield ngx_sleep(2) — suspend for 2 seconds
  • $sock = ngx_socket_create(); yield ngx_socket_connect($sock, $host, $port);
  • yield ngx_socket_send($sock, $data, $len);
  • yield ngx_socket_recv($sock, $buffer, 8192);

Important Behavioral Notes

  • No persistent memory: Avoid static properties or global caches—they persist across requests and cause race conditions.
  • Blocking calls are discouraged: Native fopen(), file_get_contents(), or curl_exec() will stall the entire worker. Prefer async socket APIs or offload to upstreams.
  • PHP extensions must be thread-safe: Only extensions built with ZTS (Zend Thread Safety) are safe in this embedded context.
  • INI directives apply globally: php_ini_path loads one config per Nginx instance—not per request.

Testing & Validation

The module includes a comprehensive Test::Nginx-based test suite covering:

  • Basic script execution and output capture
  • Variable injection and scope isolation
  • Socket lifecycle (connect/send/recv/close)
  • Header, cookie, and redirect behavior
  • Error handling and logging fidelity

All tests pass under CI environments targeting Ubuntu 14.04+, GCC 4.8+, and Nginx 1.12–1.20.

Licensing

Released under the 2-clause BSD License. Copyright (c) 2016–2020 rryqszq4.

Tags: nginx PHP embedded-php async-io web-server

Posted on Mon, 29 Jun 2026 16:08:22 +0000 by nazariah