My encounter with Caddy

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 {
    log
    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.