Back to Blog
devops

How I Replaced Nginx Chaos with One Clean Caddyfile

November 4, 20255 min min read
How I Replaced Nginx Chaos with One Clean Caddyfile

Last week, I was deep in deployment hell. More than 10 apps. 10+ APIs. One overworked developer (me).

Every time I touched Nginx, something broke — semicolons went missing, SSL expired, or ports collided. All I wanted was for report-api, report.app, and payments-demo.sksushil.info.np to just work.

Then I met Caddy — and my nights got quieter.

The Problem: Too Many Configs, Too Little Sanity

Each microservice meant another Nginx file. Another reload command. Another moment wondering, "Why isn't this proxying?"

And let's be honest: debugging Nginx inside Docker feels like fighting your own reflection.

I needed something that understood Docker — something clean, automatic, and smart.

The "Wait, That's It?" Moment

A friend told me:

Caddy is like Nginx, but with auto HTTPS and no pain.

I tried it. I wrote this one file called Caddyfile:

report-app.sksushil.info.np {
    reverse<em>proxy report-app:80
}</p><p>app1.sksushil.info.np {
    reverse</em>proxy app-1:80
}
app1-api.sksushil.info.np {
    reverse<em>proxy app1-api:8080
}
payments-demo.sksushil.info.np {
    reverse</em>proxy payments-app-demo:8080
}

One reload command later, everything was live. SSL worked. Routing worked. I didn't cry.

Note on SSL Conflicts (Cloudflare Users)

Sometimes Caddy's automatic HTTPS can conflict with Cloudflare Flexible SSL, causing redirect loops.

For example:

payments-demo.sksushil.info.np {
    reverse<em>proxy payments-app-demo:8080
}

By default, Caddy tries to get a Let's Encrypt certificate. Even if your backend is HTTP, Caddy terminates HTTPS and proxies to your app.

This can clash with Cloudflare's SSL and cause infinite redirects.

Solution

Explicitly serve plain HTTP in Caddy:

http://payments-demo.sksushil.info.np {
    reverse</em>proxy payments-app-demo:8080
}

This tells Caddy: "Do not get certificates — just serve HTTP." Now Cloudflare handles SSL entirely.

:::tip If you omit http:// or https://, Caddy defaults to HTTPS and tries to issue a certificate. :::

SPAs in Caddy v2

For single-page apps (SPAs), you should use rewrite like this:

report-app.sksushil.info.np {
    reverse<em>proxy report-app:80
    # SPA fallback
    @notFound {
        not file
    }
    rewrite @notFound /index.html
}

Didn't work? Try this instead:

report.com.np {
    reverse</em>proxy report-app:80
    @static {
        path <em>.js </em>.css <em>.png </em>.jpg <em>.svg </em>.ico <em>.json
    }
    @notStatic {
        not path </em>.js <em>.css </em>.png <em>.jpg </em>.svg <em>.ico </em>.json
    }
    handle @notStatic {
        rewrite * /index.html
    }
}

This ensures your SPA routes work correctly and fallback to index.html when a file isn't found.

The Secret: Shared Docker Network

Caddy needs to see your app containers — like neighbors on the same street. That's what the shared Docker network does:

docker network create shared-net

Now every app joins this same network.

Caddy Setup

# /srv/caddy/docker-compose.yml
services:
  caddy:
    image: caddy:latest
    container<em>name: caddy
    restart: always
    ports:
      - "80:80"
      - "443:443"
    volumes:
      - ./Caddyfile:/etc/caddy/Caddyfile
      - caddy</em>data:/data
      - caddy<em>config:/config
    networks:
      - shared-net
volumes:
  caddy</em>data:
  caddy<em>config:
networks:
  shared-net:
    external: true

Example App Service

# /srv/report/docker-compose.yml
services:
  report-app:
    image: report-app:latest
    container</em>name: report-app
    expose:
      - "80"
    restart: always
    networks:
      - shared-net
networks:
  shared-net:
    external: true

Caddyfile Reload + Format

Once deployed, reload your config anytime:

docker exec -it caddy caddy reload --config /etc/caddy/Caddyfile

Caddy even formats its own config neatly — just run:

docker exec -it caddy caddy fmt --overwrite /etc/caddy/Caddyfile

The Ending I Wanted

Now my production stack runs three sites, two APIs, and one proxy — all behind a single elegant Caddyfile. No /etc/nginx/sites-enabled, no renewal scripts, no fragile reloads.

Just this:

docker exec -it caddy caddy reload --config /etc/caddy/Caddyfile

…and a peaceful developer sipping coffee while SSL renews itself. ☕

Share this article

Tags

Caddy Nginx Docker DevOps Self-Hosted

Subscribe to Newsletter

Get notified about new articles