
August 14, 2026
11 min read
Table of Contents
By Kokil Thapa | Last reviewed: August 2026
Moving a production PHP application from Apache to Nginx often breaks sites because the two servers handle requests fundamentally differently. This Apache to Nginx migration step by step guide covers the exact process I use for Laravel, WordPress, and custom PHP applications on Ubuntu 22.04/24.04 servers in 2026. Whether you are chasing better concurrency for a high-traffic legal-tech portal or reducing memory overhead on a budget VPS, getting this transition right requires careful configuration mapping rather than simple file copying.
Before touching any production server, audit your current Apache setup. Many developers I work with, especially those managing Laravel applications in Nepal, inherit servers where critical routing logic lives in scattered .htaccess files. Nginx does not read these files. You must manually translate every rewrite rule, access restriction, and header directive into Nginx configuration syntax. Skipping this audit is the most common cause of post-migration 404 errors and broken asset paths.
How do you prepare for an Apache to Nginx migration step by step?
Preparation prevents downtime. Never uninstall Apache before Nginx is fully configured and tested. In my experience working on production Laravel applications and legal-tech portals like Notary Nepal and Court Marriage In Nepal, running both servers temporarily on different ports is the safest approach.
Audit existing Apache configuration
Document every virtual host, module dependency, and .htaccess directive. Pay special attention to:
- Rewrite rules: Laravel's front controller pattern, WordPress permalinks, and custom redirect logic.
- Access controls: IP whitelisting, basic auth, and directory restrictions.
- Header directives: CORS policies, security headers, and caching rules.
- SSL configuration: Certificate paths, chain files, and protocol versions.
For Laravel 12.x applications running on PHP 8.2+, verify that mod_rewrite is the only critical Apache module. Most modern PHP frameworks rely solely on URL rewriting and pass everything else to PHP-FPM, which simplifies migration significantly.
Install Nginx without stopping Apache
On Ubuntu 24.04, install Nginx while Apache continues serving traffic:
sudo apt update
sudo apt install nginx-core
sudo systemctl stop nginx
sudo systemctl disable nginx Stopping Nginx immediately after installation prevents port conflicts. You will enable it later on an alternate port for testing. This approach has saved me from accidental outages on multiple client projects where Apache had undocumented dependencies.
How do you convert Apache .htaccess rules to Nginx server blocks?
This is where most migrations fail. Nginx uses declarative server blocks instead of distributed .htaccess files. Every directive must be explicitly defined in the correct context.
Laravel 12.x Nginx configuration
For Laravel 12.x on PHP 8.4 (or 8.2/8.3), use this battle-tested server block. I've deployed this exact pattern across multiple legal-tech portals including Mijar Law Associates and Nepal Divorce Services:
server {
listen 8080;
server_name example.com www.example.com;
root /var/www/example/current/public;
index index.php;
charset utf-8;
client_max_body_size 64M;
location / {
try_files $uri $uri/ /index.php?$query_string;
}
location = /favicon.ico { access_log off; log_not_found off; }
location = /robots.txt { access_log off; log_not_found off; }
error_page 404 /index.php;
location ~ \.php$ {
fastcgi_pass unix:/run/php/php8.4-fpm.sock;
fastcgi_param SCRIPT_FILENAME $realpath_root$fastcgi_script_name;
include fastcgi_params;
fastcgi_hide_header X-Powered-By;
}
location ~ /\.(?!well-known).* {
deny all;
}
} Critical details often missed:
$realpath_rootresolves symlinks correctly in Deployer 7 zero-downtime deployments. Using$document_rootcan break PHP execution when the release symlink hasn't been updated atomically.try_filesmust end with/index.php?$query_string, not just/index.php. Omitting the query string breaks GET parameters in Laravel routes.- Socket path must match your installed PHP version. Run
ls /run/php/to verify the actual socket filename.
WordPress permalink conversion
WordPress relies heavily on .htaccess for pretty permalinks. The Nginx equivalent is simpler but requires explicit handling:
location / {
try_files $uri $uri/ /index.php?$args;
}
location ~* \.(js|css|png|jpg|jpeg|gif|ico|svg|woff|woff2)$ {
expires 30d;
add_header Cache-Control "public, immutable";
access_log off;
} Note the use of $args instead of $query_string for WordPress compatibility. Static asset caching should be handled at the Nginx level, not through PHP, to reduce FPM worker load during traffic spikes.
Common .htaccess translation patterns
| Apache Directive | Nginx Equivalent | Notes |
|---|---|---|
RewriteRule ^(.*)$ index.php [QSA,L] | try_files $uri $uri/ /index.php?$query_string; | Laravel/Symfony standard pattern |
Redirect 301 /old /new | return 301 /new; | Place inside specific location block |
Header set X-Frame-Options DENY | add_header X-Frame-Options DENY always; | always ensures header sent on error pages |
Require ip 192.168.1.0/24 | allow 192.168.1.0/24; deny all; | Order matters: allow first, then deny |
AuthType Basic | auth_basic "Restricted"; auth_basic_user_file /path/.htpasswd; | Generate htpasswd with openssl passwd -apr1 |
How do you configure PHP-FPM sockets for Nginx performance?
Nginx communicates with PHP via FastCGI. Socket choice and buffer tuning directly impact throughput under load.
Unix sockets vs TCP connections
Always use Unix sockets (/run/php/php8.4-fpm.sock) when Nginx and PHP-FPM run on the same server. Sockets avoid TCP stack overhead and typically deliver 20–30% lower latency for PHP requests. Reserve TCP (127.0.0.1:9000) only for architectures where PHP-FPM runs on a separate host.
FPM pool tuning for production
Edit /etc/php/8.4/fpm/pool.d/www.conf with values appropriate for your server's RAM:
[www]
user = www-data
group = www-data
listen = /run/php/php8.4-fpm.sock
listen.owner = www-data
listen.group = www-data
listen.mode = 0660
pm = dynamic
pm.max_children = 30
pm.start_servers = 8
pm.min_spare_servers = 4
pm.max_spare_servers = 12
pm.max_requests = 1000
request_terminate_timeout = 60s
slowlog = /var/log/php-fpm-slow.log
request_slowlog_timeout = 5s The formula pm.max_children = Available RAM / Average PHP Process Memory prevents OOM kills. On a 4GB VPS running Laravel, start with 30–40 children. Monitor with ps --no-headers -o rss -C php-fpm | awk '{sum+=$1} END {print sum/NR/1024 " MB avg"}' to measure actual per-process usage.
FastCGI buffer optimization
Add these directives to your Nginx server block to prevent "upstream sent too big header" errors common with Laravel sessions and cookies:
fastcgi_buffer_size 16k;
fastcgi_buffers 16 16k;
fastcgi_busy_buffers_size 32k;
fastcgi_read_timeout 60s; How do you handle SSL certificates during Apache to Nginx migration?
SSL misconfiguration is the second most frequent migration failure point after rewrite rules. Certificate format compatibility differs between Apache and Nginx.
Certificate file format differences
Apache typically uses three separate files: certificate, private key, and CA bundle. Nginx requires the certificate and CA bundle concatenated into a single file:
# Create combined certificate for Nginx
cat /etc/letsencrypt/live/example.com/fullchain.pem > /etc/nginx/ssl/example.com.crt
# Private key remains separate
cp /etc/letsencrypt/live/example.com/privkey.pem /etc/nginx/ssl/example.com.key
chmod 600 /etc/nginx/ssl/example.com.* If using Certbot with the Nginx plugin, this happens automatically. For manual migrations or commercial certificates, concatenation order matters: server certificate first, then intermediate chain.
Modern TLS configuration for 2026
Use this SSL server block aligned with current Mozilla recommendations:
server {
listen 443 ssl http2;
server_name example.com www.example.com;
ssl_certificate /etc/nginx/ssl/example.com.crt;
ssl_certificate_key /etc/nginx/ssl/example.com.key;
ssl_protocols TLSv1.2 TLSv1.3;
ssl_ciphers ECDHE-ECDSA-AES128-GCM-SHA256:ECDHE-RSA-AES128-GCM-SHA256:ECDHE-ECDSA-AES256-GCM-SHA384:ECDHE-RSA-AES256-GCM-SHA384;
ssl_prefer_server_ciphers off;
ssl_session_cache shared:SSL:10m;
ssl_session_timeout 1d;
ssl_session_tickets off;
# HSTS (include subdomains only if all subdomains support HTTPS)
add_header Strict-Transport-Security "max-age=63072000; includeSubDomains" always;
# ... rest of server block from earlier ...
} HTTP/2 is enabled via the http2 parameter on the listen directive in Nginx 1.25+. For older versions, use the deprecated http2 on; directive. Always test SSL configuration with nginx -t before reloading.
HTTP to HTTPS redirect
Create a separate server block for clean redirects:
server {
listen 80;
server_name example.com www.example.com;
return 301 https://$host$request_uri;
} Never mix HTTP and HTTPS handling in the same server block. Separate blocks simplify debugging and prevent accidental mixed-content issues that hurt Core Web Vitals and SEO rankings.
How do you test and validate Nginx before switching traffic?
Never switch DNS or ports without systematic validation. I've seen too many migrations fail because someone assumed "it works locally" meant production was ready.
Test on alternate port first
Configure Nginx to listen on port 8080 (HTTP) and 8443 (HTTPS) while Apache continues serving production traffic on 80/443. Run comprehensive tests:
- Syntax check:
sudo nginx -tmust return "syntax is ok" and "test is successful". - Static assets: Verify CSS, JS, and images load correctly. Check browser devtools for 404s.
- PHP execution: Confirm
.phpfiles execute rather than download. A downloaded PHP file means FPM misconfiguration. - Route testing: Hit every critical route type: homepage, authenticated pages, API endpoints, form submissions, file uploads.
- Header verification: Use
curl -Ito confirm security headers, caching directives, and content types match Apache behavior.
Automated regression testing
For complex applications, write a simple shell script that curls critical endpoints and checks response codes:
#!/bin/bash
PORT=8080
BASE="http://localhost:$PORT"
FAILURES=0
for path in "/" "/login" "/api/v1/health" "/dashboard"; do
CODE=$(curl -s -o /dev/null -w "%{http_code}" "$BASE$path")
if [ "$CODE" != "200" ] && [ "$CODE" != "302" ]; then
echo "FAIL: $path returned $CODE"
FAILURES=$((FAILURES + 1))
else
echo "OK: $path ($CODE)"
fi
done
[ $FAILURES -eq 0 ] && echo "All tests passed" || echo "$FAILURES tests failed" This catches regressions that manual browsing misses. On legal-tech portals with document upload workflows, I also test multipart form submissions explicitly since Nginx client_max_body_size defaults to 1MB and silently rejects larger uploads.
How do you perform zero-downtime cutover from Apache to Nginx?
Once validation passes, execute the cutover during low-traffic hours. For Nepal-based clients, this typically means late night IST/NPT when Dashain/Tihar season traffic dips.
The atomic swap procedure
# 1. Stop Apache gracefully
sudo systemctl stop apache2
# 2. Update Nginx to production ports
sudo sed -i 's/listen 8080;/listen 80;/g' /etc/nginx/sites-available/example.com
sudo sed -i 's/listen 8443/listen 443/g' /etc/nginx/sites-available/example.com
# 3. Validate and reload Nginx
sudo nginx -t && sudo systemctl reload nginx
# 4. Verify production access
curl -I https://example.com
# 5. Only after confirmation, disable Apache permanently
sudo systemctl disable apache2
sudo apt remove apache2 # Optional: keep installed for rollback safety Rollback strategy
Keep Apache installed and configured for at least 48 hours post-migration. If issues emerge:
sudo systemctl stop nginx
sudo sed -i 's/listen 80;/listen 8080;/g' /etc/nginx/sites-available/example.com
sudo systemctl start apache2 This restores service within seconds. Document the rollback commands before starting migration — panic troubleshooting at 2 AM leads to mistakes.
Post-migration monitoring
Watch these metrics for the first week:
- PHP-FPM slow log: Requests exceeding
request_slowlog_timeoutindicate bottlenecks exposed by Nginx's faster request handling. - 502/504 errors: Usually mean FPM socket permission issues or exhausted
pm.max_children. - Memory usage: Nginx uses significantly less RAM than Apache. Reallocate savings to PHP-FPM workers or Redis cache.
- SEO crawl stats: Monitor Google Search Console for unexpected 404s or redirect chains. Technical SEO issues from migration can take days to surface.
Ready to migrate your production server safely?
This Apache to Nginx migration step by step guide covers the patterns I've refined across dozens of production PHP deployments since 2010. Every configuration snippet comes from real servers running Laravel, WordPress, and custom legal-tech applications. If your infrastructure has unusual constraints, legacy Apache modules, or you need hands-on migration support for a Nepal-based or international project, reach out directly. I handle full-server migrations with zero-downtime guarantees and post-migration monitoring included.

