This article was last updated on June 19, 2023, 1 year ago. The content may be out of date.

Caddy is a powerful, extensible server written in go. Though its native configuration format is json, it supports Caddyfile which is very easy to write. For the majority of use cases, caddy’s default configuration is enough. For power users, however, some configurations are not exposed in Caddyfile and must be set using json.

Because caddy config uses json format, we can use tools that modify json to produce caddy config.

About jd

From its official repository, jd is a commandline utility and Go library for diffing and patching JSON and YAML values. Its official examples include git structural diff and kubernetes deployment diff. Since caddy also uses json, we can use jd as well.

Prepare the Patch

Usually when we need to tweak a caddy config, we only touch part of it and leave the rest as it is. We can handwrite the json config and create the patch from the adapted json the first time.

jd old.json new.json > patch

Next time we modify the Caddyfile, and if there is no need to modify the patch, we can apply the patch and use the resulting json config.


For example, we want to use caddy-l4 to handle raw TCP/UDP traffic along with the existing http server. But caddy-l4 doesn’t support Caddyfile, so we have to write its json config ourselves and merge it with the existing config. We can create the patch the first time. (Save this file as patch)

@ ["apps","layer4"]
+ {"servers":{"example":{"listen":[""],"routes":[{"handle":[{"handler":"echo"}]}]}}}

And use jd to merge them later:

caddy adapt | jd -p patch

This way when we modify Caddyfile to add new domains for example, we just need to adapt and patch. The resulting json is the config we need.

Library Usage

jd can be used as a library instead. Combining it with caddy’s own /adapt endpoint we can even generate caddy config without installing caddy and jd.

// can be replaced by any reader that contains Caddyfile as content,
// even compressed ones
file, _ := os.Open("/path/to/caddy/config")
resp, _ := http.Post("http://localhost:2019/adapt", "text/caddyfile", file)

type R struct {
    Result json.RawMessage `json:"result"`

var r R
_ = json.NewDecoder(resp.Body).Decode(&r)
_ = resp.Body.Close()

old, _ := jd.ReadJsonString(string(r.Result))
diff, _ := jd.ReadPatchFile("/path/to/patch/file")
new, _ := old.Patch(diff)