Graphic showing the logo of the Caddy web server project

Caddy is a popular modern web server engineered for high performance and memory safety. It’s written in Go, runs with no dependencies, features built-in support for static site rendering with Markdown, and offers automatic HTTPS.

Caddy’s focused on providing a simple server management experience that gives you useful functionality by default. It can be easier to configure and maintain than rival systems such as Apache and NGINX. In this article, we’ll show how to get your own server running with minimal set up by using Docker with the official Caddy image.

Selecting an Image Tag

The Caddy image comes in a few different flavors. The latest version of Caddy is shared by all which is v2.4 at the time of writing. You can use 2.4.x (replacing x with a specific patch version), 2.4, or 2 to pin to the major, minor, or patch component.

Caddy works with Linux and Windows Docker hosts. Alpine Linux, Windows Server Core 1809, and the Windows Server Core 2016 LTSC release are the current operating system options. Referencing a bare Caddy tag such as caddy:2 will select the appropriate image for your platform; you can use variations like 2.4-alpine or 2.4.6-windowsservercore-1809 instead to be more explicit.

Starting a Basic Server

Caddy ships in a ready-to-run configuration. The Docker image will serve your web content from the /usr/share/caddy directory. You can add your files to the container by mounting a host directory to this path.

Caddy also has separate config and data directories which you should mount Docker volumes to. The /config directory is optional but recommended; it stores your config files but as these are converted to API requests, they don’t strictly need to be persisted. The /data location is vital as it holds Caddy-generated TLS certificates, private keys, and the final server config that’s been processed by the API.

docker run -d -p 80:80 
    -v ./my-website:/usr/share/caddy/ 
    -v caddy-config:/config
    -v caddy_data:/data
    caddy:2

image of starting a Caddy web server container with Docker

As an HTTP server, Caddy listens on port 80 by default. This is bound to port 80 on your host via the -p flag in the example above.

image of the default Caddy web server landing page

Now you can visit http://localhost in your browser to access your site. You should see the index.html from your mounted content directory. You’ll get the default Caddy landing page if you didn’t bind anything to /usr/share/caddy.

Setting Up HTTPS

One of Caddy’s headline features is its automatic TLS support so we’d be remiss not to use it. You need to bind port 443 in addition to port 80 so Caddy can receive HTTPS traffic. The only other change is to supply the domain name your site will be served on. Everything else is handled by Caddy.

docker run -d -p 80:80 -p 443:443 
    -v ./my-website:/usr/share/caddy/ 
    -v caddy-config:/config
    -v caddy_data:/data
    caddy:2 caddy file-server --domain example.com

The command after the image name is passed by docker run to the Docker image’s entrypoint. In this case, it means caddy file-server ... is executed as the container’s foreground process. The --domain flag is used to set the domain that Caddy will acquire an HTTPS certificate for. You should check you’ve got an A-type DNS record referencing your Docker host’s IP before you start the container.

Adding Your Own Caddyfile

Caddy normally uses a config file called Caddyfile for defining routes and changing server settings. The Docker image loads the Caddyfile at /etc/caddy/Caddyfile. Mount your own file to this path to override the default settings that serve /usr/share/caddy:

docker run -d -p 80:80 -p 443:443 
    -v ./Caddyfile:/etc/caddy/Caddyfile
    -v caddy_data:/data
    caddy:2

Here’s a simple Caddyfile for a site called example.com with HTTPS enabled:

{
    email example@example.com
}

example.com {
    respond "It works!"
}

This minimal config sets the global email address to example@example.com. This email will be used when making Let’s Encrypt certificate requests. The example.com block provides routing rules for Caddy’s handling of requests to your domain. In this case, the server will always respond with a static message.

More information on the Caddyfile is available in the Caddy documentation. Docker doesn’t change anything here: as long as your file’s available at /etc/caddy/Caddyfile, Caddy will load and use it.

Creating Docker Images For Your Sites

So far we’ve looked at ad-hoc Caddy usage by starting containers straight from the Caddy base image. In practice it’s more likely you’ll want to create dedicated images for your sites so you don’t have to mount your content each time you start a container.

The Caddy base image is ready to extend with your own instructions for adding content and configuration. Here’s an example Dockerfile which includes a Caddyfile and copies your site’s content to a customized directory:

FROM caddy:2.4
WORKDIR /my-site
COPY Caddyfile /etc/caddy/Caddyfile
COPY *.html ./
COPY *.css css/
COPY *.js js/

Now you can build and run your image to start a Caddy server that’s preconfigured for your site:

docker build -t my-site:latest .
docker run -p 80:80 -p 443:443 -v caddy_data:/data my-site:latest

Adding Extra Modules

With many sites you’ll want to include extra Caddy modules for additional functionality. The best way to handle these in your Dockerfile is via Caddy’s dedicated builder image. This includes tooling needed to put together a custom Caddy instance with specified modules installed.

Docker’s multi-stage builds are ideal for this workflow. Here’s an example that adds Caddy’s replace-response module so you can rewrite portions of response data using rules in a Caddyfile:

FROM caddy:2.4-builder AS caddy-build
RUN xcaddy build --with github.com/caddyserver/replace-response

FROM caddy:2.4
COPY --from=caddy-build /usr/bin/caddy /usr/bin/caddy
WORKDIR /my-site
COPY Caddyfile /etc/caddy/Caddyfile
COPY *.html ./
COPY *.css css/
COPY *.js js/

The first build stage produces a Caddy binary with the replace-response module baked in. The xcaddy command available in the builder image places its output at /usr/bin/caddy.  The second stage uses the standard Caddy base image but overwrites the included binary with the custom built one. Your content is then layered in as normal; the result is a Caddy server that incorporates extra modules while retaining full support for the rest of the features in the Docker base image.

Summary

Caddy is a modern web server that’s a great choice for efficiently serving static files. It offers a compelling feature set with first-class support for HTTPS, built-in template rendering, and Markdown integration.

Using Docker to host your Caddy server gives you a quick way to deploy an instance without manually downloading binaries or installing service files. It’s a good way to try out Caddy or run it alongside existing workloads in a cluster setting.

As Caddy can act as a reverse proxy and load balancer, you could use it as an entrypoint to route traffic to your other Docker containers. The popular Caddy Docker Proxy module extends the server’s built-in capabilities with Traefik-like support for automatic route discovery via Docker container labels.

Caddy takes an API-first approach to configuration which simplifies the management of instances running inside a container. You don’t need to worry too much about injecting config files or managing volumes. As long as the /data directory is persisted, you can make API requests to modify Caddy’s operation without having to use the Docker CLI. This can make it a better choice for containerization when compared to more traditional options like Apache and NGINX.

Profile Photo for James Walker James Walker
James Walker is a contributor to How-To Geek DevOps. He is the founder of Heron Web, a UK-based digital agency providing bespoke software development services to SMEs. He has experience managing complete end-to-end web development workflows, using technologies including Linux, GitLab, Docker, and Kubernetes.
Read Full Bio »