DockRoute

DockRoute

Official

Docker app from DockRoute's Repository

Overview

External-DNS for plain Docker hosts: DockRoute watches your running containers, reads dockroute.* labels and reconciles the matching DNS records - and Cloudflare Tunnel routes - in a pluggable provider. Your Docker labels are the source of truth; DockRoute makes the provider match them, and never alters records it cannot prove it manages (ExternalDNS-style TXT ownership). Opt a container in with labels: dockroute.enabled=true and dockroute.hostname=app.example.com. Start with the default 'log' provider for a zero-credential dry run, then switch to 'cloudflare'. Docs: https://www.dockroute.dev

dockroute

CI CodeQL codecov Release License: MIT

External-DNS for plain Docker hosts: dockroute watches your running containers, reads dockroute.* labels and reconciles the matching DNS records — and Cloudflare Tunnel routes — in a pluggable provider.

Your Docker Compose file is the source of truth; dockroute makes the provider match it, and never alters what it cannot prove it manages (ExternalDNS-style TXT ownership).

Status: MVP + Cloudflare. DNS records, TXT ownership, sync policies and Cloudflare Tunnel routes work end to end. See ARCHITECTURE.md.

Quick start

Label a container:

services:
  whoami:
    image: traefik/whoami
    labels:
      dockroute.enabled: "true"
      dockroute.hostname: "whoami.example.com"
      # publish through an existing Cloudflare Tunnel:
      dockroute.tunnel.service: "http://whoami:80"

Run dockroute next to it:

services:
  dockroute:
    image: ghcr.io/dockroute/dockroute:latest
    environment:
      DOCKROUTE_PROVIDER: cloudflare
      DOCKROUTE_OWNER_ID: home-lab
      CLOUDFLARE_API_TOKEN: ${CLOUDFLARE_API_TOKEN}
      # only needed for tunnel publishing:
      CLOUDFLARE_ACCOUNT_ID: ${CLOUDFLARE_ACCOUNT_ID}
      CLOUDFLARE_TUNNEL_ID: ${CLOUDFLARE_TUNNEL_ID}
    volumes:
      - /var/run/docker.sock:/var/run/docker.sock:ro
    # No user/group setup needed: the entrypoint detects the socket's group,
    # grants it to the app user and drops privileges before starting.

Or start with the zero-credential dry run:

bun install
DOCKROUTE_DEFAULT_TARGET=192.168.1.10 bun start   # provider=log by default

Labels

Label Required Default Description
dockroute.enabled yes true opts the container in
dockroute.hostname yes FQDN(s), comma-separated
dockroute.type no A A, AAAA or CNAME
dockroute.target no DOCKROUTE_DEFAULT_TARGET Record value; must match dockroute.type (IPv4 / IPv6 / hostname)
dockroute.ttl no 300 TTL in seconds
dockroute.tunnel.service no Origin URL; publish via Cloudflare Tunnel instead of a plain record
dockroute.cloudflare.proxied no false Proxy plain records through Cloudflare

Configuration

Variable Default Description
DOCKER_SOCK /var/run/docker.sock Docker Engine socket path
DOCKROUTE_PROVIDER log log (dry-run) or cloudflare
DOCKROUTE_DEFAULT_TARGET Fallback target when label is omitted
DOCKROUTE_RESYNC_SECONDS 60 Interval of the periodic full reconcile
DOCKROUTE_OWNER_ID default Ownership id — lets several instances share a zone safely
DOCKROUTE_POLICY sync sync, upsert-only or create-only
DOCKROUTE_DELETE_GRACE_SECONDS 60 How long a record must stay gone before it is deleted (0 = delete at once)
DOCKROUTE_TXT_PREFIX _dockroute- Ownership TXT name prefix
DOCKROUTE_DOMAIN_FILTER Comma-separated zone allowlist
CLOUDFLARE_API_TOKEN Token with Zone→DNS→Edit (+ Account→Cloudflare Tunnel→Edit for tunnels)
CLOUDFLARE_ACCOUNT_ID For tunnel publishing
CLOUDFLARE_TUNNEL_ID For tunnel publishing (existing tunnel, you run cloudflared)
DOCKROUTE_HEARTBEAT_PATH /tmp/dockroute-heartbeat Where the reconcile loop writes its liveness heartbeat (read by the image's HEALTHCHECK)

Health check

The image ships a Docker HEALTHCHECK (bun run src/healthcheck.ts): every successful reconcile touches a heartbeat file, and the check fails if it goes stale for longer than 3 × DOCKROUTE_RESYNC_SECONDS (minimum 90s). That means docker ps / docker inspect — and orchestrators that watch container health — report unhealthy when the Docker socket goes unreachable or the provider keeps failing to sync, not just when the process itself crashes.

No extra configuration is needed; running behind docker compose or CasaOS/Arcane/Portainer picks it up automatically from the image. Bring your own healthcheck: block only if you want different thresholds:

services:
  dockroute:
    image: ghcr.io/dockroute/dockroute:latest
    healthcheck:
      interval: 30s
      timeout: 10s
      start_period: 30s
      retries: 3

Safety model

  • Every record dockroute creates gets a companion TXT record (_dockroute-a.whoami.example.com) carrying its owner id.
  • Records without that proof of ownership are never modified, deleted or adopted — conflicts are logged and skipped.
  • Orphan cleanup (container gone → records removed) only happens under the default sync policy and only for records this instance owns.
  • Deletions wait out DOCKROUTE_DELETE_GRACE_SECONDS (default 60) of continuous absence, so a container restart does not take the hostname down. Creates and updates stay immediate: they are cheap and self-correcting, while a deleted record leaves a failure cached far beyond the outage. Raise it if your stacks pull images before starting.
  • Tunnel ingress rules that dockroute did not create are preserved verbatim; dockroute assumes it is the only automated writer for the tunnels it manages.

Troubleshooting

DockRoute skips a misconfigured or conflicting entry instead of stopping the whole reconcile. Match the warning in docker logs dockroute to the table below, fix that entry, and the next reconcile will try it again.

Log message What it means What to do
[labels] <container>: dockroute.enabled but no dockroute.hostname, skipping The container opted in without a usable hostname. Add a non-empty dockroute.hostname label.
[labels] <container>: dockroute.tunnel.service set, ignoring dockroute.type/dockroute.target Tunnel publishing is enabled, so the plain-record type and target labels are unused. Remove dockroute.type and dockroute.target, or remove dockroute.tunnel.service if a plain DNS record was intended.
[labels] <container>: invalid dockroute.tunnel.service "<service>" (expected http://, https://, tcp://, ssh://), skipping The tunnel origin is not a valid URL with a supported scheme. Set dockroute.tunnel.service to a complete http://, https://, tcp:// or ssh:// URL.
[labels] <container>: unsupported record type "<type>", skipping dockroute.type is not supported. Set it to A, AAAA or CNAME.
[labels] <container>: no dockroute.target and no default target, skipping A plain DNS record has no target. Add dockroute.target or set DOCKROUTE_DEFAULT_TARGET.
[labels] <container>: dockroute.target "<target>" is not a valid <requirement>, skipping The target does not match dockroute.type: A needs an IPv4 address, AAAA an IPv6 address, and CNAME a hostname rather than an IP. Correct dockroute.target, or DOCKROUTE_DEFAULT_TARGET when the container inherits it, or set dockroute.type to the type that matches the value.
[labels] <container>: invalid dockroute.ttl "<ttl>", using 300 The TTL is not a positive number, so DockRoute falls back to 300 seconds. Set dockroute.ttl to a positive numeric value, or omit it to use the default.
[reconciler] duplicate desired entry <type>:<hostname>: first container wins, skipping entry from <source> More than one container claims the same hostname and record type. Keep that claim on one container only; the first container in the reconcile wins.
[reconciler] duplicate hostname <hostname>: already published via tunnel, skipping <type> record from <source> The same hostname is requested as both a tunnel route and a plain DNS record. Choose one publication method and remove the duplicate claim; the tunnel route wins the reconcile.
[cloudflare] <hostname>: no matching zone, skipping None of the Cloudflare zones visible after DOCKROUTE_DOMAIN_FILTER matches the hostname. Check the hostname, DOCKROUTE_DOMAIN_FILTER, and that the API token can access the intended zone.
[cloudflare] conflict on <type> <hostname>: <reason> — skipping A pre-existing DNS record or ownership TXT proves the record is unmanaged or belongs to another owner. Resolve the pre-existing record or ownership TXT deliberately: remove it if safe so DockRoute can recreate it, or use the owning DockRoute instance/owner id. Do not weaken the ownership check.
[cloudflare] <N> tunnel route(s) requested but CLOUDFLARE_ACCOUNT_ID/CLOUDFLARE_TUNNEL_ID are not set — skipping tunnel sync Tunnel labels are present but DockRoute cannot identify the Cloudflare account and tunnel. Set both CLOUDFLARE_ACCOUNT_ID and CLOUDFLARE_TUNNEL_ID, or remove the tunnel labels.
[cloudflare] tunnel route <hostname>: an unmanaged ingress rule claims this hostname — skipping The existing tunnel configuration already has an ingress rule for that hostname that DockRoute cannot prove it manages. Remove or rename the pre-existing rule if it is safe to do so, or stop asking DockRoute to publish the same hostname.

Ownership conflicts are a safety feature, not an adoption failure. In particular, a data record with no DockRoute ownership TXT, or a record/TXT owned by a different DOCKROUTE_OWNER_ID, is skipped by design. A dangling ownership TXT from another owner is treated the same way. Resolve the existing record or ownership deliberately; do not bypass the ownership checks.

Docker socket preflight errors already include their remedy in the error text. If the socket is missing, is not a Unix socket, or cannot be read, follow the mount, DOCKER_SOCK, or group-access instruction printed with that error.

Development

bun install
bun test            # unit tests (in-memory fakes, no real HTTP)
bun run typecheck   # strict TypeScript
bun run lint        # Biome — bun run lint:fix to auto-fix

See CONTRIBUTING.md for the ground rules (ownership safety, anti-corruption layer, testing style) and how to add a provider.

License

MIT

Install DockRoute on Unraid in a few clicks.

Find DockRoute in Community Apps on your Unraid server, review the template, and click Install. Unraid handles the Docker app or plugin setup from the published template.

Open the Apps tab on your Unraid server Search Community Apps for DockRoute Review the template variables and paths Click Install

Related apps

Explore more like this

Explore all

Details

Repository
ghcr.io/dockroute/dockroute:latest
Last Updated2026-09-07
First Seen2026-08-15

Runtime arguments

Network
bridge
Shell
sh
Privileged
false

Template configuration

Docker socketPathro

Docker Engine socket (read-only). DockRoute only listens to events and lists containers.

Target
/var/run/docker.sock
Default
/var/run/docker.sock
Value
/var/run/docker.sock
ProviderVariable

DNS provider: 'log' (dry run, prints desired state, no credentials needed) or 'cloudflare'.

Target
DOCKROUTE_PROVIDER
Default
log
Value
log
Owner IDVariable

Ownership id written to companion TXT records; lets several DockRoute instances share a zone safely.

Target
DOCKROUTE_OWNER_ID
Default
default
Value
default
Sync policyVariable

sync = create/update/delete owned records; upsert-only = never delete; create-only = never update or delete.

Target
DOCKROUTE_POLICY
Default
sync
Value
sync
Delete grace (seconds)Variable

Seconds a record must stay gone before DockRoute deletes it. Protects against container restarts; 0 deletes immediately.

Target
DOCKROUTE_DELETE_GRACE_SECONDS
Default
60
Value
60
Default targetVariable

Fallback record value (IP or CNAME target) when a container omits the dockroute.target label. E.g. your server's LAN or WAN IP.

Target
DOCKROUTE_DEFAULT_TARGET
Domain filterVariable

Optional comma-separated allowlist of DNS zones DockRoute may touch. Empty = all zones the token can see.

Target
DOCKROUTE_DOMAIN_FILTER
Resync interval (seconds)Variable

Interval of the periodic full reconcile that backs up the event stream.

Target
DOCKROUTE_RESYNC_SECONDS
Default
60
Value
60
Cloudflare API tokenVariable

Required when provider is cloudflare. Needs Zone > DNS > Edit (+ Account > Cloudflare Tunnel > Edit for tunnel publishing).

Target
CLOUDFLARE_API_TOKEN
Cloudflare account IDVariable

Only needed for Cloudflare Tunnel publishing.

Target
CLOUDFLARE_ACCOUNT_ID
Cloudflare tunnel IDVariable

Only needed for Cloudflare Tunnel publishing. Uses an existing tunnel - you keep running cloudflared yourself.

Target
CLOUDFLARE_TUNNEL_ID