Setting up Caddy to front for iocaine
Last updated: 2025-09-05

# Getting started

In here, I assume that iocaine has already been configured and deployed, using a custom request handler. Lets assume that we have a site running at `[::1]:8080`, and we want to serve that with `Caddy`. Normally, that would look something like this:

    blog.example.com {
      reverse_proxy [::1]:8080
    }

# Delegating decisions to iocaine

The modern way of running iocaine is to let it do the decision: pipe everything through to it, and let it decide what to do with the request. It will either serve it garbage or a challenge (and respond to the reverse proxy with a HTTP 200), or it will signal the reverse proxy to serve the real deal (by sending it a HTTP 421 response).

As such, we’ll have to wrap the above to match this expectation:

    blog.example.com {
      reverse_proxy 127.0.0.1:42069 {
        @fallback status 421
        handle_response @fallback {
          reverse_proxy [::1]:8080
        }
      }
    }

# Delegating some decisions to iocaine

While the above method works for static sites, proxying request bodies is expensive, and most of the time, pointless. As such, to make the setup both more efficient, and more reliable, we should only send `GET` or `HEAD` requests to `iocaine`.

    blog.example.com {
      @read method GET HEAD
      @not-read not {
        method GET HEAD
      }
      reverse_proxy @read 127.0.0.1:42069 {
        @fallback status 421
        handle_response @fallback {
          reverse_proxy [::1]:8080
        }
      }
      handle @not-read {
        reverse_proxy [::1]:8080
      }
    }

Of course, if you’re putting this in front of a static site where only `GET` and `HEAD` make sense in the first place, then there’s no need for the second `reverse_proxy`: we can tell Caddy to return HTTP 405 “Method Not Allowed”:

    blog.example.com {
      @read method GET HEAD
      @not-read not {
        method GET HEAD
      }
      reverse_proxy @read 127.0.0.1:42069 {
        @fallback status 421
        handle_response @fallback {
          reverse_proxy [::1]:8080
        }
      }
      handle @not-read {
        respond 405
      }
    }

# A reusable snippet

> Important

> The Caddy configuration shown here requires Caddy 2.9.x or newer.

Chances are, there’s more than one site we wish to protect with iocaine. Repeating the above boilerplate over and over again would be both mind numbingly boring, entirely pointless, and also completely unnecessary, because Caddy lets us set up reusable snippets. Like this:

    (iocaine) {
      @read method GET HEAD
      @not-read not {
        method GET HEAD
      }
      reverse_proxy @read 127.0.0.1:42069 {
        @fallback status 421
        handle_response @fallback {
          {blocks.handler}
        }
      }
      handle @not-read {
        {blocks.default}
      }
    }

Rewriting the `blog.example.com` site’s declaration to use this makes it look like so:

    blog.example.com {
      import iocaine {
        handler {
          reverse_proxy [::1]:8000
        }
        default {
          respond 405
        }
      }
    }

Still more verbose than a single `reverse_proxy` line, but only marginally, and this setup is flexible enough to allow all kinds of configurations. You can put any directive you wish inside those `handler` and `default` blocks.