Web Server #
Menjalankan PHP di web server adalah topik yang terdengar sederhana tapi penuh nuansa — pilihan konfigurasi yang salah bisa menyebabkan performa buruk, celah keamanan, atau perilaku yang tidak terduga di production. PHP bisa berjalan dalam beberapa mode: sebagai modul Apache (mod_php), melalui FastCGI (PHP-FPM) yang terhubung ke Nginx atau Apache, atau dengan built-in server PHP untuk development. Artikel ini membahas semua mode ini secara mendalam — cara kerja PHP-FPM dan mengapa ia menjadi pilihan standar production modern, konfigurasi Nginx dan Apache yang benar untuk PHP, pengaturan php.ini yang kritis, tuning OPcache untuk performa optimal, dan praktik deployment yang aman.
Arsitektur: Bagaimana PHP Dieksekusi #
PHP bisa dieksekusi dalam beberapa cara yang memiliki implikasi performa dan arsitektur yang sangat berbeda:
flowchart TD
Internet[Request dari\nInternet] --> WS
subgraph WS[Web Server Layer]
Nginx[Nginx\nApache]
end
WS --> M1[Mode 1: mod_php\nPHP sebagai modul Apache\nSatu proses per request]
WS --> M2[Mode 2: PHP-FPM\nFastCGI Process Manager\nPool of workers]
WS --> M3[Mode 3: Built-in Server\nHanya untuk development]
M2 --> FPM[PHP-FPM Pool\nWorker 1\nWorker 2\nWorker 3\n...\nWorker N]
FPM --> Cache[OPcache\nCompiled bytecode\nBersama antar worker]
style M2 fill:#dcfce7,stroke:#16a34a
style M3 fill:#fee2e2
style Cache fill:#fef9c3PHP-FPM (FastCGI Process Manager) adalah arsitektur yang direkomendasikan untuk production. PHP-FPM mengelola pool of worker processes yang menangani request PHP, terpisah dari web server. Nginx atau Apache berfungsi sebagai reverse proxy yang meneruskan request PHP ke PHP-FPM via FastCGI protocol.
PHP Built-in Development Server #
PHP menyertakan web server sederhana yang cocok untuk development lokal — tanpa perlu instalasi Nginx atau Apache:
# Jalankan server di port 8000, root di direktori saat ini
php -S localhost:8000
# Dengan document root spesifik
php -S localhost:8000 -t /path/to/public
# Dengan router script — untuk SPA atau framework
php -S localhost:8000 index.php
# Bind ke semua interface (bisa diakses dari mesin lain di jaringan)
php -S 0.0.0.0:8000 -t public/
Built-in server PHP hanya untuk development. Ia single-threaded (hanya bisa tangani satu request pada satu waktu), tidak memiliki fitur keamanan production, dan tidak dirancang untuk menangani traffic nyata. Jangan pernah gunakan di production atau staging yang diakses publik.
Router Script untuk Framework #
Framework PHP seperti Laravel dan Symfony menggunakan satu entry point (public/index.php) untuk semua request. Untuk membuat built-in server bekerja dengan pola ini:
<?php
// router.php — script yang memutuskan request mana yang di-handle PHP
// dan mana yang di-serve sebagai file statis
$uri = urldecode(parse_url($_SERVER['REQUEST_URI'], PHP_URL_PATH));
// Jika file statis ada, serve langsung
if ($uri !== '/' && file_exists(__DIR__ . '/public' . $uri)) {
return false; // built-in server handle sebagai file statis
}
// Semua request lain diteruskan ke index.php
$_SERVER['SCRIPT_FILENAME'] = __DIR__ . '/public/index.php';
require __DIR__ . '/public/index.php';
php -S localhost:8000 router.php
PHP-FPM — Konfigurasi dan Tuning #
PHP-FPM mengelola pool of worker processes. Konfigurasi pool yang tepat sangat mempengaruhi performa dan stabilitas aplikasi.
Konfigurasi Pool Dasar #
; /etc/php/8.3/fpm/pool.d/www.conf
[www]
; User dan group yang menjalankan worker
user = www-data
group = www-data
; Socket Unix (lebih cepat dari TCP untuk komunikasi lokal)
listen = /run/php/php8.3-fpm.sock
listen.owner = www-data
listen.group = www-data
listen.mode = 0660
; Atau gunakan TCP jika PHP-FPM di server berbeda dari Nginx
; listen = 127.0.0.1:9000
; Manajemen proses
pm = dynamic ; static, dynamic, atau ondemand
; dynamic — jumlah worker bervariasi sesuai beban
pm.max_children = 50 ; maksimum worker
pm.start_servers = 5 ; worker saat start
pm.min_spare_servers = 5 ; minimum worker idle
pm.max_spare_servers = 35 ; maksimum worker idle
; Restart worker setelah N request (mencegah memory leak perlahan)
pm.max_requests = 500
; Timeout request (detik) — kill worker jika terlalu lama
request_terminate_timeout = 60
; Log request yang lambat
slowlog = /var/log/php8.3-fpm-slow.log
request_slowlog_timeout = 5s ; log jika > 5 detik
; Environment variables yang tersedia untuk script PHP
env[PATH] = /usr/local/bin:/usr/bin:/bin
env[TMPDIR] = /tmp
env[APP_ENV] = production
; php.ini overrides khusus pool ini
php_admin_value[error_log] = /var/log/php/error.log
php_admin_flag[log_errors] = on
php_admin_value[memory_limit] = 256M
Menghitung pm.max_children
#
Rumus sederhana untuk menentukan jumlah worker optimal:
pm.max_children = (RAM tersedia untuk PHP) / (rata-rata memori per proses PHP)
Contoh:
- Server RAM total: 4 GB
- RAM untuk OS dan Nginx: ~500 MB
- RAM tersedia untuk PHP: ~3.5 GB
- Rata-rata PHP process: ~50 MB (cek dengan: ps aux | grep php-fpm)
- pm.max_children = 3500 / 50 = 70
# Cek rata-rata memori per PHP-FPM worker
ps aux | grep php-fpm | awk '{sum += $6; count++} END {print sum/count/1024 " MB"}'
# Cek status pool PHP-FPM secara live
# Aktifkan pm.status_path = /status di pool config
curl http://127.0.0.1/status?full
Konfigurasi Nginx untuk PHP #
Nginx adalah web server paling umum dikombinasikan dengan PHP-FPM di production.
Konfigurasi Virtual Host Dasar #
# /etc/nginx/sites-available/myapp.conf
server {
listen 80;
server_name myapp.example.com www.myapp.example.com;
# Root direktori — selalu tunjuk ke folder public/
root /var/www/myapp/public;
index index.php index.html;
# Log
access_log /var/log/nginx/myapp-access.log;
error_log /var/log/nginx/myapp-error.log;
# Maksimum ukuran upload
client_max_body_size 64M;
# Timeout
fastcgi_read_timeout 60;
# Lokasi untuk semua request
location / {
# Coba serve file statis dulu, fallback ke index.php
try_files $uri $uri/ /index.php?$query_string;
}
# Handle PHP — teruskan ke PHP-FPM
location ~ \.php$ {
# Keamanan: tolak request ke file PHP yang tidak ada
try_files $uri =404;
# Pisah script name dari path info
fastcgi_split_path_info ^(.+\.php)(/.+)$;
# Teruskan ke PHP-FPM via Unix socket
fastcgi_pass unix:/run/php/php8.3-fpm.sock;
fastcgi_index index.php;
# Parameter FastCGI standar
include fastcgi_params;
fastcgi_param SCRIPT_FILENAME $realpath_root$fastcgi_script_name;
fastcgi_param PATH_INFO $fastcgi_path_info;
# Header keamanan
fastcgi_param HTTPS on; # jika di belakang HTTPS proxy
}
# Blokir akses ke file sensitif
location ~ /\. {
deny all; # sembunyikan .env, .git, dll.
}
location ~ \.(env|log|sql|bak)$ {
deny all;
}
# Cache file statis
location ~* \.(css|js|png|jpg|jpeg|gif|ico|svg|woff|woff2|ttf)$ {
expires 30d;
add_header Cache-Control "public, no-transform";
access_log off;
}
}
Konfigurasi dengan HTTPS (Let’s Encrypt) #
server {
listen 80;
server_name myapp.example.com;
# Redirect semua HTTP ke HTTPS
return 301 https://$host$request_uri;
}
server {
listen 443 ssl http2;
server_name myapp.example.com;
# Sertifikat Let's Encrypt
ssl_certificate /etc/letsencrypt/live/myapp.example.com/fullchain.pem;
ssl_certificate_key /etc/letsencrypt/live/myapp.example.com/privkey.pem;
# Konfigurasi TLS modern
ssl_protocols TLSv1.2 TLSv1.3;
ssl_ciphers ECDHE-ECDSA-AES128-GCM-SHA256:ECDHE-RSA-AES128-GCM-SHA256:...;
ssl_prefer_server_ciphers off;
# HSTS — paksa browser gunakan HTTPS selama 1 tahun
add_header Strict-Transport-Security "max-age=31536000" always;
# Header keamanan lainnya
add_header X-Content-Type-Options "nosniff" always;
add_header X-Frame-Options "SAMEORIGIN" always;
add_header X-XSS-Protection "1; mode=block" always;
add_header Referrer-Policy "strict-origin-when-cross-origin" always;
add_header Permissions-Policy "camera=(), microphone=(), geolocation=()" always;
root /var/www/myapp/public;
index index.php;
location / {
try_files $uri $uri/ /index.php?$query_string;
}
location ~ \.php$ {
try_files $uri =404;
fastcgi_pass unix:/run/php/php8.3-fpm.sock;
fastcgi_index index.php;
include fastcgi_params;
fastcgi_param SCRIPT_FILENAME $realpath_root$fastcgi_script_name;
fastcgi_param HTTPS on;
}
location ~ /\. { deny all; }
}
# Install Certbot dan dapatkan sertifikat
sudo apt install certbot python3-certbot-nginx
sudo certbot --nginx -d myapp.example.com
# Auto-renewal (cron)
sudo certbot renew --dry-run
# Certbot otomatis membuat cron/systemd timer untuk renewal
Konfigurasi Apache untuk PHP #
Apache bisa menjalankan PHP via mod_php (lebih mudah tapi tidak fleksibel) atau via PHP-FPM (lebih baik untuk production):
Apache dengan PHP-FPM (Direkomendasikan) #
# /etc/apache2/sites-available/myapp.conf
<VirtualHost *:80>
ServerName myapp.example.com
DocumentRoot /var/www/myapp/public
# Aktifkan mod_proxy_fcgi dan PHP-FPM
<FilesMatch \.php$>
SetHandler "proxy:unix:/run/php/php8.3-fpm.sock|fcgi://localhost"
</FilesMatch>
# URL rewriting untuk framework
<Directory /var/www/myapp/public>
Options -Indexes +FollowSymLinks
AllowOverride All
Require all granted
</Directory>
# Sembunyikan .env dan file sensitif
<FilesMatch "\.(env|log|sql|bak)$">
Require all denied
</FilesMatch>
<DirectoryMatch "\.git">
Require all denied
</DirectoryMatch>
ErrorLog ${APACHE_LOG_DIR}/myapp-error.log
CustomLog ${APACHE_LOG_DIR}/myapp-access.log combined
</VirtualHost>
# Aktifkan modul yang diperlukan
sudo a2enmod proxy_fcgi setenvif rewrite headers
sudo a2ensite myapp.conf
sudo systemctl reload apache2
.htaccess untuk Framework PHP
#
# /var/www/myapp/public/.htaccess
Options -MultiViews -Indexes
RewriteEngine On
# Redirect ke HTTPS
RewriteCond %{HTTPS} off
RewriteRule ^ https://%{HTTP_HOST}%{REQUEST_URI} [L,R=301]
# Arahkan semua request ke index.php kecuali file/direktori yang ada
RewriteCond %{REQUEST_FILENAME} !-f
RewriteCond %{REQUEST_FILENAME} !-d
RewriteRule ^ index.php [QSA,L]
# Sembunyikan informasi server
Header unset X-Powered-By
ServerSignature Off
Konfigurasi php.ini untuk Production
#
; /etc/php/8.3/fpm/php.ini
;;; ERROR HANDLING ;;;
; Di production: MATIKAN display_errors
display_errors = Off
display_startup_errors = Off
; Log semua error ke file
log_errors = On
error_log = /var/log/php/error.log
; Report semua error tapi tidak tampilkan
error_reporting = E_ALL
;;; PERFORMA ;;;
; Batas memori per request
memory_limit = 256M
; Maksimum waktu eksekusi (detik)
max_execution_time = 30
; Maksimum waktu input parsing (upload, dll.)
max_input_time = 60
;;; UPLOAD ;;;
; Ukuran maksimum file yang bisa diupload
upload_max_filesize = 64M
post_max_size = 64M
; Maksimum jumlah file dalam satu upload
max_file_uploads = 20
;;; SESSION ;;;
session.cookie_httponly = On ; cegah akses session cookie dari JavaScript
session.cookie_secure = On ; cookie hanya lewat HTTPS
session.cookie_samesite = Lax ; cegah CSRF
session.use_strict_mode = On ; tolak session ID yang tidak dikenali
session.gc_maxlifetime = 1440 ; session expired setelah 24 menit idle
;;; TIMEZONE ;;;
date.timezone = Asia/Jakarta
;;; KEAMANAN ;;;
; Sembunyikan versi PHP dari header
expose_php = Off
; Nonaktifkan fungsi berbahaya
disable_functions = exec,passthru,shell_exec,system,proc_open,popen,curl_exec,curl_multi_exec,parse_ini_file,show_source
;;; REALPATH CACHE ;;;
; Percepat resolusi path (sangat berguna untuk framework)
realpath_cache_size = 4096K
realpath_cache_ttl = 600
OPcache — Akselerator PHP #
OPcache menyimpan bytecode PHP yang sudah dikompilasi di memori — setiap request tidak perlu parse dan kompilasi ulang file PHP dari awal. Ini adalah peningkatan performa paling signifikan yang bisa dilakukan untuk PHP.
flowchart LR
R[Request] --> C{OPcache\nhit?}
C -- Ya --> E[Execute\nbytecode]
C -- Tidak --> P[Parse PHP\nsource]
P --> K[Kompilasi ke\nbytecode]
K --> S[Simpan di\nOPcache]
S --> E
E --> Resp[Response]
style C fill:#fef9c3
style S fill:#dcfce7; Konfigurasi OPcache yang direkomendasikan untuk production
[opcache]
opcache.enable = 1
opcache.enable_cli = 0 ; matikan untuk CLI
; Ukuran memory untuk menyimpan bytecode
opcache.memory_consumption = 256 ; MB — sesuaikan dengan ukuran aplikasi
; Ukuran memory untuk string yang diinternalisasi
opcache.interned_strings_buffer = 16 ; MB
; Maksimum file yang bisa di-cache
opcache.max_accelerated_files = 20000 ; lebih dari jumlah file PHP aplikasi
; Periksa timestamp file untuk deteksi perubahan
; Di production: matikan untuk performa maksimal
; Di development: nyalakan agar perubahan langsung terdeteksi
opcache.validate_timestamps = 0 ; matikan di production
; Interval pengecekan timestamp (detik) — jika validate_timestamps=1
opcache.revalidate_freq = 0
; Kompilasi file saat startup (preloading — PHP 7.4+)
; opcache.preload = /var/www/myapp/preload.php
; opcache.preload_user = www-data
; Agresivitas optimasi (0-3, lebih tinggi = lebih lambat compile tapi lebih cepat run)
opcache.optimization_level = 0x7FFEBFFF ; semua optimasi
; JIT — Just-In-Time compilation (PHP 8.0+)
opcache.jit = tracing ; 'tracing' untuk web, 'function' untuk CLI
opcache.jit_buffer_size = 64M
Invalidasi OPcache Saat Deploy #
Karena validate_timestamps=0, OPcache tidak akan otomatis mendeteksi perubahan file setelah deploy. Kamu harus invalidasi manual:
<?php
// opcache_reset.php — jalankan setelah deploy
if (function_exists('opcache_reset')) {
opcache_reset();
echo "OPcache berhasil di-reset\n";
} else {
echo "OPcache tidak aktif\n";
}
# Atau via CLI
php -r "opcache_reset();"
# Atau via curl ke endpoint yang dilindungi
curl -X POST https://myapp.example.com/opcache-reset \
-H "Authorization: Bearer deploy-secret-token"
Preloading — PHP 7.4+ #
Preloading memuat file PHP ke memory saat PHP-FPM start — semua worker langsung punya bytecode siap tanpa parse apapun:
<?php
// preload.php — daftar file yang di-preload saat PHP-FPM start
// Hanya file yang selalu dibutuhkan di setiap request
$files = [
// Core framework
__DIR__ . '/vendor/autoload.php',
__DIR__ . '/vendor/psr/http-message/src/RequestInterface.php',
__DIR__ . '/vendor/psr/log/Psr/Log/LoggerInterface.php',
// ... file-file yang paling sering digunakan
];
foreach ($files as $file) {
if (file_exists($file)) {
opcache_compile_file($file);
}
}
; php.ini
opcache.preload = /var/www/myapp/preload.php
opcache.preload_user = www-data
Deployment yang Aman #
Struktur Direktori yang Aman #
/var/www/myapp/
├── public/ ← document root Nginx/Apache (satu-satunya yang accessible)
│ ├── index.php
│ ├── css/
│ ├── js/
│ └── uploads/
├── src/ ← kode sumber (tidak accessible dari web)
├── config/ ← konfigurasi (tidak accessible)
├── storage/ ← log, cache, session (tidak accessible)
├── vendor/ ← dependensi Composer (tidak accessible)
└── .env ← kredensial (tidak accessible, tidak di-commit ke Git)
Pastikan document root Nginx/Apache hanya menunjuk ke folderpublic/, bukan root project. Jika root project yang dijadikan document root, semua file.env,vendor/,config/, dan kode sumber bisa diakses publik — termasuk semua password dan API key.
Checklist Deployment Production #
SEBELUM DEPLOY:
□ composer install --no-dev --optimize-autoloader
□ php artisan config:cache (Laravel) atau cache config framework
□ php artisan route:cache
□ php artisan view:cache
□ Set APP_ENV=production dan APP_DEBUG=false
KONFIGURASI SERVER:
□ document root menunjuk ke public/ (bukan root project)
□ display_errors = Off di php.ini
□ expose_php = Off di php.ini
□ Blokir akses ke .env, .git, vendor/ dari web
□ HTTPS aktif dengan sertifikat valid
□ Header keamanan (HSTS, X-Content-Type-Options, dll.) terpasang
OPCACHE:
□ opcache.enable = 1
□ opcache.validate_timestamps = 0
□ Script reset OPcache setelah deploy
MONITORING:
□ PHP error log aktif dan dimonitor
□ PHP-FPM slow log aktif (request_slowlog_timeout = 5s)
□ Alert untuk error rate yang tinggi
Zero-Downtime Deployment #
#!/bin/bash
# deploy.sh — deployment tanpa downtime dengan symlink pattern
APP_DIR="/var/www/myapp"
RELEASE_DIR="/var/www/releases/$(date +%Y%m%d%H%M%S)"
# 1. Buat direktori release baru
mkdir -p $RELEASE_DIR
# 2. Clone/copy kode ke release dir
git clone https://github.com/myteam/myapp.git $RELEASE_DIR
# atau: rsync -az --exclude='.git' ./ $RELEASE_DIR/
# 3. Install dependensi di release baru
cd $RELEASE_DIR
composer install --no-dev --optimize-autoloader --no-interaction
# 4. Symlink file yang di-share antar release
ln -nfs $APP_DIR/shared/.env $RELEASE_DIR/.env
ln -nfs $APP_DIR/shared/storage $RELEASE_DIR/storage
# 5. Build (jika perlu)
# npm run build, dll.
# 6. Cache warming
php artisan config:cache
php artisan route:cache
# 7. Atomically switch current symlink
# ln -nfs = tidak follow existing symlink, atomic di Linux
ln -nfs $RELEASE_DIR $APP_DIR/current
# 8. Reload PHP-FPM (graceful — tidak drop existing connections)
sudo systemctl reload php8.3-fpm
# 9. Reset OPcache
php -r "opcache_reset();"
# 10. Hapus release lama (simpan 5 terakhir)
ls -dt /var/www/releases/*/ | tail -n +6 | xargs rm -rf
echo "Deploy selesai: $RELEASE_DIR"
Monitoring Performa #
# Lihat status PHP-FPM (aktifkan pm.status_path = /fpm-status di pool config)
curl http://127.0.0.1/fpm-status
# Output:
# pool: www
# process manager: dynamic
# start time: 15/Mar/2024:10:00:00 +0700
# accepted conn: 12847
# listen queue: 0
# max listen queue: 0
# listen queue len: 511
# idle processes: 8
# active processes: 2
# total processes: 10
# max active processes: 25
# max children reached: 0
# slow requests: 3
# Monitor PHP-FPM log
tail -f /var/log/php8.3-fpm-slow.log
# Cek memory usage per worker
watch -n 2 'ps aux | grep php-fpm | grep -v grep | awk "{print \$6/1024 \" MB\", \$11}"'
# Lihat OPcache status dari PHP
php -r "var_dump(opcache_get_status());"
Ringkasan #
- PHP-FPM adalah arsitektur production standar — mengelola pool of worker processes yang bisa di-tuning sesuai kebutuhan. Gunakan Unix socket (bukan TCP) untuk komunikasi Nginx ↔ PHP-FPM di mesin yang sama, lebih cepat dan tidak ada overhead network.
- Document root harus folder
public/— bukan root project. Ini adalah aturan keamanan paling mendasar yang mencegah akses ke.env, kode sumber, dan dependensi.- OPcache adalah akselerasi terbesar — aktifkan dengan
validate_timestamps=0di production dan script reset OPcache setelah setiap deploy.display_errors = Offdi production — error harus di-log ke file, bukan ditampilkan ke browser. Menampilkan error mengekspos struktur aplikasi dan informasi sensitif.expose_php = Offmenghapus headerX-Powered-By: PHP/x.x.xyang memberitahu attacker versi PHP yang digunakan.- Zero-downtime deployment dengan pola symlink — deploy ke direktori release baru, switch symlink secara atomic, reload PHP-FPM (graceful), hapus release lama.
- PHP-FPM
pm.max_childrendihitung dari RAM tersedia dibagi rata-rata memori per worker. Terlalu kecil menyebabkan antrian; terlalu besar menyebabkan swapping.- Header keamanan HTTP (HSTS, X-Content-Type-Options, X-Frame-Options) harus dipasang di Nginx/Apache — bukan di PHP — agar teraplikasi ke semua response termasuk file statis.