Running PHP Under Apache: mod_php, FPM, and Rewrite Rules
Configure Apache virtual hosts to serve a PHP application, choose between mod_php and proxying to PHP-FPM, write mod_rewrite rules for front-controller routing, and evaluate the trade-offs of .htaccess versus main vhost configuration.
Apache HTTP Server remains an extremely common way to serve PHP, especially on shared hosting and in codebases that predate the recent shift toward Nginx + PHP-FPM. Unlike Nginx, Apache can run PHP as an embedded module directly inside its own worker processes — a fundamentally different architecture with different performance and operational trade-offs. Apache also has its own long-standing mechanism for per-directory configuration overrides, .htaccess, which is both one of the most convenient features in shared hosting and one of the most commonly misused in performance-sensitive production deployments. This lesson covers how to stand up a PHP virtual host under Apache, the mod_php-versus-FPM decision, how to write clean URL rewriting for a front-controller framework, and when .htaccess helps versus hurts.
Virtual Hosts and Document Root
An Apache virtual host (<VirtualHost> block) lets a single Apache installation serve multiple distinct sites, each identified by the ServerName (and optional ServerAlias) directive and each pointing at its own DocumentRoot. Just as with Nginx, the DocumentRoot for a PHP application should point at the framework's public/ directory, not the project root — this is not an Apache-specific convention, it's a general security principle: only files meant to be publicly downloadable should live inside the directory Apache is configured to serve from.
Each virtual host typically lives in its own file under /etc/apache2/sites-available/ (Debian/Ubuntu) and is activated with a2ensite sitename.conf && systemctl reload apache2, or is defined directly inside httpd.conf on other distributions. A <Directory> block nested inside (or referencing) the document root controls what's permitted within it — most importantly AllowOverride, which determines whether .htaccess files inside that directory are even consulted at all, and Require all granted, which actually permits Apache to serve files from that path under Apache 2.4's access-control syntax.
A single Apache server commonly hosts several virtual hosts side by side — a production API, a staging copy, and a marketing site, for instance — each with a distinct ServerName, DocumentRoot, and PHP handling configuration, all resolved by Apache's name-based virtual hosting using the Host header of the incoming request.
• <VirtualHost> + ServerName + DocumentRoot define one site on a shared Apache instance. • DocumentRoot should point at public/, mirroring the Nginx best practice, to keep source and config out of the web-reachable tree. • <Directory> blocks control access and override permissions for a specific filesystem path. • Enable a site with a2ensite and reload Apache; sites are matched by the request's Host header.
mod_php Versus Proxying to PHP-FPM
mod_php embeds the PHP interpreter directly inside each Apache worker process, using Apache's prefork MPM (Multi-Processing Module) where each request is handled by its own dedicated process. This is the simplest possible setup — install libapache2-mod-php, and PHP just works with zero additional wiring — but it comes with real costs: every Apache worker process, even ones only ever serving a static image, carries the full memory overhead of an embedded PHP interpreter, and prefork MPM (required by mod_php) cannot use Apache's more memory-efficient threaded MPMs (worker, event), which limits how many concurrent connections a given amount of RAM can support.
The modern alternative is to run Apache with the event MPM and proxy PHP requests to a separate PHP-FPM pool via mod_proxy_fcgi, exactly the same FastCGI protocol Nginx uses. This decouples 'serving HTTP connections' from 'running PHP', letting Apache handle thousands of lightweight, mostly-idle connections (useful for slow clients, long-lived connections, or WebSocket-adjacent workloads) with its efficient event-driven MPM, while PHP execution happens in a separately tunable, separately scalable FPM pool — the same architecture Nginx uses, just with Apache instead as the front door.
For a new production deployment, PHP-FPM behind Apache (or Nginx) via FastCGI is generally the better default: it matches production behavior most closely to Nginx setups, supports independent scaling and process-management tuning of PHP itself, and avoids tying PHP's fate to Apache's own MPM choice. mod_php remains reasonable for very small, low-traffic sites or environments (some shared hosts, some legacy internal tools) where the operational simplicity outweighs the efficiency gap.
• mod_php: PHP embedded in every Apache worker; simplest setup, requires prefork MPM, higher memory overhead per connection. • PHP-FPM via mod_proxy_fcgi: Apache uses efficient event/worker MPM, PHP runs in a separately managed FPM pool. • FPM decouples HTTP connection handling from PHP execution — the same architectural split Nginx always uses. • New deployments generally prefer FPM; mod_php remains acceptable for small, low-traffic, simplicity-first setups.
mod_rewrite and Front-Controller Routing
mod_rewrite is Apache's URL-rewriting engine, and it is what makes 'pretty URLs' possible — turning a URL like /products/42 into a request Apache actually routes to index.php with the original path available for the application's own router to parse, instead of requiring every URL to literally correspond to a file on disk. This is the same architectural pattern as Nginx's try_files fallback, expressed through Apache's own rule syntax instead.
A typical front-controller rewrite rule set, usually inside an <IfModule mod_rewrite.c> block, first checks whether the request corresponds to a real file or directory with RewriteCond %{REQUEST_FILENAME} !-f and RewriteCond %{REQUEST_FILENAME} !-d — if either condition is true (the path is a real file or directory), no rewriting happens and Apache serves it directly. If both conditions are false (nothing real exists at that path), RewriteRule ^ index.php [QSA,L] sends the request to index.php, with QSA (Query String Append) preserving any query string and L (Last) telling Apache this is the final rule to apply for this request.
mod_rewrite must be enabled (a2enmod rewrite on Debian/Ubuntu) and, critically, the containing <Directory> block must permit .htaccess overrides of the FileInfo context (or the rules must live directly in the vhost, discussed next) for rewrite rules placed in .htaccess to have any effect at all — a very common 'my rewrite rules don't do anything' bug is AllowOverride None silently causing Apache to ignore an otherwise-correct .htaccess file entirely.
The .htaccess Trade-off: Convenience Versus Performance
.htaccess files let you place configuration — rewrite rules, custom headers, access restrictions, even PHP ini overrides in some setups — directly inside a directory in the served tree, without editing Apache's central configuration or reloading the server; the change takes effect on the very next request. This is enormously convenient on shared hosting, where a developer may not even have access to edit the main Apache config, and for handing a client control over specific overrides without giving them server access.
The cost is performance: when AllowOverride is anything other than None for a directory, Apache must check for the existence of a .htaccess file in that directory and in every parent directory up to the document root, on every single request, because .htaccess files can change between requests and Apache has no way to know in advance whether one exists without looking. For a deep directory structure or high request volume, this adds a measurable, cumulative filesystem-stat cost to every request — one that a single centrally-loaded configuration file, read once at Apache startup, simply doesn't pay.
The production-oriented alternative is AllowOverride None set at the document root (or globally), which tells Apache to never even look for .htaccess files, combined with moving whatever rules were in .htaccess — rewrite rules, headers, access control — directly into the vhost's own <Directory> block, which is loaded once and never re-checked per request. This requires a config reload to take effect on any future change, trading the shared-hosting convenience of instant, no-reload edits for meaningfully better steady-state performance — a trade nearly every performance-conscious production deployment makes deliberately.
• .htaccess — per-directory config, no reload needed, but Apache must stat for it on every request up the directory chain. • AllowOverride All/FileInfo — enables .htaccess for that scope, at a real, cumulative performance cost. • AllowOverride None + rules moved into the main vhost <Directory> block — no per-request filesystem checks, best steady-state performance. • Production deployments with full server-config access typically prefer AllowOverride None; shared hosting has little choice but .htaccess.
A Directory Block Disabling .htaccess
AllowOverride None stops Apache from checking for .htaccess files in this directory on every request. Rules that would live in .htaccess are instead placed directly in the vhost, requiring a reload to change but avoiding per-request overhead.
<Directory /var/www/myapp/public>
AllowOverride None
Require all granted
# Rewrite rules that would otherwise live in .htaccess:
RewriteEngine On
RewriteCond %{REQUEST_FILENAME} !-f
RewriteCond %{REQUEST_FILENAME} !-d
RewriteRule ^ index.php [QSA,L]
</Directory>Code Examples
This vhost serves from public/, routes every non-existent path to index.php for front-controller routing, and hands actual .php execution to a PHP-FPM socket via mod_proxy_fcgi rather than embedding PHP with mod_php — the same FastCGI handoff Nginx uses, expressed in Apache's SetHandler syntax.
On shared hosting where editing the main Apache config isn't possible, the same front-controller rewrite logic is placed in .htaccess inside public/, taking effect immediately with no server reload — at the cost of Apache having to check for this file on every request.
Common mistake: Forgetting the dotfile-denial block means a misconfigured server could serve .env or .git contents directly if AllowOverride and rewrite rules don't already catch it.
<VirtualHost *:80>
ServerName app.example.com
DocumentRoot /var/www/myapp/public
<Directory /var/www/myapp/public>
AllowOverride None
Require all granted
RewriteEngine On
RewriteCond %{REQUEST_FILENAME} !-f
RewriteCond %{REQUEST_FILENAME} !-d
RewriteRule ^ index.php [QSA,L]
</Directory>
<FilesMatch \.php$>
SetHandler "proxy:unix:/run/php/php8.3-fpm.sock|fcgi://localhost"
</FilesMatch>
ErrorLog ${APACHE_LOG_DIR}/myapp_error.log
CustomLog ${APACHE_LOG_DIR}/myapp_access.log combined
</VirtualHost># public/.htaccess
RewriteEngine On
RewriteCond %{REQUEST_FILENAME} !-f
RewriteCond %{REQUEST_FILENAME} !-d
RewriteRule ^ index.php [QSA,L]
# Deny access to dotfiles like .env
<FilesMatch "^\.">
Require all denied
</FilesMatch>mod_php vs FPM-via-Proxy Under Apache
FPM lets Apache use a lighter, event-driven MPM while PHP execution scales independently in its own pool.
mod_php: PHP-FPM via mod_proxy_fcgi:
┌───────────────────────┐ ┌────────────┐ FastCGI ┌────────────────┐
│ Apache worker process │ │ Apache │ ────────▶│ PHP-FPM pool │
│ (PHP embedded inside) │ │ (event MPM) │ socket │ (separate procs)│
└───────────────────────┘ └────────────┘ └────────────────┘
1 process = HTTP + PHP combined HTTP handling and PHP execution are decoupledImportant Rules
Point DocumentRoot at public/, never the project root.
Prefer PHP-FPM via mod_proxy_fcgi over mod_php for new production deployments.
Enable mod_rewrite (a2enmod rewrite) before relying on any RewriteRule, in .htaccess or the vhost.
Use AllowOverride None with rules in the main vhost whenever you control the server config, for steady-state performance.
Always deny access to dotfiles (.env, .git) explicitly, regardless of where rewrite rules live.
Shared Hosting's Reliance on .htaccess
Most budget shared-hosting PHP plans give customers zero access to Apache's main configuration, which is precisely why WordPress and most PHP frameworks ship a default .htaccess for pretty permalinks — it's often the only mechanism a hosted customer has available to configure URL rewriting at all.
AllowOverride All # or move the rules into the vhost's <Directory> block instead
With AllowOverride None, Apache never even reads .htaccess files in that directory — rewrite rules placed there are silently ignored no matter how correct the syntax is.
Running mod_php with the event MPM enabled Use the prefork MPM with mod_php, or switch to PHP-FPM with the event MPM mod_php is not thread-safe in the way Apache's event/worker MPMs require, so it can only run correctly under the prefork MPM — mixing mod_php with a threaded MPM causes instability or an outright startup failure.
What is the main performance cost of enabling .htaccess (AllowOverride other than None) for a directory? A. PHP scripts run slower inside that directory B. Apache must check for a .htaccess file in that directory and every parent up to the document root, on every request C. It disables mod_rewrite entirely D. It forces Apache to use mod_php instead of PHP-FPM
B. Apache must check for a .htaccess file in that directory and every parent up to the document root, on every request — Because .htaccess files can change at any time, Apache cannot cache their absence — it must stat the filesystem for one on every single request up the directory chain, which adds cumulative overhead that a config loaded once at startup avoids.
Why can't mod_php run under Apache's event MPM? A. mod_php requires prefork MPM because it isn't compatible with the threading model event/worker MPMs use B. event MPM doesn't support HTTP C. mod_php only works with Nginx D. There is no restriction; it works fine with any MPM
A. mod_php requires prefork MPM because it isn't compatible with the threading model event/worker MPMs use — mod_php embeds PHP directly in Apache's worker processes and is not thread-safe in the way the event/worker MPMs require, so it is restricted to the prefork MPM, one process per connection.
In a mod_rewrite front-controller rule set, what do the !-f and !-d RewriteCond checks do? A. They check whether the requested path is NOT an existing file and NOT an existing directory, respectively B. They force all requests to index.php regardless of what exists on disk C. They disable rewriting for .php files specifically D. They check whether the user is authenticated
A. They check whether the requested path is NOT an existing file and NOT an existing directory, respectively — !-f and !-d are Apache's negated file-exists and directory-exists tests; when both are true (nothing real exists at the requested path), the following RewriteRule sends the request to index.php instead.
Write a <Directory> block for /var/www/blog/public that sets AllowOverride None, grants access, and includes a mod_rewrite rule set that routes any request not matching a real file or directory to index.php with the query string preserved.
<Directory /var/www/blog/public> AllowOverride None Require all granted RewriteEngine On RewriteCond %{REQUEST_FILENAME} !-f RewriteCond %{REQUEST_FILENAME} !-d RewriteRule ^ index.php [QSA,L] </Directory>
Use RewriteCond %{REQUEST_FILENAME} !-f and !-d before the RewriteRule, with the QSA and L flags.