Using Caddy as a Reverse-Proxy Server

Prerequisites

We’ll use the Caddy modules and adapters we built in our previous section in this post.

What We Will Build

We now have everything we need to create something meaningful in Caddy. We’ll be configuring Caddy to reverse-proxy requests to our backend web service and serve static files on behalf of it. There will be two endpoints: one will serve up static files from Caddy, and the other will handle our requests to our backend service. In turn, this will show you how our web services can rely on Caddy to serve static content on their behalf.

Before we start, we need to set up our directory structure. Right now, you’re currently in the caddy directory which has a caddy binary we built earlier. Create two subdirectories:

$ mkdir files backend

Backend Web Service

In the backend folder created above, create a file called backend.go:

$ touch backend/backend.go

Add the following code:

func run(addr string, c chan os.Signal) error {
    mux := http.NewServeMux()
    mux.Handle("/",
	http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
		clientAddr := r.Header.Get("X-Forwarded-For")
		log.Printf("%s -> %s -> %s", clientAddr, r.RemoteAddr, r.URL)
		_, _ = w.Write(index)
	}),
)
    srv := &http.Server{
	    Addr:              addr,
	    Handler:           mux,
	    IdleTimeout:       time.Minute,
	    ReadHeaderTimeout: 30 * time.Second,
    }
    go func() {
	    for {
		    if <-c == os.Interrupt {
			    _ = srv.Close()
			    return
		    }
	    }
    }()
    fmt.Printf("Listening on %s ...\n", srv.Addr)
    err := srv.ListenAndServe()

    if err == http.ErrServerClosed {
	    err = nil
    }
    return err
}

func main() {
    addr := flag.String("listen", "localhost:8080", "listen address")
    flag.Parse()

    c := make(chan os.Signal, 1)
    signal.Notify(c, os.Interrupt)
    err := run(*addr, c)
    if err != nil {
	    log.Fatal(err)
    }
    log.Println("Server stopped")
    }
var index = []byte(`<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Caddy Backend Test</title>
</head>
<body>
    <h1>Hello from Caddy!</h1>
</body>
</html>`)

If you’ve been following along, a lot of this code is pretty self-explanatory. You’re setting up the service to listen on port 8080 of localhost which Caddy will use to direct requests. Caddy then implements an X-Forwarded-For header for requests that originate from the client’s address. The handler then writes a slice of bytes to the response with the HTML code. Your backend service is now ready to start receiving requests.

Setting Up Caddy’s Configuration

At the root of your directory, create a new file caddy.toml and add the following code:

[apps.http.servers.test_server]
listen = [
    'localhost:2020',
]

[[apps.http.servers.test_server.routes]]
[[apps.http.servers.test_server.routes.match]]
path = [
    '/backend',
    '/backend/*',
]

[[apps.http.servers.test_server.routes.handle]]
handler = 'reverse_proxy'
[[apps.http.servers.test_server.routes.handle.upstreams]]
    dial = 'localhost:8080'

[[apps.http.servers.test_server.routes]]
[[apps.http.servers.test_server.routes.handle]]
    handler = 'auth_prefix'
    prefix = '.'
[[apps.http.servers.test_server.routes.handle]]
handler = 'file_server'
    root = './files'
index_names = [
    'index.html',
]

The test_server configuration has a routes array and each route in the array has zero or more matchers. Matchers are special modules that allow you to specify matching criteria for incoming requests like the http.ServeMux.Handle method, for instance. For this route, you have a matcher that matches any request to /backend or /backend/. You then tell Caddy that you want to send all matching requests to the reverse-proxy handler which sends all its requests to port 8080.

Remember us using, http.FileServer to serve static files? Caddy’s version is the file_server handler. And unlike the previous route, this one doesn’t have a matcher. This is our default route and its position matters. If you put it above the reverse_proxy route, all requests would match it and no requests would ever make it to the reverse-proxy. Another major difference is that it uses the auth_prefix middleware because we wouldn’t want unauthorized users to access restricted content on our service. Lastly, you return the index.html file.

Let’s check our work. In your terminal, run the following command:

$ ./caddy start --config caddy.toml --adapter toml

You should get the following response that lets you know Caddy is running in the background:

caddy_start

Start your backend service:

$ cd backend

$ go run backend.go

Output:

caddy_backend

Now open your browser and visit http://localhost:2020. Caddy will send your request to the file server and respond with the index.html. If everything succeeds, you should be looking at this:

welcome_caddy

Great! Now let’s test our reverse proxy by visiting http://localhost:2020/backend. This request matches the reverse-proxy route’s matcher so the reverse-proxy handler should handle it by sending the request to the backend service. It should look like this:

backend_caddy

Success! Now let’s see how can integrate Caddy’s key feature: automatic HTTPS.

Adding Automatic HTTPS

Caddy can help you set up a website with full HTTPS support using certificates trusted by all modern web browsers in minutes. It automatically enables TLS and all you need to do is tell it which domain name to serve. Since we’re in a testing environment, Caddy won’t use Let’s Encrypt but it has an internal certificate authority for enabling HTTPS over localhost. Here’s how you add it to your matcher:

[[apps.http.servers.test_server.routes.match]]
host = [
'localhost',
]

If you restart all the services and load up localhost:2020 again, you’ll be met with a page that has HTTPS enabled as compared to earlier:

secure_caddy

That’s it!

Depending on your needs, Caddy can be deployed at the edge of your application so that you can spend most of your time working on your backend web service.

Thank you for reading, until next time.