Self-hosted service
jittering/traefik-kop avatar
jittering/traefik-kop

traefik-kop lets one Traefik instance front Docker containers on many hosts

A dynamic docker->redis->traefik discovery agent

523 stars29 forksGoMIT

At a glance

What is it?
traefik-kop is a small Go agent that reads Docker container labels on each node and republishes them into Redis, so a single Traefik instance can serve as the entry point for a multi-host cluster that is not on Swarm or Kubernetes. IP binding and port selection are the two things you have to get right.
Who is it for?
traefik-kop suits an operator running plain Docker on several hosts who wants one public Traefik to route to all of them and does not want to move to Swarm or Kubernetes. It does not suit a cluster where the nodes cannot all reach the same Redis, or a deployment where exposing the Docker socket to a third-party agent is off the table.
Can I use it commercially?
Yes. MIT is a permissive licence: you can use, modify and sell software built on it, as long as you keep its copyright and licence notices.
Is it still maintained?
Yes. The repository last received commits 10 days ago.
What is it written in?
Mainly Go, according to GitHub's language statistics.

Answers come from the project's GitHub data, last synced on October 23, 2026, and from our analysis. They are not legal advice.

Editorial analysis

The problem: one Traefik, many Docker hosts, no orchestrator

traefik-kop is described as a dynamic docker to redis to traefik discovery agent, and it solves the problem of running a non-Swarm/Kubernetes multi-host cluster with a single public-facing traefik instance.

That problem is specific. Traefik's Docker provider discovers containers by talking to one Docker daemon, so a single Traefik can only see the containers on its own host. The usual ways out are to move to Swarm, where the Docker API exposes the whole cluster, to move to Kubernetes with an Ingress controller, or to run a Traefik per host behind your own layer 4 load balancer. The first two are migrations. The third means every host needs a Traefik, and someone has to keep them in sync.

traefik-kop takes a fourth route. It uses the same traefik docker-provider logic, reads the container labels from the local docker node, and publishes them to a given redis instance. You then configure your traefik node with a redis provider pointing at the same instance. Traefik's Redis provider expects the same document shape the Docker provider produces, so the routing rules, weights, TLS settings and middleware references carry over unchanged.

The diagram in the README shows the topology. One Traefik on the public side, Redis beside it, and one traefik-kop agent per additional Docker host, each with the Docker socket mounted and a REDIS_ADDR pointing back at the shared Redis. The result is that the labels on a container running on docker2 are honoured by the Traefik running somewhere else entirely.

The audience is small infrastructure teams and homelab operators who deliberately run plain Docker on multiple machines. It is not a replacement for an orchestrator, it is a way to keep plain Docker and still have one entry point.

The agent is a container with the Docker socket mounted

Running traefik-kop on your other nodes via docker-compose means a small service with the Docker socket mounted and two environment variables:

yaml
services:
  traefik-kop:
    image: "ghcr.io/jittering/traefik-kop:latest"
    restart: unless-stopped
    volumes:
      - /var/run/docker.sock:/var/run/docker.sock
    environment:
      - "REDIS_ADDR=192.168.1.50:6379"
      - "BIND_IP=192.168.1.75"
      # Alternatively, derive from an interface (requires network_mode: host)
      # - "BIND_INTERFACE=eth0"

The Docker socket mount is the mechanism. The agent needs to read container labels, and the Docker API is the only way to get them, so the agent runs with the same access to the daemon that Traefik's own Docker provider has. That is also the security consideration to sit with: mounting /var/run/docker.sock into a container is effectively root-equivalent access to the host, and traefik-kop is a third-party image that receives that access on every node in the cluster.

The BIND_IP value is the other thing to understand immediately, and it is covered separately below. For now, the shape is: one agent per host, one shared Redis, one Traefik.

The image is published on GitHub Container Registry as ghcr.io/jittering/traefik-kop. The Dockerfile is deliberately minimal. It builds a certificates stage from alpine:3.23 with ca-certificates installed, then copies that CA bundle and the built binary into a scratch image, so the final container has no shell and no package manager. The entrypoint is /traefik-kop and the build is multi-architecture aware through the TARGETPLATFORM argument, which is how the same image tag works on arm64 as well as amd64.

Traefik needs a redis provider pointed at the same instance

On the Traefik side the change is a provider block in traefik.yml:

yaml
providers:
  providersThrottleDuration: 2s
  docker:
    watch: true
    endpoint: unix:///var/run/docker.sock
    swarmModeRefreshSeconds: 15s
    exposedByDefault: false
  redis:
    endpoints:
      # assumes a redis link with this service name running on the same
      # docker host as traefik
      - "redis:6379"

Two things are visible here. The Docker provider stays enabled, so Traefik still discovers the containers on its own host the normal way, and the Redis provider is added alongside it for the containers on other hosts. The two sets of configuration are then merged.

The exposedByDefault: false setting matters for security. It means a container without an explicit traefik.enable label is not published, which is the right default when you are about to add a discovery channel that can bring in configuration from other machines.

The providersThrottleDuration of 2s and the docker provider's watch and swarmModeRefreshSeconds settings are ordinary Traefik tuning. The throttle in particular is what stops a burst of Redis updates from turning into a burst of provider reloads.

The comment inside the redis endpoints list is a practical hint rather than a hard requirement: the example assumes a redis link with that service name running on the same docker host as traefik. In a multi-host setup the endpoint will usually be a fixed address on the network, or a service name resolved on the Traefik host, rather than a link on the Traefik host itself.

The target service keeps ordinary Traefik labels

The service that should be published through the remote Traefik uses the labels you would write anyway, with one addition of substance.

yml
services:
  nginx:
    image: "nginx:alpine"
    restart: unless-stopped
    ports:
      # The host port binding will automatically be picked up for use as the
      # service endpoint. See 'service port binding' in the configuration
      # section for more.
      - 8088:80
    labels:
      - "traefik.enable=true"
      - "traefik.http.routers.nginx.rule=Host(`nginx-on-docker2.example.com`)"
      - "traefik.http.routers.nginx.tls=true"
      - "traefik.http.routers.nginx.tls.certresolver=default"
      # [opptional] explicitly set the port binding for this service.
      # See 'service port binding' in the configuration section for more.
      - "traefik.http.services.nginx.loadbalancer.server.scheme=http"
      - "traefik.http.services.nginx.loadbalancer.server.port=8088"

The router rule points at a hostname mentioning docker2, which is the only hint in the labels that this container lives on another host. The service definition sets the scheme to http and the port to 8088, which is the port the container is published on rather than the port it listens on inside the container, 80.

That distinction is the second real decision in this project. Traefik normally talks to a container over the Docker network on the container's internal port. Here the container is on a different host, so Traefik has to reach it at the host's published address and published port instead. The README documents two ways to handle it, and the comments in the example show both: the host port binding is picked up automatically for use as the service endpoint, or you set the port binding explicitly with the loadbalancer.server.port label, as this example does.

The scheme label matters for the same reason. If the remote container is only published on a host port and is not reachable on the Docker network, the scheme has to be http rather than https, because there is no certificate terminating at that container.

IP binding has six levels of precedence, and the default is the wrong one to trust

The IP Binding section is the part of the documentation you should read before deploying anything, because it is where the tool makes a decision you might not have made.

There are a number of ways to set the IP published to traefik, listed in order of precedence, highest first:

1. `kop.<service name>.bind.ip` label 2. `kop.bind.ip` label 3. Container networking IP 4. `--bind-ip` CLI flag (or `BIND_IP` env var) 5. `--bind-interface` CLI flag (or `BIND_INTERFACE` env var), requires network_mode: host 6. Auto-detected host IP

Read that again. The per-service label wins, then a node-wide label, then the container's own networking IP, and only then the flags, then the interface derivation, and only as a last resort the auto-detected host IP.

The awkward part is the ordering of the flags below the container IP. Setting BIND_IP does not override what the container networking IP says, it only overrides the auto-detection. So if you set BIND_IP on a node and the container has a Docker-network address, the container address may still be what gets published. The per-service and node-wide labels are the settings that actually take precedence, and the README also documents a --skip-replace flag that disables custom IP replacement entirely if you would rather Traefik use the address as the container reports it.

The bind-interface option derives the address from a network interface, and the compose example in the usage section notes it requires network_mode: host.

So the practical advice is to set the address explicitly, per service or per node with a label, and to check what actually got published in the Traefik configuration after the first deploy. A container networking IP is a private address on the Docker bridge, and Traefik on another host cannot reach it.

Redis connection options cover Sentinel, TLS and mutual TLS

The Redis configuration is the most complete part of the CLI surface, and the flags map one to one onto environment variables.

The basics are --redis-addr, defaulting to 127.0.0.1:6379, with --redis-user defaulting to default, --redis-pass for a password, --redis-db defaulting to 0, and --redis-ttl in seconds with a default of 0. The TTL is the mechanism for the ephemeral case: with a non-zero TTL, published entries expire, so a stopped container's configuration disappears from Traefik rather than lingering.

Then there is TLS, and the flag set is thorough. --redis-tls turns on TLS, with the default being false. --redis-tls-ca points at a PEM CA bundle used to verify the Redis server, defaulting to the system roots. --redis-tls-cert and --redis-tls-key provide a PEM client certificate and private key for mutual TLS. --redis-tls-server-name is the server name used to verify the Redis certificate, and --redis-tls-insecure-skip-verify skips server certificate verification entirely, which the help text marks as insecure.

Sentinel is supported too, through --redis-sentinel-addrs, a comma-separated list of addresses such as host1:26379,host2:26379, and --redis-sentinel-master for the master name.

The compose file in the repository shows the environment-variable form of all of this, including the commented lines for a TLS-only Redis on a different port and the certificate paths. The comment about mutual TLS notes that the cert and key must be set together, which is the kind of detail that otherwise produces a confusing connection failure.

Two other flags are worth knowing. --hostname identifies this node in Redis, defaulting to server.local, which matters when several agents publish into the same Redis and you need to tell their entries apart. And --poll-interval controls how often the container list is refreshed, defaulting to 60 seconds, so a change to a container on a remote host is not visible to Traefik instantly.

The agent is a Traefik provider reimplementation in Go

The dependency list in go.mod explains the design more clearly than any description could.

The module depends directly on github.com/traefik/traefik/v3 at v3.7.14, plus github.com/traefik/paerser, which is Traefik's own configuration parsing library. The claim that traefik-kop uses the same traefik docker-provider logic is therefore literal: it imports the provider code rather than reimplementing the label semantics, which is why a label set written for a local Docker provider works unchanged when it arrives through Redis.

The go.mod file also shows a set of replace directives, and they explain themselves in a comment: Traefik uses a local replacement for this nested module, so it is pinned to v3.7.14's commit, pointing github.com/traefik/traefik/dynamic/ext at github.com/traefik/traefik/pkg/config/dynamic/ext. There are similar replacements for containous/go-http-auth, containous/mux and containous/minheap. These are the kind of pins that exist because the Traefik module graph has nested modules with unpublished versions, and they are the main upgrade friction in this repository: a Traefik bump means checking that the nested module pin still resolves.

The rest of the dependency set is a normal Go service stack: go-redis/v9 for the Redis client, urfave/cli/v2 for the command line, zerolog and logrus for logging, gofiber and fasthttp for the HTTP layer, go-docker/docker and docker/cli for the Docker API, yaml.v3 and BurntSushi/toml for configuration, and echovault/sugardb for the key-value abstraction.

The source layout matches. The repository root holds config.go, configuration_watcher.go, docker.go, docker_proxy.go, kv.go, store.go, multi_provider.go, polling_provider.go, redis_tls.go, traefik_kop.go and util.go, with test files beside them and a fixtures/ directory, a testing/ directory and a bin/ directory for the command entry point. The naming of docker_proxy.go and multi_provider.go suggests the agent also serves as a proxy for the Docker API rather than only reading from it.

The module requires Go 1.26.0.

Where traefik-kop is the wrong tool

The first wrong-tool case is a cluster where the nodes cannot all reach one Redis. The design assumes a shared, reachable Redis instance, and the agent is configured with a single REDIS_ADDR. A set of hosts on separate networks, or an environment where you cannot run a shared stateful service, is exactly the situation this tool does not address. The multi-provider and store code in the tree hints at more than one backend, but the configuration surface is Redis.

The second case is anything where the Docker socket cannot be mounted into a third-party container. Mounting the socket is root-equivalent on the host, and some environments, managed container platforms and hardened hosts, forbid it. Traefik's own Docker provider has the same requirement on the Traefik host, but here you are extending that requirement to every additional host in the cluster, which is a wider blast radius.

The third case is a team that wants managed routing, health checks, or dynamic configuration without writing configuration files. traefik-kop is a discovery bridge. It does not add capabilities to Traefik; it makes a single Traefik aware of containers elsewhere. If the problem is that your routing rules are static, this is not the answer.

For the alternative approach, the difference in approach matters. Moving to Docker Swarm keeps a single Docker API view of the cluster, which is the same outcome without a new agent, and Traefik's Docker provider already knows how to read it, including swarmModeRefreshSeconds as the example config shows. The trade is that Swarm brings its own overlay networking and service model, which is a larger change than adding a sidecar agent.

The other alternative is to run a Traefik on every host and let an existing layer 4 balancer in front of them terminate TLS and route by host, or to use a hosted overlay VPN so that all hosts share an address space and the container networking IP is routable. Both keep the standard Docker provider, and both trade a sidecar per host for either a networking change or an extra network hop. traefik-kop is the option that adds the least to the cluster when the cluster is already plain Docker and you already run Redis.

MIT licensed, actively released, and released through goreleaser

The licence is MIT, which is the permissive choice and the one you want for a component that sits in the request path of your ingress.

The release cadence is healthy. The recent releases are v0.21.0 on 2026-10-13, v0.20.1 on 2026-05-11 and v0.20 on 2026-05-09, and the last push to the repository was on 2026-10-13. The version is still in the 0.x range, which by the usual convention means the API is not promised to be stable, though for a container that is configured through environment variables that matters little.

The build and release machinery is in the repository. The Makefile has a build target that runs go build on the bin/traefik-kop package, a test target running go test ./..., a cover target producing a coverprofile, and watch targets that use watchexec to re-run tests on changes to Go or YAML files. The default goal is run, which starts the agent with go run. There is also a build-linux target that cross-compiles with GOOS=linux, and a build-docker target that builds the container image tagged to the project name.

Release is handled by goreleaser, driven by a .goreleaser.yml, and the release target runs goreleaser release with --rm-dist and --skip-validate. The changelog is generated from GitHub releases with the update-changelog target, which pulls the most recent release body, strips the changelog heading, rewrites heading levels and prepends it to the existing CHANGELOG.md.

The one upgrade risk to plan for is the Traefik dependency. Because the module replaces Traefik's nested dynamic/ext module with a specific commit, and because the go.mod comment says the pin is to v3.7.14, a Traefik major upgrade can require an update to traefik-kop before you can move. Check the CHANGELOG.md before scheduling a Traefik upgrade across the cluster.

Editorial conclusion

traefik-kop suits an operator running plain Docker on several hosts who wants one public Traefik to route to all of them and does not want to move to Swarm or Kubernetes. It does not suit a cluster where the nodes cannot all reach the same Redis, or a deployment where exposing the Docker socket to a third-party agent is off the table. Before adopting, set the bind address explicitly on every node with BIND_IP or a per-service kop.<service name>.bind.ip label, because the auto-detected host IP is the setting most likely to publish the wrong address, and confirm the Redis instance is reachable from every node before you label anything.

Frequently asked questions

What problem does traefik-kop solve?

It solves the problem of running a non-Swarm/Kubernetes multi-host cluster with a single public-facing Traefik instance. The agent reads container labels from the local Docker node and publishes them to Redis, and Traefik picks them up through its redis provider.

How do I set the IP that traefik-kop publishes for a service?

The order of precedence is the kop.<service name>.bind.ip label, then the kop.bind.ip label, then the container networking IP, then the --bind-ip flag, then --bind-interface, then the auto-detected host IP. Use the per-service or node-wide label if you need to override the container address.

Does traefik-kop support TLS and Sentinel for Redis?

Yes. There are flags for --redis-tls, --redis-tls-ca, --redis-tls-cert, --redis-tls-key, --redis-tls-server-name and --redis-tls-insecure-skip-verify, plus --redis-sentinel-addrs and --redis-sentinel-master, and each maps to a matching environment variable.

How often does traefik-kop refresh the container list?

The --poll-interval flag controls it, with a default of 60 seconds, and it maps to the KOP_POLL_INTERVAL environment variable. Changes on a remote host are therefore not visible to Traefik instantly.

Official sources

  1. Issues
  2. jittering/traefik-kop on GitHub
  3. License: MIT
  4. README
  5. Releases
Add this badge to your README

If you maintain this project, the badge below links readers to this analysis and shows its maintenance status from the daily GitHub snapshot. Paste the markdown into your README; add ?metric=license or ?metric=stars to the image URL for a different field.

Add this badge to your README

markdown
[![Hysen Labs](https://hysenlabs.com/badge/jittering-traefik-kop.svg)](https://hysenlabs.com/projects/jittering-traefik-kop)

Was this useful?