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
}
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.
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.
If you omit http:// or https://, Caddy defaults to HTTPS and tries to issue a certificate automatically.
SPAs in Caddy v2
For single-page apps (SPAs), use rewrite to handle client-side routing:
report-app.sksushil.info.np {
reverse</em>proxy report-app:80</p><p> # SPA fallback
@notFound {
not file
}
rewrite @notFound /index.html
}
If that doesn't work, try the explicit static file matcher approach:
report.com.np {
reverse<em>proxy report-app:80</p><p> @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 fall back to index.html when a static 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 and Caddy can reach them by container name.
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</p><p>volumes:
caddy<em>data:
caddy</em>config:</p><p>networks:
shared-net:
external: true
Example App Service
/srv/report/docker-compose.yml
services:
report-app:
image: report-app:latest
container_name: report-app
expose:
- "80"
restart: always
networks:
- shared-net</p><p>networks:
shared-net:
external: true
\expose instead of ports. The container is reachable only within the shared Docker network — Caddy is the only public entry point. \
Caddyfile Reload + Format
Once deployed, reload your config anytime without downtime:
docker exec -it caddy caddy reload --config /etc/caddy/Caddyfile
Caddy even formats its own config — 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
Related Articles
Building My Own Hosting Server (Completely Free — Just Electricity & Internet Bills!)
How I built my own hosting server - just electricity and internet bills, no hosting fees!...
Tickets, Tunnels, and Tailscale: Journey to Simple Theater Connectivity
Instead of the central server reaching into a local DB (hard if it's behind NAT), the local device c...