Using Caddy Server with Docker

February 19, 2019

Overview

Caddy is an HTTP/2 web server with automatic HTTPS management. It can serve static files, but it can also be used as a reverse proxy for a Node.js application, for example. Its configuration is also very simple. You can visit this page to see all of its features.

In this article, we will look at several usage examples:

  • Use for a static site.
  • Use for a Node.js application.
  • Use for a static site that communicates with a Node.js application.

We will see how to use it with Docker in a local environment, and also how to use it on a server with a domain and HTTPS.

Before starting, we will create a CaddySamples folder that will contain our different examples:

mkdir CaddySamples

Using it for a static site

Let's start with the most basic case: using the web server to serve a static site. Add a static-website-sample folder:

mkdir static-website-sample

In this example, we will have three files:

  • docker-compose.yml: the configuration file for our Docker services.
  • Caddyfile: the Caddy configuration file.
  • /src/index.html: the web page we want to serve.

Now add the index.html file in the src folder with the following content:

<!DOCTYPE html>
<html>
  <head>
    <meta charset="utf-8" />
    <meta http-equiv="X-UA-Compatible" content="IE=edge" />
    <title>Caddy Web Server</title>
    <meta name="viewport" content="width=device-width, initial-scale=1" />
    <link rel="stylesheet" type="text/css" media="screen" href="main.css" />
    <script src="main.js"></script>
  </head>
  <body>
    <h1>Every Site on HTTPS</h1>
    <h2>Caddy is the HTTP/2 web server with automatic HTTPS.</h2>
    <iframe
      src="https://ghbtns.com/github-btn.html?user=mholt&amp;repo=caddy&amp;type=star&amp;count=true&amp;size=large"
      scrolling="0"
      class="github-stars"
      width="160px"
      height="30px"
      frameborder="0"
    ></iframe>
  </body>
</html>

Let's move on to the Caddy configuration. As you saw just above, Caddy is configured with a file named Caddyfile. In our case, we want to access the contents of our src folder through our local Caddy server on port 80.

Create the Caddyfile file and add the following configuration:

localhost:80

Here, our configuration file tells the server that we want to access the contents of the server's root folder through localhost:80. The Docker image we will use has /srv as its root folder. If you are not using Docker, or if you are using a different image, you can specify the root folder with the following configuration:

localhost:80 {
    root /www/mysite.com
}

Now all that remains is to install Caddy and run it. To do that, we will use Docker and docker-compose. The Caddy Docker image used is this one.

Add a docker-compose.yml file with the following content:

version: '2'
services:
  server:
    image: abiosoft/caddy
    container_name: caddy
    ports:
      - '80:80'
    volumes:
      - './Caddyfile:/etc/Caddyfile'
      - './src:/srv'

For this first example, we could skip docker-compose and run the container directly with Docker, but this will serve as a base for the rest of the article.

Here:

  1. We create a service named server from the abiosoft/caddy image.
  2. We map port 80 of the container to port 80 of our host.
  3. We specify the configuration file to use through the './Caddyfile:/etc/Caddyfile' volume.
  4. We map the src folder that contains our page to the container's root folder, which is srv.

Now all that remains is to run the command:

docker-compose up

And what about HTTPS?

For HTTPS, the configuration will not change much, but first let's assume that you have the following:

  • a server (if you do not have one, you can use DigitalOcean).
  • a domain (for our example, we will use example.com) and a subdomain (api.example.com).
  • a DNS record that points your domain and subdomain to your server's IP address.

If you have all of the prerequisites, the first thing to do is modify the Caddyfile configuration file as shown below:

example.com

We simply replaced localhost:80 with the domain where our site will be accessible once deployed. To finish, we modify the docker-compose.yml file by adding these values:

ports:
  - '80:80'
  - '443:443'
volumes:
  - './Caddyfile:/etc/Caddyfile'
  - './src:/srv'
  - './caddy:/root/.caddy'

For HTTPS, you need to add the mapping for port 443. Then we add a new volume that lets us persist the SSL certificates generated by Caddy at startup. If we do not add this volume, the certificates will be recreated every time the container is recreated.

Using it for a Node.js application

Now we will move on to using it as a reverse proxy for a Node.js application.

The goal of this article is not the Node application itself, so we will go through this part quickly.

At the root of the CaddySamples folder, create a node-js-sample folder and add an api folder inside it:

mkdir node-js-sample && cd node-js-sample
mkdir api && cd api

Then, in the api folder, start by adding the package.json file:

{
  "name": "node_js_app",
  "version": "1.0.0",
  "description": "Node.js",
  "author": "tiby",
  "main": "server.js",
  "scripts": {
    "start": "node server.js"
  },
  "dependencies": {
    "express": "^4.16.1"
  }
}

Then a server.js file:

'use strict'
 
const express = require('express')
 
// Constants
const PORT = 3000
const HOST = '0.0.0.0'
 
// App
const app = express()
app.get('/', (req, res) => {
  res.send('Hello world node js\n')
})
 
app.listen(PORT, HOST)
console.log(`Running on http://${HOST}:${PORT}`)

And finally a Dockerfile file:

FROM node:8
WORKDIR /usr/src/app
COPY package*.json ./
RUN npm install
COPY . .
EXPOSE 3000
CMD [ "npm", "start" ]

Let's create the docker-compose.yml file. Add it in the node-js-sample folder:

version: '2'
services:
  api:
    build: ./api
    container_name: 'node-api'
    restart: always
    ports:
      - '3000:3000'
  server:
    image: abiosoft/caddy
    container_name: caddy
    ports:
      - '80:80'
    links:
      - api
    volumes:
      - './Caddyfile:/etc/Caddyfile'

We have two services:

  • api: our Node application built from our Dockerfile.
  • server: our Caddy web server.

Now you can add the Caddyfile file:

localhost:80 {
  proxy /  api:3000
}

Here, we tell Caddy to redirect requests arriving on localhost:80 to api:3000. api is the name of our node-js service in the docker-compose.yml file.

As in the previous example, all that remains is to run the command:

docker-compose up

HTTPS setup is done exactly the same way as in the previous example.

Using it for a static site that communicates with a Node.js application

Now we will set up the two previous examples together and make them communicate with each other: the static page will make an HTTP call to our API, and the API will respond.

The static page will be accessible through localhost:80 (example.com), and the API through localhost:8080 (api.example.com).

To do this, in the CaddySamples folder, create a static-website-and-node-sample folder, then add a web folder inside it.

mkdir static-website-and-node-sample && cd static-website-and-node-sample
mkdir web

In the web folder, copy the index.html file from the first example. Then, in the static-website-and-node-sample folder, copy the api folder from the previous example.

Now we will modify our HTML page to add the script that will make an HTTP request to our API. In the index.html page, add the following script before the end of the body:

<div style="background-color:green" id="data"></div>
<script>
  fetch('http://localhost:8080/test')
    .then(data => {
      return data.json()
    })
    .then(jsonData => {
      var element = document.getElementById('data')
      element.innerHTML = JSON.stringify(jsonData)
      console.log(jsonData)
    })
    .catch(err => {
      alert(err)
    })
</script>

Then we add the test route to our server in the server.js file:

app.get('/test', (req, res) => {
  res.header('Access-Control-Allow-Origin', '*')
  res.header('Access-Control-Allow-Headers', 'Origin, X-Requested-With, Content-Type, Accept')
  res.send({ hello: 'world' })
})

Let's move on to the Caddyfile configuration:

localhost:80 {
}
 
localhost:8080 {
  proxy / api:3000
}
 

Here, we can see that the file contains the configurations from each of the previous examples.

Then the docker-compose.yml file:

version: '2'
services:
  api:
    build: ./api
    container_name: 'node-api'
    restart: always
    ports:
      - '3000:3000'
  server:
    image: abiosoft/caddy
    container_name: caddy
    ports:
      - '80:80'
      - '8080:8080'
    volumes:
      - './Caddyfile:/etc/Caddyfile'
      - './web:/srv'

Now, when you launch your services with Docker, they will be available on ports 80 and 8080 respectively.

That's it: we have seen a small part of Caddy Server's features.

Each example above includes a localhost version and can be adapted to use the example.com domain.