Caddy is an open-source web server that aims for a more secure web as it automatically configures LetsEncrypt for your domains (it also has the support for ZeroSSL starting with v2.2.0 Caddy). It is fast and written in Go, as well as the underlying library for issuing and renewing certificates - certmagic. Besides its speed and auto-ssl, it is also very light, easy to configure and use in Docker/Kubernetes, and very extensible.

One thing that differentiates Caddy from other webservers is its ask endpoint. Basically - it asks the endpoint if it is allowed to issue the certificate for the particular domain and only then issues the certificate for it. This is useful for us at MailerLite and our landing pages feature which allows users to easily create landing pages for them via drag and drop editor and they can use their custom domains for it. As we are also aiming for a more secure web, we are giving out the free certificates for each custom domain users set.

The company of our size has a lot of custom domains used in our landing pages, so we wanted to use some other storage rather than files for certificates. Fortunately, there is a plugin that utilizes Redis for that operation. It is really easy to add plugins to your Caddy installation with Caddy builder. You can find our implementation here. This Dockerfile also includes Cloudflare DNS plugin so you can use dns-01 verification instead of http-01 (useful when running certificates on your local network).

Back to the implementation, we utilize the following caddy configuration:

{
	email [email protected]
	on_demand_tls {
		ask {$CADDY_ASK_HOST}:{$CADDY_ASK_PORT}
	}

	storage redis {
		host          {$CADDY_CLUSTERING_REDIS_HOST}
		port          {$CADDY_CLUSTERING_REDIS_PORT}
		aes_key       {$CADDY_CLUSTERING_REDIS_AESKEY}
	}
}

:8080 {
    respond /healthz "200 ok"
}

:443 {
	tls {
		on_demand
	}
	reverse_proxy {$CADDY_LANDINGS_HOST}:{$CADDY_LANDINGS_PORT} {
		transport http {
			keepalive 30s
		}
	}
}

As you can see, we are utilizing env variables to set our ask endpoint, as well the other config, redis host, and finally, our microservice that servers landing pages. Our stack for landing services contains a couple of microservices - Caddy, Redis, NodeJS landings microservice, and Golang ask endpoint microservice. I won’t get into depth on how we deploy the apps to Kubernetes in the post, but in a nutshell, we are using Helm, Helm Secrets, Skaffold, and GitHub Actions.

To get a better overview of how the flow is, you can check it out in the following image:

Caddy flow

Overall, we are really satisfied with how easy and performant Caddy is, and in version 2.2.0 you can even have native Prometheus integration so you could increase your observability in Grafana, for example.

Let me know in the comments if you have any questions about the implementation.