---
title: "Outbound proxies"
description: "Configure reusable proxies and routing rules that control how Kiln connects to stream sources, guide sources, and logo files."
---

> Documentation Index
> Fetch the complete documentation index at: https://kiln.wbxdocs.com/llms.txt
> Use this file to discover all available pages before exploring further.

# Outbound proxies

Kiln uses two layers to manage outbound traffic: `[[proxies]]` defines reusable proxies, and `[egress]` decides which requests use each route. Stream sources, guide sources, and logo requests share one routing table, so you do not need to configure them separately.

## The model

Every outbound URL is resolved on its own:

1. **Match rules by priority**

   Rules are evaluated in ascending `priority` order. The first match wins and evaluation stops there.
2. **Fall back to the default**

   When nothing matches, `egress.default` applies.
3. **Resolve the route**

   `direct` connects directly. A proxy ID uses that route. If the referenced route is missing or disabled, the request falls back to a direct connection.

```toml title="kiln.toml"
[[proxies]]
id = "proxy-http"
name = "HTTP route"
url = "http://127.0.0.1:7890"

[[proxies]]
id = "proxy-socks"
name = "SOCKS5 route"
url = "socks5h://127.0.0.1:7891"

[egress]
default = "direct"
playlist_policy = "rewrite"
# docker_proxy_host = "host.docker.internal"

[[egress.rules]]
id = "example-cdn"
priority = 10
kind = "host_suffix"
pattern = "example-cdn.com"
proxy = "proxy-http"
```

> **The console is the source of truth**
>
> Like channels, outbound settings live in SQLite. `[[proxies]]` and `[[egress.rules]]` from the config file seed the database once, and only while no proxy rows exist; `default`, `playlist_policy` and `docker_proxy_host` are written only if the corresponding setting is absent. After the first start, edit them in the [admin console](/en/guide/admin/).

## Defining a route

- `id` must be unique and is what `egress.default`, rules, and EPG sources reference. `name` is for display only.
- `url` accepts the `http`, `https`, `socks5` and `socks5h` schemes, for example `http://127.0.0.1:7890` or `socks5h://127.0.0.1:7891`.
- Put credentials in the URL when the proxy needs them, for example `http://user:password@127.0.0.1:7890`. The console shows that a route has credentials without echoing the password back.
- `socks5h` resolves hostnames at the proxy, `socks5` resolves them locally first. Prefer `socks5h` when the origin only resolves correctly from the proxy's network.
- Routes can be disabled instead of deleted. A route referenced by `egress.default` or by an enabled rule cannot be left disabled.

> **Both engines support all four schemes**
>
> The ffmpeg engine never touches your routes directly; it reads through a short-lived HTTP proxy Kiln opens locally, and Kiln still performs the egress according to the rules. Whether a route is HTTP or SOCKS is therefore equally transparent to it. See [Media engine](/en/guide/media-engine/) for which engine a channel uses.

## Default egress and rules

`egress.default` is either `direct` or the ID of a route, and covers everything no rule matched.

A rule consists of `id`, `priority`, `kind`, `pattern`, and `proxy`, and can be disabled individually. Lower `priority` numbers win. Set `proxy` to `direct` to create a direct exception in a deployment that otherwise proxies everything.

| `kind` | Matches against |
| --- | --- |
| `host_suffix` | Hostname suffix. `example.com` matches `example.com` and `cdn.example.com`, but not `notexample.com` |
| `host_exact` | The full hostname, exact comparison |
| `host_regex` | A regular expression over the hostname |
| `channel_id` | Exact channel ID |
| `url_regex` | A regular expression over the full URL |

An omitted `kind` is treated as `host_suffix`. Regex rules are compiled before they are saved, and a rule that fails to compile never takes effect.

## Playlist handling

`playlist_policy` controls how absolute origin URLs are presented to the player when Kiln relays an upstream HLS playlist.

| Value | Behavior |
| --- | --- |
| `rewrite` (default) | Everything is rewritten back to Kiln; the player talks only to Kiln |
| `passthrough` | Origin URLs are left untouched; the player connects to the origin |
| `auto` | Only URLs that need a proxy are rewritten, the rest stay as origin URLs |

Under `rewrite`, Kiln resolves every media line and every `URI="..."` attribute to an absolute URL, then replaces it with `/v1/play/<channel id>/u/<encoded url>?sig=...` on its own origin. Behind an access token the prefix becomes `/p/<token>/play/<channel id>/u/` instead. The URL is encoded with URL-safe Base64 and keeps a recognizable extension, and the signature is an HMAC over the channel ID and target URL, keyed by a random value generated at process start, so rewritten URLs stop working after a restart. The player therefore only ever contacts Kiln, and Kiln performs the upstream fetch through whichever route the rules selected. Nested variant playlists are rewritten again as they are pulled.

`passthrough` sends the player straight to the origin. Those requests bypass Kiln's proxy routes and never carry the channel's custom headers, so it only makes sense when the player itself can reach the origin. `auto` splits the difference: URLs that need a proxy come back through Kiln, everything else keeps its origin address and saves a hop.

> **Viewer limits take precedence**
>
> A channel with `max_viewers` set is always rewritten regardless of the policy, since Kiln cannot count concurrent viewers otherwise.

## Reaching a proxy from inside a container

Kiln never rewrites the address on a route. When it runs in a container, `url` has to be reachable **from inside the Kiln container**: the `127.0.0.1` that works on the host points at the container itself, so copying it over will not connect.

### The proxy runs on the host

Use `host.docker.internal` to reach back to the host. Docker Desktop provides that name; on Linux you have to map it to the gateway yourself:

```yaml title="compose.yaml" ins={6,7}
services:
  kiln:
image: ghcr.io/babywbx/kiln:core
ports:
  - "8080:8080"
extra_hosts:
  - "host.docker.internal:host-gateway"
volumes:
  - ./kiln.toml:/etc/kiln/kiln.toml:ro
  - kiln-data:/var/lib/kiln/data
restart: unless-stopped

volumes:
  kiln-data:
```

Point the route at that hostname, with the port the proxy listens on over on the host:

```toml title="kiln.toml"
[[proxies]]
id = "proxy-host"
name = "Host proxy"
url = "http://host.docker.internal:7890"
```

> **The host proxy must accept LAN connections**
>
> The usual failure here is not in Kiln: a proxy bound to `127.0.0.1` never sees connections coming from a container. Bind it to `0.0.0.0` or to the bridge address instead, which most proxy programs expose as an "allow LAN" style switch.

### The proxy is another container

With both containers on the same Docker network, address the proxy by service name; it does not need to publish a port to the host at all:

```yaml title="compose.yaml"
services:
  proxy:
image: your-proxy-image
container_name: proxy
volumes:
  - ./proxy-config:/etc/proxy:ro
restart: unless-stopped
networks: [kiln-net]

  kiln:
image: ghcr.io/babywbx/kiln:core
depends_on: [proxy]
ports:
  - "8080:8080"
volumes:
  - ./kiln.toml:/etc/kiln/kiln.toml:ro
  - kiln-data:/var/lib/kiln/data
restart: unless-stopped
networks: [kiln-net]

networks:
  kiln-net:

volumes:
  kiln-data:
```

Use the service name plus the port the proxy listens on **inside its own container**, which is not the port it may publish to the host:

```toml title="kiln.toml"
[[proxies]]
id = "proxy-sidecar"
name = "Proxy container"
url = "http://proxy:7890"

[[proxies]]
id = "proxy-sidecar-socks"
name = "Proxy container SOCKS"
url = "socks5h://proxy:7891"
```

Traffic within one network never leaves the host, so the proxy container needs neither a published port nor `extra_hosts`.

### The proxy is on another machine on the LAN

This one needs nothing special on the Docker side. The default bridge network already reaches the LAN the host sits on, so just point the route at that machine:

```toml title="kiln.toml"
[[proxies]]
id = "proxy-lan"
name = "LAN proxy"
url = "http://192.168.1.100:7890"
```

No `extra_hosts`, no shared network; it is written exactly as it would be with Kiln running directly on the host. The requirement is the same as before: the proxy over there has to listen on `0.0.0.0` or on the relevant interface, with the port open through that machine's firewall.

### `docker_proxy_host` plays no part in this

`docker_proxy_host` only applies when `ffmpeg.mode = "docker"`, where it tells the ffmpeg container Kiln spawns how to reach the Kiln process itself. It has nothing to do with route addresses, and neither topology above needs it changed.

### Verifying

The **Outbound proxies** page refuses to apply a route until one connectivity test passes, and that test runs from inside the Kiln container, so a pass means the address is reachable from where it matters. When it fails, work through it in this order: whether the proxy accepts non-local connections, whether the container resolves that hostname or service name, and whether the port is the one seen from inside the container.

## What outbound routing covers

| Traffic | Notes |
| --- | --- |
| Channel origin fetches | Playlists and segments, on both the native and the ffmpeg path |
| EPG fetches | Routed by the rules only when the source's egress is `auto`; `direct` or a named route is honored as configured, see [EPG](/en/guide/epg/) |
| Channel logos | The upstream fetch behind `/v1/logo/{id}`, matched with the channel ID in scope |
| Admin probes | Channel probe, preview, and the outbound connectivity test |

`channel_id` rules can only match when the request carries a channel context. EPG fetches have none, so they are never selected by that rule kind.

## Console and connectivity test

The **Outbound proxies** page edits a draft. Adding or changing routes and rules only touches the draft, and **Apply** stays disabled until a connectivity test passes; any further edit resets that state and requires a new test. It is a cheap way to avoid pushing a typo in a proxy address to a running service.

The test maps to `POST /v1/admin/egress/test` and needs the `refresh` scope. Optional parameters:

- `target`: omit it to use the built-in public probe target. `source` or `custom` requires a `url`, which must resolve to a public address.
- `channel_id`: resolve the route as that channel would, which is how you verify `channel_id` rules.
- `proxy_id` or `proxy_url`: test one saved route, or an address that has not been saved yet.
- `draft`: resolve against the draft configuration without touching the running one.

The response reports the selected `proxy_id`, the matching `reason`, whether playlists would be rewritten, the elapsed `dur_ms`, and reachability with HTTP status and final URL. A proxy demanding authentication is called out explicitly.

## Common topologies

- **Direct by default, proxy a few hosts** — Keep `default = "direct"` and add one `host_suffix` rule per origin that needs the proxy. The right shape when most origins are reachable and only a few are not.
- **Proxy by default, a few direct** — Set `default = "proxy-http"`, then add rules with `proxy = "direct"` for LAN or local origins. Internal upstreams stay off the proxy, which never sees your internal addresses.
- **Kiln in a container, proxy on the host** — Point the route at `http://host.docker.internal:7890` and give the Kiln container a `host.docker.internal:host-gateway` mapping. See [Reaching a proxy from inside a container](#reaching-a-proxy-from-inside-a-container).
- **Kiln and the proxy in separate containers** — Put both on one Docker network and point the route at `http://<service>:<internal port>`; the proxy publishes nothing. See [Reaching a proxy from inside a container](#reaching-a-proxy-from-inside-a-container).

For diagnosing a route that behaves unexpectedly, see [Troubleshooting](/en/guide/troubleshooting/).

Source: https://kiln.wbxdocs.com/en/guide/proxy/index.mdx
