A software engineer website

My encounter with Caddy

Gautier DI FOLCO July 26, 2023 [dev] #devops #infra #cloud

At some point in my previous position we had to set up live updates (i.e. getting changes without reloading the page) in our SPA.

Instead of relying on WebSockets I have chosen SSE as I was much simpler (as it is unidirectional) while perfectly fitting our use-cases.

Instead of building ours, we relied on Mercure, which itself relies on Caddy which is a "new" HTTP server.

While you don't really need to know it to have something working, we had come concerns which forced me to dig a little deeper, this log is about some snippets I collected.

The first awesome thing with Caddy is its dynamical configuration.

Which means, once it runs (caddy run), this command is enough to change the configuration:

$ curl localhost:2019/load \
    -H "Content-Type: text/caddyfile" \
    --data-binary @Caddyfile

So, with a simple definition:

http://localhost:8080 {
	respond "Hello, world!"

(http:// here forces HTTP protocol, Caddy if HTTPS by default)

We get

$ curl http://localhost:8080
Hello, world!

To be transparent, it often ends up in:

while inotifywait -e close_write Caddyfile test.sh; do ./test.sh; done

test.sh containing the load curl and the curls I want to test.

Caddy gives you the opportunity to quickly create a reverse proxy:

http://localhost:8081 {
  reverse_proxy :8080

You can be more specific adding requests matchers:

http://localhost:8081 {
 reverse_proxy /api/* :8080
 reverse_proxy /assets/* :8082

or having named matchers:

http://localhost:8081 {
  @api_ro {
    method GET
    path /api/*

 reverse_proxy @api_ro :8083
 reverse_proxy /api/* :8080
 reverse_proxy /assets/* :8082

Since I was running everything in kubernetes, I had to have a /healthz endpoint to ensure container readiness/liveness:

http://localhost:8080 {
  respond /healthz 200

At some point I also needed specific reverse proxy on another subdomain, involving CORS.

(cors) {
    @origin{args.0} header Origin {args.0}
    header @origin{args.0} Access-Control-Allow-Headers "content-type, x-requested-with"
    header @origin{args.0} Vary Origin

localhost:8080 {
    encode zstd gzip

    import cors https://app.acme.crp
    import cors https://backoffice.acme.crp

    handle /healthz {
        respond 200

    handle /api {
        rewrite * /v2/api
        reverse_proxy https://backend.product.com {
            header_up X-Real-IP {remote}

As you can see, you can easily factor-out directives.

I also use it extensively to prototype APIs (instead of Postman-like tools), as it allows me to have a mocked API code-first.

Back to top