DEV Community

Migsar Navarro
Migsar Navarro

Posted on

First steps with Caddy

As part of my effort to securely host my own Filestash instance I've been playing around with Caddy in an effort to better understand how to use it to serve as a very basic gateway for my server.

Caddy configuration can be quite complex, from what I've seen in other places. Happily, it can also be quite simple and straightforward for the more basic scenarios. In any case, it is good to learn some of the concepts behind caddy's configuration, those will be very helpful for any kind of use case, but I feel the docs can be overwhelming sometimes.

Caddy has three ways of configuring it:

  • Caddyfile
  • caddy.json
  • API

The more expressive format and the one used under the hood is the JSON configuration, but it can be harder to read than the Caddyfile. The API and the JSON file offer more flexibility for automated configuration. In this post I will only discuss the Cadyfile.

It is possible to automatically generate the JSON file from the Caddyfile using a the adapt command that is provided as part of the CLI:

caddy adapt -pc ./Caddyfile

Here, the -p option means pretty, to get the JSON format with newlines and tabs, although I don't really like it since I am used to 2-space indentation, the -c option is to explicitly refer to the file used, if omitted it looks for a Caddyfile in the current directory.

Building blocks of a Caddyfile

The Caddyfile is composed of blocks and directives, you can see a more detailed introduction in caddyfile :: concepts section of the docs. Blocks group directives, and directives are functional keywords, that is, can be translated into actions to take, a list of directives is available here. The last fundamental concept to understand is the matchers, as the name suggests the matchers match something, in Caddy thay can match several different things, like paths, methods, query variables, etc.

With these three things we can do a lot. We will often see three directives in the examples, respond that sends something as a response to the client, reverse_proxy that forwards the requests to another server, and file_server that is for static files server.

Matchers

Now I feel it very intuitive, but I still remember a few days ago I was a bit confused with the concept of a matcher, since I was expecting something more rigid. I was thinking about matching strings, paths in particular, and then considering the rest of the stuff, like headers, cookies, or other request properties, and I think all of this was because that would be the way I would have structured the problem in javascript, or other similar routers for servers in which you have middleware to which a request and a response objects are passed.

Caddy's matchers are way more flexible by placing many of the request properties at the same level as the path, so you can create a matcher for ip, headers, host, path, method, protocol, or query among other things. This allows for an incredible flexibility. You can, for example:

  • Use paths to decide which server handles it.
  • Serve a difference response based on ips.
  • Have different services based on headers or query variables.
  • Use a host matcher for subdomains
  • Handle websockets using protocol and headers.

It took me some time to wrap my head around this concept, but once I understood how it worked I was amazed by the simplicity and power it provides.

Super basic auth with a matcher

This is NOT something you should use for production, but I was looking for a quick way to protect my server, and I found that using a header was so straightforward that is not even documented.

Simple HTTP auth is also included by default and it may be more secure, although it is very similar in my opinion, you can also use cookies or ip following the same approach. For me, what was important was not having to create some HTML form and have some server code to process it, but I need to say it one more time, I wanted a quick solution for me as a single user, with my Proof-of-Concept, it is not something I would use for something more serious.


localhost:3000 {
    @protected {
        path /protected/*
        header token here-is-my-token
    }

    respond @protected "I'm protected"
    respond "I'm public"
}
Enter fullscreen mode Exit fullscreen mode

What happens here is that we use a named matcher, the block defined with @protected in one of the respond directives, this works like an if that will only execute when the conditions of the matcher are met.

Top comments (0)