Skip to content

Instantly share code, notes, and snippets.

@brucejo75
Last active July 6, 2024 11:22
Show Gist options
  • Save brucejo75/03202b16d362d168ca81069e0441146f to your computer and use it in GitHub Desktop.
Save brucejo75/03202b16d362d168ca81069e0441146f to your computer and use it in GitHub Desktop.

Variables in the Caddyfile

Caddy is a super webserver that has many useful features. Caddy can enable very very powerful scenarios and many of them are documented in this Wiki. As these scenarios become more elaborate (some might say complex!) writing a caddy config file starts to feel more like programming than basic configuration.

When that starts to happen I find myself reaching out for variables to enable multiple scenarios in a single configuration file by manipulating those variables.

For those situations caddy has a few types of variables to consider. What I hope to do here is illuminate how to use these variable types in the Caddyfile.

A note about examples

I will use examples to illustrate the concepts. I use the respond directive to verify how caddy works.

All the caddy example configurations are in a file named Caddyfile.

Open a command window and you can run caddy with caddy run.

In another command window you will be able to execute the caddy reload and curl commands.

Placeholders

You can read about placeholders in the Conventions#placeholders section of the docs and more infortion in the Concepts#placeholders section of the docs.

Note: Placeholders are generally usable anywhere there is a text field you would like to substitute. But... not every field can substitute placeholders today. The team has worked hard enabling placeholders in as many places as they thought they would be super useful, but not all fields. If you run into a field that you think should be enabled for placeholders simply add an issue to caddy.

OK, let's see them in action!

Simple reference

Let's just respond to a query with a string that has substitutions.

# My Caddyfile...
:2022 {
  respond "{time.now}:{system.os}:{system.arch}"
}

Remember to caddy run in it's own command window 😸.

Now that we have changed the Caddyfile we can reload it with the caddy reload command and execute the curl command.

$ caddy reload && curl localhost:2022

Expected output (something like):

2022-03-20 12:26:34.55429 -0700 PDT m=+59.288837501:linux:amd64

Great, I can see the time, my OS & my architecture separated by ":".

This just showed how you can reference placeholders within a Caddyfile.

Setting a custom placeholder with map

There are a couple of ways to set your own custom placeholders. Lets start with a map.

# My Caddyfile...
:2022 {
  map 1 {my_customvar} {
    default "this_is_my_custom_var"
  }
  respond "{my_customvar}"
}

outputs: "this_is_my_custom_var" Great we have set our own custom variable.

Using custom placeholders inside a snippet

Snippets can process arguments that are passed to the import directive so this is a form of setting variables also.

# My Caddyfile...
# declare mycustomargs snippet
(mycustomargs) {
  respond "arg0: {args.0} arg1: {args.1}"
}

:2022 {
  import mycustomargs my_argument1 my_argument2
}

outputs: "arg0: my_argument1 arg1: my_argument2" Slick! The arguments can only be referenced inside the Snippet but this turns out great for some customizations.

Custom placeholder limits

OK, we can set custom placeholders and we can reference them. But hold on, they cannot be referenced everywhere! Lets try to reference a placeholder within a map directive.

# My Caddyfile...
:2022, :2023 {
  map 1 {my_customvar} {
    default "custom1"
  }
  
  map {port} {mynewvar} {
    2022 "custom2022"
    2023 {my_customvar}
  }
  respond "{my_customvar} {mynewvar}"
}

I am trying to create a new custom variable mynewvar in the 2nd map directive. For port 2022 I set it to a string. For port 2023 I set it to the reference of my other custom variable my_customvar.

output from curl localhost:2022 : custom1 custom2022

so far so good, but...

output from curl localhost:2023 : custom1 {my_customvar}

Note: Directive ordering could be an issue here because the order of the 2 map directives is not deterministic. In this case it works in file order. But as you start to work on your Caddyfile it is important to know that the order of directives is not deterministic for multiple instances of the same directive.

To be more explicit we could have ordered the map directives in a route directive.

hang on, the custom variable my_customvar is not evaluated in the 2nd map configuration. It just set mynewvar to "{my_customvar}".

In Version v2.4.6 This is one of those places where placeholder substitution is not supported (yet). As part of writing this article I pointed out that substitution is not happening here and the Caddy team added that capability for later releases. (The team responded very quickly to my questions and suggestions which is a big reason I love working with Caddy).

But when doing this work, I had the thought it would be great if Caddy had a simple substitution method prior to parsing the Caddyfile. So I would not have to completely depend on placeholders.

How could I do that?

Caddy Environment variables are a little different in that they only work in a Caddfyfile (not a JSON config file) and they perform the substitution before the file is parsed by caddy to create the webserver.

Using environment variables

# My Caddyfile...
:2022, :2023 {
  map {port} {mynewvar} {
    2022 "custom2022"
    2023 {$MYENVVAR}
  }
  respond "{$MYENVVAR} {mynewvar}"
}
$ MYENVVAR=custom1 caddy reload && curl localhost:2023
custom1 custom1
$ curl localhost:2022
custom1 custom2022

When I hit port 2023, "{$MYENVVAR}" is substituted with the value of your environment variable MYENVVAR and it even works on the parameters of the map directive. So now when I hit localhost:2023 map will set {mynewvar} equal to the subtitution for {$MYENVVAR}. Pretty nice, now I could paramaterize the parameters in a map, yay!

BTW! Setting the environment variable with a reload works. That is what I call a "surprise & delight", it just kind of works like you hope it might. (I was not sure it would work... thinking I might have to restart the first command window.)

Unintialized environment variables & defaults

We can set a default value for any environment variable substitution, but it is not a global substitution for all variable references.

# My Caddyfile...
:2022, :2023 {
  map {port} {mynewvar} {
    2022 "custom2022"
    2023 {$MYENVVAR:defaultValue}
  }
  respond "{$MYENVVAR} {mynewvar}"
}
$ MYENVVAR=custom1 caddy reload && curl localhost:2023
custom1 custom1
$ curl localhost:2022
custom1 custom2022

Still works the same, but what if I do not set the environment variable?

$ caddy reload && curl localhost:2023
 defaultValue
$ curl localhost:2022
 custom2022

This makes sense... MYENVVAR is not a defined environment variable. The value for {$MYENVVAR} is substituted with the value "defaultvalue" specified in the map parameters & {mynewvar} gets set with that default substitution. But the {$MYENVVAR} has no default in the response, so the empty string (value of the MYENVVAR environment variable) is substituted there.

That's a wrap for now!

Other topics for possible later post(s)

  • What about variables in Caddy JSON configuration files?
  • How does an expression directive interact with variables?
  • How is a Named Matcher like a variable?
  • What is this vars directive, how does it relate to variables?
@brucejo75
Copy link
Author

@mholt,

I just need to push a commit that evaluates placeholders in the table so they're not only static. I will push that today probably.

So if you make that change, then my placeholder example4 would work.

Maybe I should hold this article until you finish up with any changes you are doing?

@brucejo75
Copy link
Author

brucejo75 commented Mar 21, 2022

@mholt, updated with the feedback...

@mholt
Copy link

mholt commented Mar 22, 2022

@brucejo75

So if you make that change, then my placeholder example4 would work.

It should, yes. You can try it out if you want. (Just build from the latest tip.)

updated with the feedback...

Cool, but I think something was misunderstood. I mean you can remove the -config and -adapter flags entirely and just leave the commands as caddy run and caddy reload.

@brucejo75
Copy link
Author

@mholt,

Cool, but I think something was misunderstood. I mean you can remove the -config and -adapter flags entirely and just leave the commands as caddy run and caddy reload.

😸 Read a little more closely... I used this as an opportunity to explain/reinforce that those arguments are unnecessary if you refer to a Caddyfile in your own folder. The entire section...:

Assuming you copy the examples into a file named Caddyfile the caddy command is:

caddy run --config ./Caddyfile --adapter caddyfile

As an alternative you can also run caddy in the background with:

caddy start --config ./Caddyfile --adapter caddyfile

But it turns out that the default arguments for caddy run & caddy start are -config ./Caddyfile and --adapter caddyfile so all you need to enter is:

caddy run
--or--
caddy start

@brucejo75
Copy link
Author

brucejo75 commented Mar 22, 2022

@mholt, PS: what is a tip? And how do I use/get the latest version with it? Thanks!

@francislavoie
Copy link

francislavoie commented Mar 22, 2022

"tip" refers to the latest commit in the repo, you can run xcaddy build master to build from the latest commit on the master branch (which is what Matt called "tip") https://caddyserver.com/docs/build#xcaddy

General comment, Caddyserver -> Caddy; the program is called "Caddy", and it happens to be a server (and the domain has server in it because caddy.com was taken). For example (among many):

- to verify how `caddyserver` works
+ to verify how Caddy works

Also "the Caddyfile" is the better way to say it; you're never working with many of them, just the one file. And being consistent about it and not pluralizing reinforces that it matches the name of the file on disk:

- What I hope to do here is illuminate how to use these variable types in Caddyfiles.
+ What I hope to do here is illuminate how to use these variable types in the Caddyfile.

When giving command examples, use $ instead which is a better indicator that it should be run in a terminal:

- > caddy run --config ./Caddyfile --adapter caddyfile
+ $ caddy run --config ./Caddyfile --adapter caddyfile

FWIW I'm not a big fan of recommending caddy start because 99% of the time users should be running Caddy as a service, and if they installed it with the apt repo or whatever, running caddy start will conflict with the instance running as a service. Also, you may lose out on Caddy's log output, which is usually very important for debugging issues. There's just less "asterisks" involved when just saying "use caddy run", less ways for the user to shoot themselves in the foot. I also don't think it's relevant to this wiki article to mention that. It should be the job of the official docs, or other guides, to cover how to actually run Caddy.

map 1 {my_customvar}

I think it's worth mentioning that you might run into trouble if you use a variable name that overlaps with any of the Caddyfile placeholder shortcuts as per https://caddyserver.com/docs/caddyfile/concepts#placeholders because the Caddyfile adapter will rewrite {port} to {http.request.port} for example, before outputting the JSON, whereas you might expect to be able to use a variable named {port} (and expect it to remain as {port} in the JSON output), but you can't do that.

- ### placeholder example 4: custom placeholder limits
+ ### Custom placeholder limits

I don't think it does much to number your examples like that, I think you can just make a ## Placeholder examples section and then put those within, basically.

I am trying to create a new custom variable mynewvar in the 2nd map directive. For port 2022 I set it to a string. For port 2023 I set it to the reference of my other custom variable my_customvar.

Another thing to note, the sorting order of directives of the same type is not guaranteed, it may not be sorted according to the order you wrote it in your config. If you need to make sure one runs before the other, you'd need to wrap it in a route. But keep in mind that route itself has its own directive order, and it's much lower than map's by default, so it might cause your maps to run too late. I'd just caution against trying to make one map chain into another (or one vars handler into another)

https://caddyserver.com/docs/caddyfile/directives#directive-order

When I hit port 2023, "my_envvar" is substituted with the value of env.my_envvar

I think this is misleading -- firstly, I'd recommend to always UPPERCASE environment variables that you're talking about, to avoid confusion with other kinds of user-defined vars/map; Also, env.MY_ENVVAR has no meaning on its own, that's not really a thing, because the placholder is {env.MY_ENVVAR} (with the braces), but also that is only replaced at runtime which is a separate concept altogether. And quoting "my_envvar" is also misleading because you'd never quote it like that in your Caddyfile, it would be written as {$MY_ENVVAR}.

@mholt
Copy link

mholt commented Mar 22, 2022

@brucejo75

smile_cat Read a little more closely...

Ha, thanks. I did indeed skim it when I was pretty tired. However, I think it's still best to remove that information because it's unrelated to the topic, and if it confused me, it'll definitely confuse others who aren't familiar with the software. I don't want people thinking that they ever need to write those out in their commands. Since it's unrelated to what the article is about, and it will most likely only give users the wrong impression, I vote that those unnecessary and anti-pattern commands be removed... 🤷‍♂️

PS: what is a tip? And how do I use/get the latest version with it? Thanks!

Tip is the latest commit on master: https://github.com/caddyserver/caddy/commits/master

Instructions here: https://caddyserver.com/docs/build

@brucejo75
Copy link
Author

brucejo75 commented Mar 22, 2022

@francislavoie, great feedback & observations. I will work on incorporating.
@mholt, good point. Not the main topic of this article. I was thinking that as I wrote it too.

Great to have more 👀 on the words! Thanks!

@mholt
Copy link

mholt commented Mar 22, 2022

Sounds good, thanks for working on this!

@brucejo75
Copy link
Author

Updated, now I will wait until you release the version that has the vars directive available.

@mholt
Copy link

mholt commented Mar 23, 2022

@brucejo75 Cool, nice work!

With v2.5 (the next release), you can replace all instances of map that look like this:

  map 1 {my_customvar} {
    default "this_is_my_custom_var"
  }

with this:

vars my_customvar this_is_my_custom_var

(Although, again, you might have to double-check, but the resulting placeholder might be {http.vars.my_customvar} instead of {my_customvar}. But maybe we can iron that out before the release if needed.

@francislavoie
Copy link

francislavoie commented Mar 23, 2022

But as you start to include other directives it is important to know that the order of directives is not necessarily guaranteed unless you use a route directive.

That's not accurate, the order is guaranteed if you use different directives, but the order of same-directives is not guaranteed.

So if you use two map then the order of those two is not guaranteed, but if you use map and vars (or whatever combination of two different directives) then those will be guaranteed.

If you use matchers on these directives, then the matcher will be used when sorting, so matchers with the longest path matcher will go first, then any other matcher afterwards, and ones with no matcher at all go last. The idea is that the "most specific" matcher should be first.

(Although, again, you might have to double-check, but the resulting placeholder might be {http.vars.my_customvar} instead of {my_customvar}. But maybe we can iron that out before the release if needed.

I plan to work on this in the coming days

@brucejo75
Copy link
Author

@mholt,

Interesting issue with regards to "http.vars" variables, e.g.:[http.vars.variable} v. "top-level"? custom placeholders, e.g.: {my_custom_placeholder}.

How you solve it will be interesting!

@francislavoie,

That's not accurate, the order is guaranteed if you use different directives, but the order of same-directives is not guaranteed.

D'oh! Thanks for the catch. Maybe I should just place the 2 map example within the route directive to bring home the point?

@francislavoie
Copy link

Maybe I should just place the 2 map example within the route directive to bring home the point?

You could, but then it might run too late because route itself is ordered further down the list.

I'd really just strongly suggest avoiding cascading altogether, it's unlikely to ever have a clean way to do it.

@francislavoie
Copy link

See caddyserver/caddy#4657

So TL;DR, map can set outputs as whatever top-level placeholder name you want, but now prevents you from doing it with a name that might conflict with an existing Caddyfile placeholder shorthand.

For vars, you can now use {vars.*} instead of {http.vars.*}

It won't be possible to make vars actually output to top-level, for implementation reasons. The vars handler is defined as type VarsMiddleware map[string]interface{} so there's no way to add new options to it (unless we do UnmarshalJSON hackery).

The way vars and map work are actually pretty different. The vars handler actually just puts things in a map[string]interface{} (caddyhttp.VarsCtxKey) in the request context, then {http.vars.*} reads from that. Simple ahead-of-time value assignment.

The map handler sets up a callback in the replacer (caddy.ReplacerCtxKey) which gets invoked any time an otherwise unknown placeholder is encountered, and if it matches one of the destination outputs, then the configured map is evaluated to find the matching output. Of note, this means the map will be evaluated each time you use one of the destination outputs, not just once ahead of time.

This means (correct me if I'm wrong @mholt) if you need to use an output multiple times (and you have a huge map with like 10k+ items), it might not be a good idea to use map directly and it might be a good idea to use vars to evaluate the map to "cache" the output values instead (because map has higher order than vars so you can chain map -> vars but not the other way). But that's pretty edgecase-y.

@brucejo75
Copy link
Author

brucejo75 commented Mar 30, 2022

@mholt, @francislavoie,

Hmm, another question for you.

When can I use placeholders? Here was a surprise (to me):

    file_server {
      index "index.{host}.html"
    }

{host} placeholder was not substituted in the string. e.g. my host: example.com:

    file_server {
      index "index.example.com.html"
    }

works, so I am thinking that the placeholder substitution does not work here.

What defines a place where substitution would work? I kind of expect it to work anywhere, but maybe with some exceptions. This is what is I took from the documentation:

Not all config fields support placeholders, but most do where you would expect it.

Thanks!

@francislavoie
Copy link

It's hard to properly document everywhere whether a field supports placeholders or not. We'd probably need to set up this little marker/widget that we'd plop throughout the docs to say whether something does or doesn't support placeholders, but that's a lot of work to go through the code and check each thing to see if they do or not.

But yeah, file_server's index that is one spot that likely doesn't right now, but probably not for any particular reason. It's just that there needs to be a line of code that actually triggers the placeholder replacement to happen at runtime, and it didn't get done for that one, because nobody's had a need for it there yet.

If you want to open an issue requesting support for it there to remind us, we can add it when we have a few minutes.

@mholt
Copy link

mholt commented Mar 30, 2022

(Just seeing this again)

@francislavoie is right about the differences between vars and map. Although, I want to clarify something that I think I'm remembering correctly:

Of note, this means the map will be evaluated each time you use one of the destination outputs, not just once ahead of time.

This means (correct me if I'm wrong @mholt) if you need to use an output multiple times (and you have a huge map with like 10k+ items), it might not be a good idea to use map directly and it might be a good idea to use vars to evaluate the map to "cache" the output values instead (because map has higher order than vars so you can chain map -> vars but not the other way). But that's pretty edgecase-y.

Yes, map calls a function each time a custom placeholder is looked up. However, no (or only very few) allocations are made unless a match is found, in which case only the output value is allocated. See this PR (linked above in a previous comment as well): caddyserver/caddy#2674

So the map function being called is very cheap even for large maps, assuming the callback function is implemented efficiently (usually a switch statement which of course is mostly just a JMP or JE in assembly so it can be very fast like that).

I am not so sure about the performance of vars, it might be worse, or might be about the same. I haven't benchmarked it. 🤷‍♂️ Could be a good exercise left to the reader 😄

@brucejo75

When can I use placeholders?

The unfortunate answer is, anywhere the developer (often me, oops) added support for them. In general, placeholders can be used in most fields where you might expect them, but every so often we find places where users are putting placeholders that we didn't anticipate, so just open an issue to request it. When I wrote the index feature of the file server, I didn't anticipate people using dynamic values there; like it wasn't obvious that someone would want a dynamic value for a file index. Usually it's a pretty static index.html. So things like that we just wait for feature requests.

And yeah, it's not really worth documenting all the fields that do or don't support placeholders. Mainly because most fields can support placeholders even if they don't right now, so no need to codify one way or the other. And if we're going to cover the whole code base and document thusly, we might as well just add placeholder support while we go along... so. It's just a time savings thing.

Basically if you need placeholders somewhere they aren't expanded already, just request it in an issue.

@brucejo75
Copy link
Author

Thanks @mholt, @francislavoie!

I dropped an issue and I updated the wiki page with a little note:

Note: Placeholders are generally usable anywhere there is a text field you would like to substitute. But... not every field can substitute placeholders today. The team has worked hard enabling placeholders in as many places as they thought they would be super useful, but not all fields. If you run into a field that you think should be enabled for placeholders simply add an issue to caddy.

@mholt
Copy link

mholt commented Apr 7, 2022

Sounds good. Any plans to publish the wiki sometime?

@brucejo75
Copy link
Author

Hi @mholt,

Quite a few changes have happened in v2.5. And this section will be wrong, because you enabled placeholders for maps. I was thinking I would wait until v2.5 released?

Would you rather I post what I have now? And not wait?

@mholt
Copy link

mholt commented Apr 8, 2022

Ah yeah, good point. The post would need to be updated for 2.5, I think that's best, rather than publishing information that will go out of date very soon. When you have a chance to update the post for 2.5, I'll review it happily!

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment