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. ☕
Tags
Subscribe to Newsletter
Get notified about new articles