Migrate from Apache to Caddy + Coraza WAF
Step-by-step migration guide for replacing Apache with Caddy and the Coraza WAF plugin. Covers inventory, building Caddy with the Coraza module, .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)
- xcaddy installed, or Docker, to build Caddy with the Coraza module
- 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. Once you have built a Coraza-enabled binary, there are no apt module packages or library conflicts to manage.
- 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
Build Caddy with the Coraza Module
Vanilla Caddy does not understand the coraza_waf directive. Before any Caddyfile in this guide will load, you need a Caddy binary compiled with the Coraza WAF plugin. The current module path is github.com/corazawaf/coraza-caddy/v2 (v2 major version, current release v2.5.0).
Option A: Build with xcaddy
xcaddy build --with github.com/corazawaf/coraza-caddy/v2
This produces a caddy binary in the current directory that includes the Coraza module. Run ./caddy version to confirm.
Option B: Multi-stage Dockerfile
FROM caddy:builder AS builder
RUN xcaddy build --with github.com/corazawaf/coraza-caddy/v2
FROM caddy
COPY --from=builder /usr/bin/caddy /usr/bin/caddy
docker build -t caddy-coraza .
Alternatively, download a Coraza-enabled build from the Caddy download server (caddyserver.com/download, select the coraza-caddy package) or use a prebuilt Coraza image.
Loading the OWASP CRS
Every Caddyfile snippet below uses the load_owasp_crs subdirective inside the coraza_waf block. This registers the OWASP Core Rule Set embedded in the module, which is what makes the @coraza.conf-recommended, @crs-setup.conf.example, and @owasp_crs/*.conf macro includes resolve. Without load_owasp_crs, those @-prefixed includes fail because the referenced files are not registered.
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:
basic_auth {
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
coraza_waf is an unknown directive
Your Caddy binary was not built with the Coraza module. Rebuild it with xcaddy build --with github.com/corazawaf/coraza-caddy/v2 (or use the Dockerfile in step 3) and run the resulting binary.
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 the "Handle .htaccess Files" step 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.