Configuring iocaine
Last updated: 2025-12-23

# Overview

`iocaine` can be configured in a number of dialects: TOML, JSON, YAML, and KDL. The other formats are primarily kept as a way for programmatic configuration, for the simple reason that those formats are easier to generate than KDL. Nevertheless, serialized formats are also documented - just not here, we’re dealing only with KDL here, because that’s the most human-friendly.

Out of the box, `iocaine` has a built-in configuration, one you can view by running `iocaine show config`. This configuration can be augmented or replaced, and `iocaine` can assemble its configuration from multiple files and directories. See the Configuration merging section for details!

The configuration is not hot-reloaded! If it is changed, `iocaine` has to be restarted for the new configuration to take effect.

# Syntax

The syntax described in this documentation is based on KDL, and we aim to document the syntax in enough detail, through examples, that you won’t need to read the KDL spec. Lets dive in!

The main purpose of the configuration is to define “servers” (any number of them), each of which will act (mostly) independently. Apart from servers, the configuration lets us declare “handlers” (again: any number of them) - handlers are used by some of the server types to serve requests. It also lets us configure an initial seed.

There are multiple server types: HTTP servers, HAProxy SPOA servers, and Prometheus servers. The first two require a handler to serve request, and can use a Prometheus server for their metrics. All of these have a name, but the names live in separate namespaces: we can have a `http-server`, a `prometheus-server`, a `haproxy-spoa-server`, and a handler all share the same name, without conflicts. We’ll show an example at the end of this document.

Handlers are not shared between servers, but Prometheus servers can be. We’ll talk about these specifics later.

The format also recognizes comments: start a comment with `//`, and the rest of the line will be ignored. It is also possible to comment out entire nodes by prefixing them with `/-`. We’ll show a few examples below.

Because configuration can be split into multiple files, it is possible and allowed for partial configurations to have incomplete nodes, as long as the final, merged configuration has all required parts. This too will be shown in the examples section.

## `initial-seed` or `initial-seed-file`

The “initial seed” controls the initial seed used by `iocaine`’s random number generator. The random number generator will be seeded by this value, and certain properties of the request: the requested host, the requested path, and all query parameters serialized.

As long as the same seeds are in play, `iocaine` will generate the same random numbers for any given request. As such, if we wish to change the generated garbage from time to time, changing the initial seed is the easiest way to accomplish that. We can either set a static string (which we’d change every so often):

    initial-seed "Trans Rights are Human Rights"

Or we can point it to a file, to externalize the randomization. For example, on a NixOS system, setting it to `/run/current-system/boot.json` will result in the initial seed changing every time a new configuration is deployed.

    // Set the initial seed to the contents of our system's boot configuration.
    initial-seed-file "/run/current-system/boot.json"

These nodes can be repeated, but repeats will override previous declarations, and only the last one will be effective. There is no priority difference between `initial-seed` and `initial-seed-file`: whichever has been declared last will be used.

If not set, defaults to the empty string.

## `instance-id` & `state-directory`

> Important

> Added in iocaine 3.1.0.

`instance-id` is an identifier for this particular instance of `iocaine`. This can be - and is used as such by the built-in script - used to generate URLs or other data that poisons them, and can be easily identified later. If not specified, `iocaine` will generate a random UUID, and store it in a file in `state-directory`, so that it persists across restarts.

The `state-directory` is where iocaine can store persistent state. It defaults to the current working directory.

    state-directory "/var/lib/iocaine"
    instance-id "CVsfEhWMnA4umxJfJ9VQ"

The built-in script will derive the poison ID from this, using the `instance-id` and the name of the server it was instantiated for.

## `http-server`

An HTTP server is one where iocaine will serve HTTP requests, and use a handler to decide their fate. The handler shall return a HTTP 421 response if the request should be served normally. Any other status indicates that iocaine’s response should be served instead of the real content. We can have any number of HTTP servers, listening on different ports, with their own initial seed (if so desired), and using metrics provided by a particular Prometheus server.

For each HTTP server we wish to run, we need to declare them with the `http-server` stanza. This requires a single positional argument: the name of the HTTP server. It also requires a bind address, and a handler. We’ll look at those in detail in a bit, but first, the minimal boilerplate required for a HTTP server declaration:

    http-server "name" {
      bind "127.0.0.1:42069"
      use handler-from="name"
    }

This declares that we want to start a HTTP server bound to port 42069 on `127.0.0.1`, using a handler named “name”.

### Available settings

Each `http-server` can be configured independently, and they support the following settings:

#### `initial-seed` or `initial-seed-file`

These work exactly the same as the global `initial-seed`, but the seed is bound to this server only. If not set, the server will use the global seed.

#### `bind`

The `bind` setting controls where `iocaine` will bind this server to. It can be an IP address and a port (both IPv4 and IPv6 are supported, of course), a UNIX domain socket, or - on Linux - an Abstract Unix Domain socket or a systemd-provided socket file descriptor. The address to bind to must be the first argument to this setting, and the setting is required for each `http-server` declaration.

The setting accepts a `unix-socket-access` named property, but it is only used if the bind address is a unix domain socket. It’s value can be any one of: “owner”, “group”, or “everyone”. This property controls the access restrictions on the unix domain socket. If unset, the permissions on the socket will not be explicitly set, and will inherit it from `umask`.

Lets see a few examples!

    http-server "tcp" {
      bind "127.0.0.1:42069" // binds to TCP port 42069 on 127.0.0.1
    }

    http-server "uds" {
      // Binds to /run/iocaine/uds.sock, and sets permissions to 0660.
      bind "/run/iocaine/uds.sock" unix-socket-access="group"
    }

    http-server "auds" {
      // Binds to the "iocaine.auds.sock" Abstract Unix Socket.
      // This feature is only available under Linux.
      bind "@iocaine.auds.sock"
    }

    http-server "systemd" {
      // "binds" to a socket provided by systemd's socket activation feature
      bind "sd-listen:iocaine-systemd"
    }

#### `use`

The `use` setting allows us to connect our `http-server` with a `prometheus-server`, and it is also used to set the configuration of the handler our server will use. It accepts only named properties, and does not allow positional arguments.

Two named properties are available: `metrics`, the name of a `prometheus-server` to connect to, and send metrics to (this property is optional), and `handler-from`, a handler declaration to instantiate our handler from (this property is required).

Each `http-server` will run its own handler, but the handler declaration can be shared. On the other hand, metrics servers are shared between `http-server` instances. Lets see a few examples!

    http-server "default" {
      use handler-from="default"
    }

This will result in a HTTP server using the `default` handler. Unless otherwise declared, that’s going to be iocaine’s built-in request handler.

    http-server "default" {
      use metrics="default" handler-from="default"
    }

    haproxy-spoa-server "default" {
      use metrics="default" handler-from="default"
    }

    prometheus-server "default"

Now this is a more interesting declaration! It sets up three servers: a `http-server`, a `haproxy-spoa-server`, and a `prometheus-server`. Both the `http-server` and the `haproxy-spoa-server` will use same `prometheus-server`, but they’ll each have their own instance of the request handler, instantiated from the `"default"` declaration.

## `haproxy-spoa-server`

An `haproxy-spoa-server` starts an agent that can be used with HAProxy’s Stream Processing Offload Engine. How to configure HAProxy to use it falls outside of the scope of this documentation, and will be explored elsewhere. We’re only dealing with the `iocaine`-side of it here.

Similar to `http-server`, we can have any number of declared `haproxy-spoa-server`s, and the declaration can be configured exactly the same as `http-server`. See its documentation for details.

## `prometheus-server`

The purpose of a `prometheus-server` is two-fold: it provides an endpoint for Prometheus to scrape, and also provides a metrics registry for `http-server`ss and `haproxy-spoa-server`s to use.

We can have any number of `prometheus-server`s, each with a different name, and a different bind address, both of which are required. As such, the minimal declaration necessary for a `prometheus-server` is this:

    prometheus-server "name" {
      bind "address"
    }

A more complete example would look like the following:

    prometheus-server "main" {
      bind "127.0.0.1:42042"
      persist-path "/var/lib/iocaine/main.metrics.json"
      persist-interval "10min"
    }

This declares a server to listen on port 42042 of `127.0.0.1`, persist its metrics to `/var/lib/iocaine/main.metrics.json` every 10 minutes (and during shutdown).

### Available settings

Apart from the `bind` setting, which has the same syntax as in the `http-server`’s case, two other options are available: `persist-path` and `persist-interval`.

### `persist-path`

If set, `persist-path` should be the path to a file the `prometheus-server` being declared can save its metrics to. Saving the metrics allow handlers to load the saved metrics, so they aren’t lost between restarts. If not set (which is the default), metrics will not be saved.

If a path is set, metrics will be persisted periodically, at `persist-interval` intervals, and when shutting iocaine down (or restarting it).

### `persist-interval`

The `persist-interval` setting controls how often the declared `prometheus-server` will save its metrics to the file given through `persist-path`. If no path is configured, this setting is ignored. Unless configured otherwise, it defaults to one hour.

The interval can be specified in a human-readable format, like “1h” (1 hour, which is also a valid and recognised interval itself), “10m” (10 minutes). See this list of units for the list of recognised units.

Periodic saving cannot be turned off at this time, only set to an unreasonably long interval like a thousand years.

## `declare-handler`

Handlers are at the core of `iocaine`, they’re responsible for deciding what to do with any incoming request. `iocaine` does not function without them. How they work, and what they can do, is outside of the scope of this document, we’re only dealing with their declarations here.

Handlers are used by `http-server`s and `haproxy-spoa-server`s, but each server will use its own instance of a handler. They can share the template the handler is instantiated from, but each will run its own copy.

To declare such a template, we use the `declare-handler` stanza. Like most other things in `iocaine`’s configuration, it requires a single positional argument: its name.

    declare-handler "name" {
      // configuration
    }

### Available properties

Apart from the name, the `declare-handler` stanza - unlike other settings - accepts a number of properties:

#### `path`

If specified, `path` must point to a directory containing the request handler. If not specified, `iocaine` will use the default handler.

#### `language`

The programming language the handler is written in. Can be either of `roto` (the default), `lua`, or `fennel`. The default handler, used when no `path` is set is only available in `roto` and `lua`.

If using `fennel` as the language, `compiler` must be set too.

#### `compiler`

When using a handler written in Fennel, a compiler must be specified. This must be a path to Fennel’s compiler, not to the command-line tool!

### Configuration for the handler

The `declare-handler` stanza allows setting custom options, which will be made available to the handler. This lets us configure the handler from within iocaine’s own configuration files! Unlike other parts of the configuration, handler configuration is limited to key-value pairs, and does not allow named properties. A short example demonstrates this best:

    declare-handler "test" path="/usr/share/iocaine/handlers/test" language="roto" {
      should-do-something #true
      data-file-path "/some/path"
      template {
        min-words 10
        max-words 42
      }

      // The following, however, are not allowed:
      /-props foo=bar
      /-something prop=name {
        another-setting
      }
    }

# Configuration merging

`iocaine` comes with a limited, but sensible default configuration out of the box. This configuration can be augmented, or replaced. `iocaine` can be pointed to configuration files, or directories via the `--config-path` command-line option. This option can be specified multiple times, and can point to either a file, or a directory.

If pointed at a file, then all previous configuration (including the built-in configuration) will be discarded, and the file will serve as the new base. If pointed at a directory, `iocaine` will look for configuration files within (recursing into subdirectories), in any of the supported formats (TOML, JSON, YAML, and KDL), and will load them in alphabetical order.

For example, to replace the built-in configuration, and augment that with snippets, we can do:

    iocaine --config-path /etc/iocaine/config.kdl \
            --config-path /etc/iocaine/defaults/config.d \
            --config-path /etc/iocaine/config.d

Configuration parts are merged in the same order as given on the command-line. Taking the example above, `iocaine` will first load `/etc/iocaine/config.kdl`, then it will merge all files under `/etc/iocaine/defaults/config.d` into it (in alphabetical order within that directory), then it will merge the files under `/etc/iocaine/config.d` (again, in alphabetical order within that directory).

When merging configuration, dictionaries are recursively merged, arrays and other types will use the incoming value. To demonstrate this with an example, lets say we have two files, `/etc/iocaine/config.kdl`, and `/etc/iocaine/config.d/00-demo.kdl`, with the following contents:

    // /etc/iocaine/config.kdl
    http-server default {
      bind "127.0.0.1:42069"
      use handler-from=default
    }

    declare-handler default {
      ai-robots-txt-path "data/robots.json"
      unwanted-visitors Perplexity
      sources {
        training-corpus "data/corpus/1984.txt"
      }
    }

    // /etc/iocaine/config.d/00-demo.kdl
    http-server default {
      bind "0.0.0.0:42069"
    }

    declare-handler default language=lua {
      ai-robots-txt-path "data/ai.robots.txt-robots.json"
      unwanted-visitors Googlebot ClaudeBot
      sources {
        wordlists "data/words.txt"
      }
    }

In this case, `http-server default` will have its properties merged, because it is a dictionary. It’s `bind` setting will get replaced, because it’s a string type. Similarly, `declare-handler default` also merges! The `language=lua` property will replace the implicit default of `roto` (because properties are dictionaries), `ai-robots-txt-path` and `unwanted-visitors` will also be replaced, because one’s a string, and the other is an array. The `sources` setting, on the other hand, is a dictionary, and will get merged.

Thus, we’ll end up with the following merged configuration:

    http-server default {
        bind "127.0.0.1:42069"
        use handler-from=default
    }
    declare-handler default language=lua {
        ai-robots-txt-path "data/ai.robots.txt-robots.json"
        sources {
            training-corpus "data/corpus/1984.txt"
            wordlists "data/words.txt"
        }
        unwanted-visitors Googlebot ClaudeBot
    }

This sounds more confusing than it really is. Hopefully.

# Examples

While the syntax described above is hopefully detailed and comprehensive enough, nothing beats trial and error, and experimenting. To get you up and running, in this section, we’ll provide a number of examples to get started with.

## Default handler with HTTP & Prometheus servers

Lets create a complete configuration, all in one file, with every required parameter set. Save it somewhere, and run `iocaine --config-path /path/to/this/file.kdl`

    http-server default {
        bind "127.0.0.1:42069"
        use handler-from=default metrics=default
    }

    prometheus-server default {
        bind "127.0.0.1:42042"
        persist-path "/var/lib/iocaine/default.metrics.json"
        persist-interval "10m"
    }

    declare-handler default {
        /-ai-robots-txt-path "/opt/iocaine/share/data/ai.robots.txt-robots.json"
    }

This sets up a HTTP server to listen on port 42069 of `127.0.0.1`, use the default handler, and the Prometheus server set up to listen on port 42042 of `127.0.0.1`, with metrics persisted to a file every ten minutes.

While it also has a handler declared, that handler is the default one, and no additional configuration is applied to it, because the `ai-robots-txt-path` setting is commented out. We do need it declared, however, because this configuration overrides the built-in configuration, and a handler must be defined. We just defined it without a `path` property, to make `iocaine` use the built-in handler.

## Splitting the configuration file

It’s also possible to split the configuration into multiple files. This is useful both for organizing a larger configuration, and when you’re packaging iocaine (for example) and wish to ship a default that users of the package can augment.

Like in the previous example, we’ll start our configuration with a file that’ll override the built-in configuration. However, we will omit some of the required parts from it, and fill them out in separate snippets.

Lets write the following to a file, say, to `config.kdl`:

    // config.kdl
    http-server default {
        use handler-from=default metrics=default
    }

    prometheus-server default
    declare-handler default

There are no `bind`s set up for neither the `http-server`, nor the `prometheus-server`! How will that work?

Lets create a directory, `config.d`, and a file within it: `00-binds.kdl`.

    // config.d/00-binds.kdl
    http-server default {
        bind "127.0.0.1:42069"
    }

    prometheus-server default {
        bind "127.0.0.1:42042"
    }

We’re also going to want those metrics persisted, so lets create `config.d/01-persist-metrics.kdl`:

    // config.d/01-persist-metrics.kdl
    prometheus-server default {
        persist-path "/var/lib/iocaine/default.metrics.json"
        persist-interval "10m"
    }

Now, lets see how `iocaine` merges these!

    # iocaine --config-path config.kdl --config-path config.d show config
    initial-seed ""
    http-server default {
        bind "127.0.0.1:42069"
        use handler-from=default metrics=default
    }
    prometheus-server default {
        bind "127.0.0.1:42042"
        persist-path "/var/lib/iocaine/default.metrics.json"
        persist-interval "10m"
    }
    declare-handler default language=roto

Wonderful!