Portainer Templates logo

Portainer Templates

DockRoute DockRoute

Container

DNSNetworkingTools

External-DNS for plain Docker hosts: watches running containers, reads dockroute. labels and reconciles the matching DNS records — and Cloudflare Tunnel routes — in a pluggable provider. TXT-based ownership means it never alters records it cannot prove it manages. Source: https://github.com/Dockroute/Dockroute

Image details

Architecture: amd64, arm64
Image size: 98 MB
User: dockroute

Source details

Stars: 20
Forks: 6
Language: TypeScript
License: MIT
Updated: 1 day ago

Configuration

Type
Container
Platform
linux
Image
ghcr.io/dockroute/dockroute:latest
Volumes
/var/run/docker.sock : /var/run/docker.sock
Env vars
DOCKROUTE_PROVIDER=logDOCKROUTE_OWNER_ID=defaultDOCKROUTE_POLICY=syncDOCKROUTE_DEFAULT_TARGET=DOCKROUTE_DOMAIN_FILTER=DOCKROUTE_RESYNC_SECONDS=60CLOUDFLARE_API_TOKEN=CLOUDFLARE_ACCOUNT_ID=CLOUDFLARE_TUNNEL_ID=
Restart
unless-stopped

Notes

Deploys with zero configuration: the entrypoint grants the app user the Docker socket's group and drops privileges before starting, and the default 'log' provider is a dry run that needs no credentials. Once the output looks right, switch DOCKROUTE_PROVIDER to cloudflare and set CLOUDFLARE_API_TOKEN. Opt containers in with the labels dockroute.enabled=true and dockroute.hostname=app.example.com. Docs: https://www.dockroute.dev

Standalone Install

Select an install method, to see config/commands for deploying DockRoute

Installation method

Install on Portainer

Import all app templates into your Portainer instance, for easy 1-click deploys

  1. Ensure both Docker and Portainer are installed, and up-to-date
  2. Log into your Portainer web UI
  3. Under Settings → App Templates, paste the below URL
  4. Head to Home → App Templates, and the list of apps will show up
  5. Select DockRoute, fill in any config options, and hit Deploy

Template Import URL

https://raw.githubusercontent.com/Lissy93/portainer-templates/main/templates.json
Show Me demo

More install options in our documentation, or see Dockroute/Dockroute for app-specific guidance.

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

LabelRequiredDefaultDescription
dockroute.enabledyestrue opts the container in
dockroute.hostnameyesFQDN(s), comma-separated
dockroute.typenoAA, AAAA or CNAME
dockroute.targetnoDOCKROUTEDEFAULTTARGETRecord value; must match dockroute.type (IPv4 / IPv6 / hostname)
dockroute.ttlno300TTL in seconds
dockroute.tunnel.servicenoOrigin URL; publish via Cloudflare Tunnel instead of a plain record
dockroute.cloudflare.proxiednofalseProxy plain records through Cloudflare

Configuration

VariableDefaultDescription
DOCKERSOCK/var/run/docker.sockDocker Engine socket path
DOCKROUTEPROVIDERloglog (dry-run) or cloudflare
DOCKROUTEDEFAULTTARGETFallback target when label is omitted
DOCKROUTERESYNCSECONDS60Interval of the periodic full reconcile
DOCKROUTEOWNERIDdefaultOwnership id — lets several instances share a zone safely
DOCKROUTEPOLICYsyncsync, upsert-only or create-only
DOCKROUTEDELETEGRACESECONDS60How long a record must stay gone before it is deleted (0 = delete at once)
DOCKROUTETXTPREFIXdockroute-Ownership TXT name prefix
DOCKROUTEDOMAINFILTERComma-separated zone allowlist
CLOUDFLAREAPITOKENToken with Zone→DNS→Edit (+ Account→Cloudflare Tunnel→Edit for tunnels)
CLOUDFLAREACCOUNTIDFor tunnel publishing
CLOUDFLARETUNNELIDFor tunnel publishing (existing tunnel, you run cloudflared)
DOCKROUTEHEARTBEATPATH/tmp/dockroute-heartbeatWhere 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 messageWhat it meansWhat to do
labels <container>: dockroute.enabled but no dockroute.hostname, skippingThe container opted in without a usable hostname.Add a non-empty dockroute.hostname label.
labels <container>: dockroute.tunnel.service set, ignoring dockroute.type/dockroute.targetTunnel 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://), skippingThe 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>", skippingdockroute.type is not supported.Set it to A, AAAA or CNAME.
labels <container>: no dockroute.target and no default target, skippingA plain DNS record has no target.Add dockroute.target or set DOCKROUTEDEFAULTTARGET.
labels <container>: dockroute.target "<target>" is not a valid <requirement>, skippingThe 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 DOCKROUTEDEFAULTTARGET when the container inherits it, or set dockroute.type to the type that matches the value.
labels <container>: invalid dockroute.ttl "<ttl>", using 300The 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, skippingNone of the Cloudflare zones visible after DOCKROUTEDOMAINFILTER matches the hostname.Check the hostname, DOCKROUTEDOMAINFILTER, and that the API token can access the intended zone.
cloudflare conflict on <type> <hostname>: <reason> — skippingA 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 CLOUDFLAREACCOUNTID/CLOUDFLARETUNNELID are not set — skipping tunnel syncTunnel labels are present but DockRoute cannot identify the Cloudflare account and tunnel.Set both CLOUDFLAREACCOUNTID and CLOUDFLARETUNNELID, or remove the tunnel labels.
cloudflare tunnel route <hostname>: an unmanaged ingress rule claims this hostname — skippingThe 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

Check the logs first

Nine times out of ten the logs tell you exactly what went wrong.

  • In Portainer, go to Containers, click the container, then Logs. Or run docker logs dockroute
  • Exit codes help too: 137 means killed, usually out of memory. 126 or 127 means the command inside the image is broken.

Can't reach the Docker socket

dockroute talks to Docker through /var/run/docker.sock. If the logs show "permission denied while trying to connect to the Docker daemon socket", the app's user can't access it.

  • Check the socket exists on the host: ls -l /var/run/docker.sock
  • Run the container as root, or add the docker group's id to the container with group_add.

Image won't pull

Test the pull directly on the host: docker pull ghcr.io/dockroute/dockroute:latest

  • "manifest unknown" means the tag no longer exists. This template uses latest, so try pinning a specific version instead.
  • "toomanyrequests" is the Docker Hub rate limit. Log in with docker login to raise it.
  • "no space left on device" means a full disk. Reclaim space with docker system prune

"exec format error"

This means the image was built for a different CPU architecture than your server.

  • This image supports: amd64, arm64
  • Check yours with uname -m: x86_64 is amd64, aarch64 is arm64. Raspberry Pi and other ARM boards are the usual culprits.

Container keeps restarting

The unless-stopped restart policy relaunches the app after every crash, so the real error can scroll past.

  • Check the logs right after a restart, the last few lines before it died are the useful ones.
  • Get the exit code with docker inspect dockroute --format '{{.State.ExitCode}}'
  • Still stuck? Redeploy once with the restart policy set to no so the failure stays visible.

Required settings are blank

DOCKROUTE_DEFAULT_TARGET, DOCKROUTE_DOMAIN_FILTER, CLOUDFLARE_API_TOKEN, CLOUDFLARE_ACCOUNT_ID, CLOUDFLARE_TUNNEL_ID have no default value, and dockroute may crash or misbehave if left empty.

  • Fill them in on the deploy screen before hitting deploy.

Raise an issue

Found something which isn't working as it should? Here's how to report it.

A single container

DockRoute runs as one container, the simplest kind of app here. Just the one image to pull and nothing else wired up alongside it.

The app image

An image is the app packed up ready to go, everything DockRoute needs bundled into one download. This template pulls ghcr.io/dockroute/dockroute:latest, which Docker fetches once (about 98 MB) and then starts your own copy from.

Where the image comes from

Docker pulls its images from registries, public libraries of ready-built apps. DockRoute's comes from the GitHub Container Registry, published by dockroute.

Version tags

The bit after the colon in the image name is the version tag. Here it's latest, which always points at the newest build, so a redeploy can bump you to a newer release without you asking. Pin a specific tag if you would rather stay on one version.

Which machines it runs on

Every image is built for particular CPU types. This one ships for amd64, arm64, so it runs on both regular x86 servers and ARM boards like a Raspberry Pi.

Volumes

A volume is where DockRoute keeps its files so they survive an update or a restart. Without one, anything it saves would sit inside the container and vanish the moment it's recreated. This template mounts:

  • /var/run/docker.sock from /var/run/docker.sock on the host, read-only (a socket it talks to, not storage)

Environment variables

Environment variables are the settings you hand over when you deploy, things like a password or a timezone. DockRoute takes 9 of them, and 5 need a value before it'll start properly:

  • DOCKROUTE_PROVIDER, pick one of log, cloudflare. DNS provider ('log' is a credential-free dry run that only prints the desired state)
  • DOCKROUTE_OWNER_ID, defaults to default. Ownership id written to companion TXT records (lets several instances share a zone safely)
  • DOCKROUTE_POLICY, pick one of sync, upsert-only, create-only. Sync policy — sync creates/updates/deletes owned records, upsert-only never deletes, create-only never updates or deletes
  • DOCKROUTE_DEFAULT_TARGET, needs a value. Default target — fallback record value (IP or CNAME target) when a container omits the dockroute.target label
  • DOCKROUTE_DOMAIN_FILTER, needs a value. Domain filter — comma-separated allowlist of DNS zones DockRoute may touch (blank = every zone the token can see)
  • DOCKROUTE_RESYNC_SECONDS, defaults to 60. Resync interval in seconds (periodic full reconcile that backs up the event stream)
  • CLOUDFLARE_API_TOKEN, needs a value. Cloudflare API token — required when provider is cloudflare (Zone → DNS → Edit)
  • CLOUDFLARE_ACCOUNT_ID, needs a value. Cloudflare account ID (only needed for Cloudflare Tunnel publishing)
  • CLOUDFLARE_TUNNEL_ID, needs a value. Cloudflare tunnel ID — an existing tunnel; you keep running cloudflared yourself (only needed for Tunnel publishing)

Restart policy

The restart policy here is unless-stopped, so Docker restarts DockRoute after a crash or reboot, but leaves it off when you stop it on purpose. You can change this on the deploy screen. The choices are no (never restart), on-failure (only after a crash), unless-stopped (restart unless you stop it), and always (bring it back no matter what).

Networking

Nothing custom is set, so DockRoute sits on Docker's default bridge network: its own private space that reaches the outside world only through the ports it publishes.

Container name

Once it's deployed, Portainer names the container dockroute. That's what you'll spot in the containers list and use in commands like docker logs dockroute.

Platform

The platform is linux, the kind of system the container is built to run on. Docker and Portainer handle this on a normal Linux server.

Open source license

DockRoute is open source, released under the MIT license. In plain terms the code is out in the open, so you're free to run it and change it to fit what you need.

Portainer app templates

Zooming out, this whole page comes from a Portainer app template: a short recipe telling Portainer how to set DockRoute up. Add the template list to Portainer once, then deploying DockRoute is a click rather than a wall of config.