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.

2-4 hours intermediate 7 steps
Last updated: Jun 6, 2026

If you have decided to replace Nginx with Caddy + Coraza, this guide walks through the full migration process. It is not about whether to switch (see our Nginx WAF Protection guide for that comparison), it is about how to do it safely.

This guide assumes you have read the directive mapping table in the Nginx WAF guide. It focuses on the migration process itself: inventory, conversion, testing, cutover, and rollback.

Prerequisites

  • An existing Nginx deployment you want to replace
  • Docker installed (recommended for testing)
  • Familiarity with the Nginx-to-Caddy directive mapping from the Nginx WAF guide
  • A staging or testing environment

Step-by-Step Guide

1

Why Replace Nginx with Caddy + Coraza

Before migrating, make sure your reasons are solid. Here are the valid ones:

  • ModSecurity maintenance burden: compiling ModSecurity from source, resolving C library conflicts, and keeping up with security patches is ongoing work. Coraza has zero C dependencies.
  • ModSecurity EOL trajectory: since Trustwave handed ModSecurity to the OWASP community in January 2024, development has slowed significantly. Coraza is where new WAF engine development is happening.
  • Automatic HTTPS: Caddy handles Let's Encrypt certificates automatically. No certbot cron jobs, no renewal scripts, no expired certificate incidents.
  • Simpler config: a typical Nginx config translates to one-third the lines in Caddy. Less config means fewer mistakes.
  • Docker-native: Caddy + Coraza runs as a single container with no volume-mounted modules or multi-stage builds for ModSecurity.

Do not migrate if:

  • Your Nginx setup is complex (heavy use of Lua, njs, proxy_cache, or stream modules) and works fine
  • You depend on Nginx Plus features (live dashboard, active health checks beyond what Caddy offers)
  • Your team has deep Nginx expertise and no interest in learning Caddy
  • You have a large fleet of Nginx servers and the migration cost outweighs the benefits
2

Pre-Migration Checklist

Before touching any config, inventory what you have:

1. List all sites and configs

# List all enabled sites ls /etc/nginx/sites-enabled/
# Find all config files that are actually loaded nginx -T 2>/dev/null | grep "# configuration file" | sort -u
# Count server blocks grep -r "server_name" /etc/nginx/sites-enabled/ | sort

2. Document your upstream services

# Find all proxy_pass targets grep -rh "proxy_pass" /etc/nginx/sites-enabled/ | sort -u

3. Check for features that need special handling

# Lua modules grep -r "lua_" /etc/nginx/ | head
# Stream/TCP proxying grep -r "stream" /etc/nginx/nginx.conf
# Cache zones grep -r "proxy_cache" /etc/nginx/ | head
# Rate limiting grep -r "limit_req" /etc/nginx/ | head
# Custom maps grep -r "^[[:space:]]*map" /etc/nginx/ | head

4. Note your SSL certificate setup

# Find certificate paths grep -r "ssl_certificate" /etc/nginx/sites-enabled/ | sort -u
# Check if certbot manages them ls /etc/letsencrypt/live/ 2>/dev/null

Save this inventory. You will reference it during conversion and use it as a verification checklist after migration.

3

Convert Your Config

Use the directive mapping table from our Nginx WAF Protection guide to convert each site config. For complex configs, use the AI-assisted conversion prompt from that guide.

Key conversion rules:

  • Each Nginx server { } block becomes a Caddy site block: example.com { }
  • proxy_pass becomes reverse_proxy
  • root + index + try_files becomes root * /path + file_server
  • SSL config disappears entirely (Caddy auto-HTTPS) unless you use custom certs
  • HTTP-to-HTTPS redirects disappear (automatic in Caddy)
  • add_header becomes header
  • location /path { } becomes handle /path/* { } or named matchers

Add the Coraza WAF block to each site:

{ 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 ` }
# ... your converted config here ... }
Tip: Convert one site at a time. Do not try to convert everything in one batch.
4

Test in a Staging Environment

Never cut over production without testing first. Here is the process:

1. Build the Caddy+Coraza image

FROM caddy:2-builder AS builder RUN xcaddy build --with github.com/corazawaf/coraza-caddy/v2
FROM caddy:2 COPY --from=builder /usr/bin/caddy /usr/bin/caddy
docker build -t caddy-coraza .

2. Validate your Caddyfile syntax

docker run --rm \ -v ./Caddyfile:/etc/caddy/Caddyfile:ro \ caddy-coraza caddy validate --config /etc/caddy/Caddyfile --adapter caddyfile

3. Run locally and test each site

docker run -d --name caddy-test -p 8443:443 -p 8080:80 \ -v ./Caddyfile:/etc/caddy/Caddyfile:ro \ -v ./site:/srv:ro \ -v caddy_data:/data \ caddy-coraza

4. Verify against your inventory checklist

For each item in your pre-migration inventory, verify:

  • Static files load correctly
  • Reverse proxy targets respond
  • Redirects work as expected
  • Headers are set correctly (check with curl -I)
  • WAF blocks attack payloads (SQLi, XSS, scanner probes)
  • WAF does NOT block legitimate application traffic
5

Gradual Cutover Strategy

For production migrations, do not switch all traffic at once. Use one of these approaches:

Option 1: DNS-based cutover (simplest)

  1. Run Caddy+Coraza on a separate host or port
  2. Lower DNS TTL to 60 seconds a few days before
  3. Point DNS to the Caddy instance
  4. Monitor for errors for 1-2 hours
  5. If problems: point DNS back to Nginx (takes effect within 60 seconds)

Option 2: Load balancer split (safer)

  1. Run both Nginx and Caddy behind a load balancer
  2. Send 10% of traffic to Caddy
  3. Monitor error rates and response times
  4. Gradually increase to 25%, 50%, 100%
  5. Decommission Nginx once 100% is stable

Option 3: Per-site migration (for multi-site setups)

  1. Migrate the lowest-traffic site first
  2. Run it on Caddy for a few days
  3. If stable, migrate the next site
  4. Continue until all sites are on Caddy
Tip: Keep your Nginx config and infrastructure intact until Caddy has been running in production for at least a week without issues.
6

Rollback Plan

Things you need ready before starting the cutover:

  • Nginx config backup: keep a complete copy of your Nginx config, SSL certs, and any custom modules
  • Nginx still running: do not stop Nginx until Caddy has been stable for days. Run both in parallel if possible.
  • DNS TTL lowered: if using DNS-based cutover, ensure TTL is 60 seconds before switching
  • Monitoring alerts: set up alerts for 5xx error rate increases, response time spikes, and WAF false positive rates

Rollback procedure:

  1. Point traffic back to Nginx (DNS change, load balancer switch, or port remap)
  2. Verify Nginx is serving traffic correctly
  3. Investigate what went wrong with the Caddy setup
  4. Fix and re-test before attempting cutover again

Rollback should take less than 2 minutes if you have kept Nginx running in parallel.

7

Post-Migration Cleanup

Once Caddy has been running stable in production for at least a week:

  • Remove certbot cron jobs (Caddy handles TLS renewal automatically)
  • Decommission the Nginx instance
  • Update deployment scripts and CI/CD pipelines
  • Update monitoring to point to Caddy logs and metrics
  • Archive the Nginx config (do not delete it yet, keep for reference)

Monitor WAF logs for the first month. CRS rules may trigger false positives on application traffic that Nginx did not inspect. Tune with SecRuleRemoveById directives as needed.

Conclusion & Next Steps

Migrating from Nginx to Caddy + Coraza is not complex, but it requires discipline: inventory first, convert carefully, test thoroughly, cut over gradually, and keep a rollback path ready.

The payoff is a simpler stack: one binary, one config file, automatic HTTPS, built-in WAF, and no C dependency chain. Your OWASP CRS protection stays identical because Coraza runs the exact same rules.

For the directive mapping table and AI-assisted config conversion, see the Nginx WAF Protection guide.

Troubleshooting

Caddy cannot bind to port 80 or 443

Nginx is still running on those ports. Either stop Nginx first, run Caddy on different ports, or use a load balancer in front of both.

Automatic HTTPS fails

Caddy needs port 80 and 443 open to the internet for ACME challenges. If you are behind a firewall or using custom certs, configure tls manually in the Caddyfile. For internal-only services, use tls internal.

WebSocket connections drop after migration

Caddy supports WebSocket proxying automatically through reverse_proxy. If connections are dropping, check that CRS rules are not blocking the upgrade headers. Add SecRuleRemoveById 920420 if needed.

Frequently Asked Questions

How long does the migration take?

For a simple Nginx setup (1-3 sites, mostly reverse proxy), expect 2-4 hours including testing. For complex setups with many sites, custom Lua modules, or advanced caching, plan for a multi-day process where you migrate one site at a time.

Will I lose any Nginx features?

Caddy covers 90% of what Nginx does. The gaps are: proxy_cache (less mature in Caddy), Lua/njs scripting (no equivalent), stream/TCP proxy (Caddy has layer4 as a plugin), and some edge-case load balancing features. If you rely on these, keep Nginx for those specific workloads and migrate the rest.

Can I keep some sites on Nginx and migrate others to Caddy?

Yes. Run both Nginx and Caddy on different ports or IPs, and use DNS or a load balancer to route traffic to the right server per site. This is the recommended approach for large deployments.

What about Nginx Unit or Nginx Plus?

Nginx Plus has commercial features (live dashboard, advanced health checks, session persistence) that Caddy covers partially. If you depend on Nginx Plus features, evaluate Caddy's alternatives carefully before migrating. Nginx Unit is a separate application server and is not relevant to this migration.

Related Guides