Published on

Caddy: Imports

Authors

Welcome back! We're going to examine one of Caddy's most powerful features: Imports

What are imports?

An import is a Caddy directive which includes a snippet or a file, replacing the directive with the contents. Sounds kind of complicated - but it's not. Let's say that this is my file structure:

Caddyfile
imports/
--- global
--- headers
--- routing
--- static
sites-enabled/
--- (Use symlinks - example: 'ln -s /etc/caddy/sites-available/landing /etc/caddy/sites-enabled/landing')
sites-available/
--- landing
--- application
--- landing-beta
--- newsletter
--- analytics

Note that all of the files you see here are just text files, so you could put on an arbitrary file extension if it helps (e.g. .cfg). Caddy does not care.

Order matters

Here's our Caddyfile - this is the one that we call with the Caddy application itself.

Caddyfile
import imports/global
import imports/routing
import imports/headers
import imports/static
# Import all files in sites-enabled
import sites-enabled/*

Notice our 6 lines of configuration are ordered specifically: We want our global file loaded first, then our routing file, then headers, then static. The reason for this is because we may want to import a snippet from the headers file in the static file, for example. If the headers file is imported after the static file, then the configuration won't load.

In our global file, we're going to create our global options block.

imports/global
{
	email certificates@company.org
	cert_issuer zerossl thisismysupersecretapikey
	log default {
		format json
		level info
		output file /var/log/caddy/caddy.log
	}
	servers {
		metrics
	}
}

Looks great! Keeping our configuration set up in organized blocks like this makes editing so much easier. Let's start reusing imports now.

Creating reusable imports

So far, we've imported full files, but not snippets. You can define a snippet by creating a name and surrounding it in parentheses:

Snippet
(snippet-name) {
	# config goes here!
}

We can put this to use in tons of scenarios, but the first one we'll cover is defining your upstream once and importing that for multiple sites.

imports/routing
(proxyto-chicago) {
	reverse_proxy {
		to ord1.company.org ord2.company.org
		lb_policy round_robin
		lb_retries 3
		fail_duration 30s
		max_fails 3
		unhealthy_latency 700ms
		header_down X-Routed-Via ORD
	}
}

These ~8 lines of configuration define a snippet that we can import on any site. All requests to the site will be load-balanced and proxied between two upstream servers, ord1.company.org and ord2.company.org, with some health check parameters and a custom header.

Let's import it on our landing page:

https://www.company.org,
https://company.org {
	import proxyto-chicago
}

We'll also import it on our landing-beta page:

https://landing-beta.company.org {
	import proxyto-chicago
}

We can use this same import on more than one site, as we can see above. Love this for us! But let's amp it up and introduce some arguments.

Using arguments to customize imports

Arguments are awesome - there's no other way to say it. Let's give you an example first, since this one can get a little complicated.

imports/routing
(loadbalancer) {
	reverse_proxy {
		to {args[1:]}
		lb_policy round_robin
		lb_retries 3
		fail_duration 30s
		max_fails 3
		unhealthy_latency 700ms
		header_down X-Routed-Via {args[0]}
	}
}

(proxyto-chicago) {
	import loadbalancer ord ord1.company.org ord2.company.org
}

In this configuration, we've created an import snippet called 'loadbalancer' that takes some arguments. On line 3 of the snippet, we see that we're directly referencing args by using {args[]}. In our case, we want to reference all arguments, starting with position 1, so we use {args[1:]} meaning 'starting with 1, give all arguments'. There are lots of options for selecting arguments in the range:

{args[1]} where n is the 0-based positional index of the parameter:
import name [arg0] [arg1] [arg2] [arg3] [arg4] [arg5] (... forever)
{args[:]} where all the arguments are inserted
{args[:5]} where all arguments before the  5th argument are inserted
{args[6:]} where all arguments, beginning with the 6th argument, are inserted
{args[n:m]} where the arguments in the range between n and m are inserted

Near the bottom of the snippet, we reference a single argument in position 0. {args[0]} refers to the argument in position 0 - again, arguments are 0-based, not 1.

When we import it for use, we start with the import directive, then call the import by name loadbalancer. Next up is position 0 ord, in our case, this is just a loadbalancer name we're adding to a header for troubleshooting purposes. In the following positions, we can place as many upstreams as we want, since we called the arguments as a range {args[1:]}.

Continue Reading

Don't stop there! There are countless excellent examples using Imports and Snippets on Caddy's official documentation site.

Wrapping up

Imports are an incredible way to save time when writing large or complicated Caddyfile configurations. Creatively used, imports and snippets could be combined with other Caddy directives to create simple web applications - all within the Caddyfile!

Want to learn more about Caddy?

Visit their official website at caddyserver.com Read through the docs at caddyserver.com/docs If you use it, sponsor the project (we do!) caddyserver.com/sponsor

This post is one of a series of tutorials about Caddy Server. You can view all relevant posts here.

Learn about Skip2

Sign up for our newsletter

Get Started