Help and info on cache handler

1. My Caddy version (caddy version):

Docker image caddy/caddy:2.0.0-beta.19-alpine

2. How I run Caddy:

Docker Compose

a. System environment:

Ubuntu 18.04
Docker 19.03.8
docker-compose version 1.25.4

b. Command:

The image default command, though I do not use the Caddyfile (I configure Caddy loading a json with the API).

c. Service/unit/compose file:

version: "3.7"

services:
  caddy:
    container_name: caddy
    image: caddy/caddy:alpine
    restart: always
    volumes:
      - ./:/etc/caddy/
      - ./config:/config
      - ./data:/data
    ports:
      - "80:80"
      - "443:443"
      - "2019:2019"

networks:
  default:
    external:
      name: firefly

d. My complete Caddyfile or JSON config:

{
  "admin": {
    "listen": "0.0.0.0:2019"
  },
  "apps": {
    "http": {
      "http_port": 80,
      "https_port": 443,
      "servers": {
        "galactica": {
          "experimental_http3": true,
          "listen": [
            ":443"
          ],
          "routes": [
            {
              "handle": [
                {
                  "handler": "static_response",
                  "headers": {
                    "Location": [
                      "https://{http.request.host.labels.1}.{http.request.host.labels.0}{http.request.uri.path}"
                    ]
                  },
                  "status_code": 301
                }
              ],
              "match": [
                {
                  "host": [
                    "www.itsallsotireso.me",
                    "www.normco.re"
                  ]
                }
              ],
              "terminal": true
            },
            {
              "handle": [
                {
                  "handler": "subroute",
                  "routes": [
                    {
                      "handle": [
                        {
                          "encodings": {
                            "gzip": {},
                            "zstd": {}
                          },
                          "handler": "encode"
                        },
                        {
                          "handler": "headers",
                          "response": {
                            "set": {
                              "Expect-Ct": [
                                "max-age=604800"
                              ],
                              "Feature-Policy": [
                                "accelerometer 'none'; ambient-light-sensor 'none'; autoplay 'none'; camera 'none'; encrypted-media 'none'; fullscreen 'self'; geolocation 'none'; gyroscope 'none'; magnetometer 'none'; microphone 'none'; midi 'none'; payment 'none'; picture-in-picture 'self'; speaker 'none'; usb 'none';"
                              ],
                              "Referrer-Policy": [
                                "strict-origin-when-cross-origin"
                              ],
                              "Strict-Transport-Security": [
                                "max-age=31536000; includesubdomains; preload"
                              ],
                              "X-Content-Type-Options": [
                                "nosniff"
                              ],
                              "X-Frame-Options": [
                                "SAMEORIGIN"
                              ],
                              "X-Xss-Protection": [
                                "1; mode=block"
                              ]
                            }
                          }
                        }
                      ]
                    },
                    {
                      "handle": [
                        {
                          "handler": "headers",
                          "response": {
                            "set": {
                              "X-Robots-Tag": [
                                "noindex, nofollow, noarchive, nosnippet, notranslate, noimageindex"
                              ]
                            }
                          }
                        },
                        {
                          "handler": "reverse_proxy",
                          "headers": {
                            "request": {
                              "set": {
                                "X-Forwarded-For": [
                                  "{http.request.remote}"
                                ],
                                "X-Forwarded-Proto": [
                                  "{http.request.scheme}"
                                ],
                                "X-Real-Ip": [
                                  "{http.request.remote}"
                                ],
                                "X-Script-Name": [
                                  "/isso"
                                ]
                              }
                            },
                            "response": {
                              "set": {
                                "Server": [
                                  "CERN httpd"
                                ],
                                "X-Powered-By": [
                                  "the Holy Spirit"
                                ]
                              }
                            }
                          },
                          "upstreams": [
                            {
                              "dial": "isso:8080"
                            }
                          ]
                        }
                      ],
                      "match": [
                        {
                          "path": [
                            "/isso*"
                          ]
                        }
                      ]
                    },
                    {
                      "handle": [
                        {
                          "handler": "headers",
                          "response": {
                            "set": {
                              "X-Robots-Tag": [
                                "index, follow, noarchive, snippet, translate, noimageindex"
                              ]
                            }
                          }
                        },
                        {
                          "handler": "reverse_proxy",
                          "headers": {
                            "request": {
                              "set": {
                                "X-Forwarded-For": [
                                  "{http.request.remote}"
                                ],
                                "X-Forwarded-Proto": [
                                  "{http.request.scheme}"
                                ],
                                "X-Real-Ip": [
                                  "{http.request.remote}"
                                ]
                              }
                            },
                            "response": {
                              "set": {
                                "Server": [
                                  "CERN httpd"
                                ],
                                "X-Powered-By": [
                                  "the Holy Spirit"
                                ]
                              }
                            }
                          },
                          "upstreams": [
                            {
                              "dial": "ghost:2368"
                            }
                          ]
                        }
                      ]
                    }
                  ]
                }
              ],
              "match": [
                {
                  "host": [
                    "itsallsotireso.me"
                  ]
                }
              ],
              "terminal": true
            },
            {
              "handle": [
                {
                  "encodings": {
                    "gzip": {},
                    "zstd": {}
                  },
                  "handler": "encode"
                },
                {
                  "handler": "headers",
                  "response": {
                    "set": {
                      "Expect-Ct": [
                        "max-age=604800"
                      ],
                      "Feature-Policy": [
                        "accelerometer 'none'; ambient-light-sensor 'none'; autoplay 'none'; camera 'none'; encrypted-media 'none'; fullscreen 'self'; geolocation 'none'; gyroscope 'none'; magnetometer 'none'; microphone 'none'; midi 'none'; payment 'none'; picture-in-picture 'self'; speaker 'none'; usb 'none';"
                      ],
                      "Referrer-Policy": [
                        "strict-origin-when-cross-origin"
                      ],
                      "Strict-Transport-Security": [
                        "max-age=31536000; includesubdomains; preload"
                      ],
                      "X-Content-Type-Options": [
                        "nosniff"
                      ],
                      "X-Frame-Options": [
                        "SAMEORIGIN"
                      ],
                      "X-Xss-Protection": [
                        "1; mode=block"
                      ]
                    }
                  }
                },
                {
                  "handler": "headers",
                  "response": {
                    "set": {
                      "X-Robots-Tag": [
                        "noindex, nofollow, noarchive, nosnippet, notranslate, noimageindex"
                      ]
                    }
                  }
                },
                {
                  "handler": "reverse_proxy",
                  "headers": {
                    "request": {
                      "set": {
                        "X-Forwarded-For": [
                          "{http.request.remote}"
                        ],
                        "X-Forwarded-Proto": [
                          "{http.request.scheme}"
                        ],
                        "X-Real-Ip": [
                          "{http.request.remote}"
                        ]
                      }
                    },
                    "response": {
                      "set": {
                        "Server": [
                          "CERN httpd"
                        ],
                        "X-Powered-By": [
                          "the Holy Spirit"
                        ]
                      }
                    }
                  },
                  "upstreams": [
                    {
                      "dial": "shaarli:80"
                    }
                  ]
                }
              ],
              "match": [
                {
                  "host": [
                    "normco.re"
                  ]
                }
              ],
              "terminal": true
            }
          ],
          "strict_sni_host": true,
          "tls_connection_policies": [
            {
              "alpn": [
                "h2"
              ],
              "curves": [
                "x25519",
                "p521",
                "p384",
                "p256"
              ]
            }
          ]
        }
      }
    },
    "tls": {
      "automation": {
        "policies": [
          {
            "issuer": {
              "email": "address@mail.com",
              "module": "acme"
            },
            "key_type": "p384",
            "must_staple": true
          }
        ]
      }
    }
  },
  "logging": {
    "logs": {
      "default": {
        "level": "DEBUG"
      }
    }
  }
}

3. The problem I’m having:

Not a problem per se, just needing some inputs. I am still at a development stage, so I would like to experiment the cache module. I would like to see it on action, and this helps me practice the json config too.

Ghost is not the most efficient at serving static content, but since this is just a simple personal project, I do not want to bother adding nginx or Varnish for caching. I will just wait until Caddy is ready!

The documentation is a bit scarce, so I would like to get more info on how to configure this handler.

Basically, I have 1 domain which reverse proxies to 2 services:

  • a Ghost blog instance (ghost:2368)
  • an Isso instance (isso:8080) only on the path /isso/

I do not know if what my json config is the most efficient or elegant, but it works (any feedback on the structure is welcome btw!)

So what I would like to achieve with the cache module, is to cache everything in the Ghost instance, except the paths /p/ and /ghost/ (which are the post previews and the admin panel). Isso must not be cached.

4. Error messages and/or full log output:

No error yet, it is still just all theoretical for now.

5. What I already tried:

Below is the json I would like to load with the API with a cache handler.

I created a subroute (so that it does not apply to /isso/ reverse proxy) and added a negation match for /p/ and /ghost/

I do not know if the match will be OK like that, I made a try with a dummy header, just on /ghost/ and I could see that the header was not responded in the /ghost/ path. Maybe a path_regexp would be better to handle both /p/ and /ghost/?

I do not know what to value in self and peers. What kind of data should I put?

I also just notice that there is an issue with my whole route: encode comes after cache, and logically (and according to the doc) cache should be last. I will change this later but manipulating manually a json a is a bit tedious.

{
	"admin": {
		"listen": "0.0.0.0:2019"
	},
	"apps": {
		"http": {
			"http_port": 80,
			"https_port": 443,
			"servers": {
				"galactica": {
					"experimental_http3": true,
					"listen": [
						":443"
					],
					"routes": [
						{
							"handle": [
								{
									"handler": "static_response",
									"headers": {
										"Location": [
											"https://{http.request.host.labels.1}.{http.request.host.labels.0}{http.request.uri.path}"
										]
									},
									"status_code": 301
								}
							],
							"match": [
								{
									"host": [
										"www.itsallsotireso.me",
										"www.normco.re"
									]
								}
							],
							"terminal": true
						},
						{
							"handle": [
								{
									"handler": "subroute",
									"routes": [
										{
											"handle": [
												{
													"encodings": {
														"gzip": {},
														"zstd": {}
													},
													"handler": "encode"
												}
											]
										},
										{
											"handle": [
												{
													"handler": "headers",
													"response": {
														"set": {
															"Expect-Ct": [
																"max-age=604800"
															],
															"Feature-Policy": [
																"accelerometer 'none'; ambient-light-sensor 'none'; autoplay 'none'; camera 'none'; encrypted-media 'none'; fullscreen 'self'; geolocation 'none'; gyroscope 'none'; magnetometer 'none'; microphone 'none'; midi 'none'; payment 'none'; picture-in-picture 'self'; speaker 'none'; usb 'none';"
															],
															"Referrer-Policy": [
																"strict-origin-when-cross-origin"
															],
															"Strict-Transport-Security": [
																"max-age=31536000; includesubdomains; preload"
															],
															"X-Content-Type-Options": [
																"nosniff"
															],
															"X-Frame-Options": [
																"SAMEORIGIN"
															],
															"X-Robots-Tag": [
																"noindex, nofollow, noarchive, nosnippet, notranslate, noimageindex"
															],
															"X-Xss-Protection": [
																"1; mode=block"
															]
														}
													}
												},
												{
													"handler": "reverse_proxy",
													"headers": {
														"request": {
															"set": {
																"X-Forwarded-For": [
																	"{http.request.remote}"
																],
																"X-Forwarded-Proto": [
																	"{http.request.scheme}"
																],
																"X-Real-Ip": [
																	"{http.request.remote}"
																],
																"X-Script-Name": [
																	"/isso"
																]
															}
														},
														"response": {
															"set": {
																"Server": [
																	"CERN httpd"
																],
																"X-Powered-By": [
																	"the Holy Spirit"
																]
															}
														}
													},
													"upstreams": [
														{
															"dial": "isso:8080"
														}
													]
												}
											],
											"match": [
												{
													"path": [
														"/isso*"
													]
												}
											]
										},
										{
											"handle": [
												{
													"handler": "subroute",
													"routes": [
														{
															"match": [
																{
																	"not": {
																		"path": [
																			"/ghost/*"
																		],
																		"path": [
																			"/p/*"
																		]
																	}
																}
															],
															"handle": [
																{
																	"handler": "cache",
																	"self": "????",
																	"peers": [
																		"???"
																	],
																	"max_size": 512
																}
															]
														}
													]
												},
												{
													"handler": "headers",
													"response": {
														"set": {
															"Expect-Ct": [
																"max-age=604800"
															],
															"Feature-Policy": [
																"accelerometer 'none'; ambient-light-sensor 'none'; autoplay 'none'; camera 'none'; encrypted-media 'none'; fullscreen 'self'; geolocation 'none'; gyroscope 'none'; magnetometer 'none'; microphone 'none'; midi 'none'; payment 'none'; picture-in-picture 'self'; speaker 'none'; usb 'none';"
															],
															"Referrer-Policy": [
																"strict-origin-when-cross-origin"
															],
															"Strict-Transport-Security": [
																"max-age=31536000; includesubdomains; preload"
															],
															"X-Content-Type-Options": [
																"nosniff"
															],
															"X-Frame-Options": [
																"SAMEORIGIN"
															],
															"X-Robots-Tag": [
																"index, follow, noarchive, snippet, translate, noimageindex"
															],
															"X-Xss-Protection": [
																"1; mode=block"
															]
														}
													}
												},
												{
													"handler": "reverse_proxy",
													"headers": {
														"request": {
															"set": {
																"X-Forwarded-For": [
																	"{http.request.remote}"
																],
																"X-Forwarded-Proto": [
																	"{http.request.scheme}"
																],
																"X-Real-Ip": [
																	"{http.request.remote}"
																]
															}
														},
														"response": {
															"set": {
																"Server": [
																	"CERN httpd"
																],
																"X-Powered-By": [
																	"the Holy Spirit"
																]
															}
														}
													},
													"upstreams": [
														{
															"dial": "ghost:2368"
														}
													]
												}
											]
										}
									]
								}
							],
							"match": [
								{
									"host": [
										"itsallsotireso.me"
									]
								}
							],
							"terminal": true
						},
						{
							"handle": [
								{
									"encodings": {
										"gzip": {},
										"zstd": {}
									},
									"handler": "encode"
								},
								{
									"handler": "headers",
									"response": {
										"set": {
											"Expect-Ct": [
												"max-age=604800"
											],
											"Feature-Policy": [
												"accelerometer 'none'; ambient-light-sensor 'none'; autoplay 'none'; camera 'none'; encrypted-media 'none'; fullscreen 'self'; geolocation 'none'; gyroscope 'none'; magnetometer 'none'; microphone 'none'; midi 'none'; payment 'none'; picture-in-picture 'self'; speaker 'none'; usb 'none';"
											],
											"Referrer-Policy": [
												"strict-origin-when-cross-origin"
											],
											"Strict-Transport-Security": [
												"max-age=31536000; includesubdomains; preload"
											],
											"X-Content-Type-Options": [
												"nosniff"
											],
											"X-Frame-Options": [
												"SAMEORIGIN"
											],
											"X-Xss-Protection": [
												"1; mode=block"
											]
										}
									}
								},
								{
									"handler": "headers",
									"response": {
										"set": {
											"X-Robots-Tag": [
												"noindex, nofollow, noarchive, nosnippet, notranslate, noimageindex"
											]
										}
									}
								},
								{
									"handler": "reverse_proxy",
									"headers": {
										"request": {
											"set": {
												"X-Forwarded-For": [
													"{http.request.remote}"
												],
												"X-Forwarded-Proto": [
													"{http.request.scheme}"
												],
												"X-Real-Ip": [
													"{http.request.remote}"
												]
											}
										},
										"response": {
											"set": {
												"Server": [
													"CERN httpd"
												],
												"X-Powered-By": [
													"the Holy Spirit"
												]
											}
										}
									},
									"upstreams": [
										{
											"dial": "shaarli:80"
										}
									]
								}
							],
							"match": [
								{
									"host": [
										"normco.re"
									]
								}
							],
							"terminal": true
						}
					],
					"strict_sni_host": true,
					"tls_connection_policies": [
						{
							"alpn": [
								"h2"
							],
							"curves": [
								"x25519",
								"p521",
								"p384",
								"p256"
							]
						}
					]
				}
			}
		},
		"tls": {
			"automation": {
				"policies": [
					{
						"issuer": {
							"email": "address@mail.com",
							"module": "acme"
						},
						"key_type": "p384",
						"must_staple": true
					}
				]
			}
		}
	},
	"logging": {
		"logs": {
			"default": {
				"level": "DEBUG"
			}
		}
	}
}

6. Links to relevant resources:

Just the official doc! Not a lot of people are asking about the cache module yet, since it is a WIP.

I tried with:

"handle":[{"handler":"cache"}]

But it seems like the cache handler is not included by default in the alpine image:

{"error":"loading config: loading new config: loading http app module: provision http: server galactica: setting up route handlers: route 1: loading handler modules: position 0: loading module 'subroute': provision http.handlers.subroute: setting up subroutes: route 2: loading handler modules: position 0: loading module 'subroute': provision http.handlers.subroute: setting up subroutes: route 0: loading handler modules: position 0: loading module 'cache': unknown module: http.handlers.cache"}

I will wait for further progression on the WIP :slight_smile:

To be completely honest, the caching module specifically will probably never be done or “ready” until people contribute to its development, since I doubt I’ll put much more time into it unless a customer needs it at this point.

I’ve used it locally to test and stuff and it works okayish as-is, but haven’t used it in production so I don’t know about that.

Hmm, according to which doc? Cache should never be last, because if nothing comes after it, then it can’t read the responses from upstream middlewares and cache them. You’ll probably want the handler near the front of your chain. You can read how handlers work here: Modules - Caddy Documentation

requests flow from the first handler to the last (top of the list to the bottom) … Responses flow back through the chain (bottom of the list to the top) as they are written out to the client.

Just so you know, in beta 20 we removed the cache handler from the standard modules and made it a separate project: GitHub - caddyserver/cache-handler: Distributed HTTP caching module for Caddy

So you’ll have to plug it in yourself, which is pretty easy.

That’s so if you have more than one instance, they can talk to each other and share the cache load. The latest HEAD of the cache handler doesn’t require specifying self or peers, as it uses a gosipping algorithm to figure it out automatically. Although, I haven’t tested this because it require setting up multiple machines which is too much work for me right now. :stuck_out_tongue:

1 Like

Thank you for your detailed answer!

My bad, actually my image is beta 20, I forgot I pulled it recently. This is why I got this error message.

No worries, I wanted to test cache just for the fun of it. I do not think I would actually need caching, it is unlikely I will get a massive load of visits. But I managed to make the cache work, I added the cache module with the Caddy builder image :smiley:

Actually yes this is the doc I was referring to. Either I have chosen the wrong set of words or I understood all of the concept backwards.

From top to bottom in my json conf, my current route is:
encode
headers
reverse proxy

In my test, I put cache between encode and headers.

Is my understanding correct?

Things became a bit confused now, I do not know if I made my whole route correctly. I have 2 reverse proxies for the same host: one for the domain, one for a specific path of the domain. What would be the most clean way to set up a chain? Make 2 separate routes or use a subroute within the main route for the /path/ reverse proxy?

Maybe I should open a different topic, as this is not about cache.

I think you could even put cache at the very beginning (top) of your list, since the cache doesn’t read into the bytes, it just stores an opaque blob of bytes, whether they are compressed or not. If you cache it, you don’t have to do the compression again. But then again, there are nuances here that might not be implemented yet, like whether the client supports compression :wink: hence the WIP.

First, major props to you for diving into the JSON to try to understand it better. Way to be fearless!

One thing I’ll often do is make a Caddyfile that’s pretty close to the config I want, then use caddy adapt to make the JSON and tweak it further if I need to. It doesn’t always produce the most minimal JSON possible, but it’s usually pretty good.

I mean, if the proxies are different, they will need to be configured differently (unless you can use placeholders, which are like variables, in any of the fields, to achieve the different configs in a single place)…

1 Like

This topic was automatically closed 30 days after the last reply. New replies are no longer allowed.