Migrate from Apache to Caddy + Coraza WAF
Step-by-step migration guide for replacing Apache with Caddy and the Coraza WAF plugin. Covers inventory, .htaccess conversion, PHP-FPM setup, gradual cutover, and rollback strategy.
This guide walks through migrating from Apache to Caddy + Coraza as a full replacement. It covers the process, not the rationale. For a comparison of both approaches (native ModSecurity vs. Caddy+Coraza), see our Apache WAF Protection guide.
Apache migrations have a unique challenge that Nginx migrations do not: .htaccess files. Caddy does not support .htaccess, so every rule needs to move into the Caddyfile. This guide covers that conversion in detail.
Prerequisites
- An existing Apache deployment you want to replace
- Docker installed (recommended for testing)
- Familiarity with the Apache-to-Caddy directive mapping from the Apache WAF guide
- A staging or testing environment
Step-by-Step Guide
Why Replace Apache with Caddy + Coraza
Valid reasons to migrate:
- Simpler configuration: Apache's VirtualHost + .htaccess + mod_rewrite stack is powerful but complex. A Caddyfile for the same setup is typically one-third the size.
- Automatic HTTPS: no more certbot, no more cron jobs, no more expired certificate emergencies. Caddy manages TLS certificates automatically.
- Modern WAF engine: while Apache's ModSecurity integration is the most mature, Coraza is a modern Go reimplementation that runs the same CRS rules with zero C dependencies.
- Docker-native: Caddy + Coraza runs as a single container. No module compilation, no apt packages, no library conflicts.
- Performance: Apache's prefork/worker model is battle-tested but Caddy's event-driven architecture handles concurrent connections with less memory overhead.
Do not migrate if:
- You rely heavily on mod_php (though php_fastcgi is a clean alternative)
- You have hundreds of .htaccess files across many directories
- Your team has deep Apache expertise and the current setup works well
- You use Apache-specific modules (mod_perl, mod_python, mod_jk) with no alternatives
Pre-Migration Inventory
Document everything before you start:
1. List all sites and VirtualHosts
# List enabled sites ls /etc/apache2/sites-enabled/ # or on CentOS/RHEL: ls /etc/httpd/conf.d/
# Find all ServerName entries grep -rh "ServerName\|ServerAlias" /etc/apache2/sites-enabled/ | sort -u
2. Find all .htaccess files
# This is critical - Caddy does not support .htaccess find /var/www -name ".htaccess" -exec echo "=== {} ===" \; -exec cat {} \;
Save this output. Every .htaccess rule needs to move into the Caddyfile.
3. Document proxy targets and backend services
grep -rh "ProxyPass\|ProxyPassReverse" /etc/apache2/sites-enabled/ | sort -u
4. Check for special modules
# List loaded Apache modules apache2ctl -M 2>/dev/null || httpd -M 2>/dev/null
# Check for modules that need special handling # mod_php, mod_rewrite, mod_security2, mod_ssl, mod_proxy, mod_headers
5. Note PHP setup
# Check if using mod_php or PHP-FPM apache2ctl -M 2>/dev/null | grep php # If php is listed, you are using mod_php # Caddy uses PHP-FPM instead - you will need to set this up
Handle .htaccess Files
This is the biggest difference between Apache and Caddy migrations (Nginx does not use .htaccess, so this step does not apply to Nginx migrations).
Common .htaccess patterns and their Caddyfile equivalents:
# .htaccess: Force HTTPS RewriteEngine On RewriteCond %{HTTPS} off RewriteRule (.*) https://%{HTTP_HOST}%{REQUEST_URI} [R=301,L] # Caddy: automatic (just use a domain name)
# .htaccess: Deny directory listing Options -Indexes # Caddy: file_server (without browse flag)
# .htaccess: Block specific files <FilesMatch "\.(env|git|sql)$"> Require all denied </FilesMatch> # Caddy: @blocked path *.env *.git *.sql respond @blocked 403
# .htaccess: Custom error pages ErrorDocument 404 /404.html # Caddy: handle_errors { rewrite * /{err.status_code}.html file_server }
# .htaccess: Password protection AuthType Basic AuthName "Restricted" AuthUserFile /path/.htpasswd Require valid-user # Caddy: basicauth { user $2a$14$... (bcrypt hash) }
# .htaccess: WordPress pretty permalinks RewriteRule ^index\.php$ - [L] RewriteCond %{REQUEST_FILENAME} !-f RewriteCond %{REQUEST_FILENAME} !-d RewriteRule . /index.php [L] # Caddy: php_fastcgi unix//run/php/php-fpm.sock { try_files {path} /index.php }
Set Up PHP-FPM (if using PHP)
If your Apache setup uses mod_php, you need to switch to PHP-FPM. This is actually the recommended approach even in Apache for better performance.
Install PHP-FPM:
sudo apt install php-fpm # or for specific version: sudo apt install php8.3-fpm
Configure Caddy for PHP:
example.com { coraza_waf { load_owasp_crs directives ` Include @coraza.conf-recommended Include @crs-setup.conf.example Include @owasp_crs/*.conf SecRuleEngine On ` }
root * /var/www/html php_fastcgi unix//run/php/php-fpm.sock file_server }
The php_fastcgi directive handles index.php routing, PATH_INFO, and all the PHP-specific headers automatically. For WordPress, this single directive replaces the entire .htaccess mod_rewrite block.
Convert VirtualHosts to Caddyfile
Use the directive mapping table from our Apache WAF Protection guide to convert each VirtualHost. Inline any .htaccess rules from the previous step.
Example conversion of a typical Apache VirtualHost:
Before (Apache):
<VirtualHost *:80> ServerName example.com Redirect permanent / https://example.com/ </VirtualHost>
<VirtualHost *:443> ServerName example.com DocumentRoot /var/www/html
SSLEngine on SSLCertificateFile /etc/letsencrypt/live/example.com/fullchain.pem SSLCertificateKeyFile /etc/letsencrypt/live/example.com/privkey.pem
<IfModule security2_module> SecRuleEngine On IncludeOptional /etc/modsecurity/crs/crs-setup.conf IncludeOptional /etc/modsecurity/crs/rules/*.conf </IfModule>
Header set X-Content-Type-Options nosniff Header set X-Frame-Options DENY
<Directory /var/www/html> AllowOverride All Require all granted </Directory> </VirtualHost>
After (Caddy):
{ order coraza_waf first }
example.com { coraza_waf { load_owasp_crs directives ` Include @coraza.conf-recommended Include @crs-setup.conf.example Include @owasp_crs/*.conf SecRuleEngine On ` }
root * /var/www/html file_server encode gzip
header { X-Content-Type-Options nosniff X-Frame-Options DENY } }
The HTTP-to-HTTPS redirect, SSL config, and certbot integration all disappear. Caddy handles them automatically.
Test, Cut Over, and Roll Back
The testing, cutover, and rollback process is the same as for Nginx migrations:
- Validate Caddyfile syntax:
caddy validate --config Caddyfile --adapter caddyfile - Run in staging: test every site against your pre-migration inventory
- Test WAF: verify attacks are blocked and legitimate traffic passes
- Test PHP: if using PHP, verify all pages render, forms submit, and uploads work
- Gradual cutover: DNS-based (lower TTL first), load balancer split, or per-site migration
- Keep Apache running: do not decommission until Caddy has been stable for at least a week
Rollback:
Point traffic back to Apache (DNS change or load balancer switch). Apache should still be running and ready to serve. The rollback takes under 2 minutes.
For the detailed cutover strategies, see the Nginx migration guide. The process is identical.
Conclusion & Next Steps
Apache to Caddy migrations require more work than Nginx migrations because of .htaccess files. Every .htaccess rule needs to move into the Caddyfile. But the result is a cleaner, simpler setup with automatic HTTPS, a built-in WAF, and no mod_security compilation headaches.
The CRS protection is identical. ModSecurity and Coraza run the exact same rules. The only thing changing is the engine and the web server around it.
For the directive mapping table and AI-assisted config conversion, see the Apache WAF Protection guide.
Troubleshooting
PHP pages return blank or 502
PHP-FPM is not running or the socket path is wrong. Check systemctl status php-fpm and verify the socket path in your Caddyfile matches the PHP-FPM config (/etc/php/8.x/fpm/pool.d/www.conf).
.htaccess rules are not working
Caddy does not support .htaccess. All rules must be in the Caddyfile. Use the conversion patterns in step 3 of this guide.
WordPress shows "too many redirects"
Remove any HTTPS redirect rules from your WordPress config (wp-config.php or plugins). Caddy handles HTTPS automatically and WordPress may conflict with it. Also check that WordPress's site URL is set to https://.
Frequently Asked Questions
What about my existing ModSecurity rules?
Your OWASP CRS rules work identically on Coraza. Custom SecRule directives and rule exclusions (SecRuleRemoveById) also work because Coraza supports the same SecLang language. Test any highly custom rules before switching, as Coraza has partial compatibility with some advanced ModSecurity features.
How do I handle .htaccess files with hundreds of rewrite rules?
Use the AI-assisted conversion prompt from the Apache WAF guide. Paste your .htaccess content and let the AI convert the rewrite rules to Caddy matchers and rewrites. Review the output carefully, especially complex RewriteCond chains.
Is Caddy as stable as Apache for PHP hosting?
Yes. Caddy with php_fastcgi and PHP-FPM is a well-tested combination used in production by many WordPress, Laravel, and Drupal sites. The PHP-FPM process handles PHP execution identically regardless of whether the web server is Apache or Caddy. The key difference is that Apache embeds PHP (mod_php) while Caddy talks to PHP-FPM over a socket, which is actually the recommended approach for performance even in Apache.
Can I migrate one VirtualHost at a time?
Yes. Run both Apache and Caddy on different ports or IPs. Use DNS to point individual domains to whichever server handles that site. Migrate the lowest-traffic site first, verify it works, then continue with the rest.
Related Guides
How to Add WAF Protection to Apache
Two approaches to protect Apache with a WAF. Use ModSecurity as a native Apache module, or replace Apache with Caddy+Coraza for a simpler, modern setup with the same OWASP CRS rules.
Migrate from Nginx to Caddy + Coraza WAF
Step-by-step migration guide for replacing Nginx with Caddy and the Coraza WAF plugin. Covers pre-migration checklist, config conversion, gradual cutover, rollback plan, and post-migration validation.