Skip to main content

Service Discovery

Nekzus provides automatic service discovery to find and catalog services running on your local network. The discovery system supports multiple sources including Docker containers, mDNS/Bonjour services, and Kubernetes clusters.


Overview

The discovery system uses a proposal-based workflow where discovered services are presented as proposals for administrator approval before being added to the service catalog and route table.

Architecture

d2

Key Features

  • Multi-source discovery: Docker, mDNS, and Kubernetes
  • Confidence scoring: Prioritizes explicitly labeled services
  • Proposal workflow: Review before adding to catalog
  • Deduplication: Prevents duplicate proposals
  • Self-exclusion: Automatically excludes Nekzus containers
  • Network filtering: Fine-grained Docker network control
  • Label inheritance: Kubernetes namespace-level configuration
  • WebSocket notifications: Real-time proposal updates

Discovery Sources

Nekzus supports three discovery sources, each with its own worker process.

Docker discovery scans running containers using the Docker API and creates proposals based on container labels and exposed ports.

Features:

  • Automatic HTTP port detection via probing
  • Multi-network support with filtering
  • Self-container exclusion
  • System container filtering
  • Label-based configuration

Docker Discovery

Docker discovery is the most commonly used discovery method, automatically finding services running in Docker containers.

Configuration

discovery:
enabled: true
docker:
enabled: true
socket_path: "unix:///var/run/docker.sock"
poll_interval: "30s"

# Network mode: "all" | "first" | "preferred"
network_mode: "all"

# Optional: Only discover on specific networks
networks:
- "app-network"
- "web-tier"

# Optional: Exclude specific networks
exclude_networks:
- "bridge"
- "host"

Configuration Options

OptionTypeDefaultDescription
enabledbooleantrueEnable Docker discovery
socket_pathstringunix:///var/run/docker.sockDocker socket path
poll_intervalduration30sHow often to scan for containers
network_modestringallNetwork selection mode
networks[]string[]Include only these networks
exclude_networks[]string[]Exclude these networks

Network Modes

ModeBehavior
allUse all container networks (after filtering)
firstUse only the first available network
preferredUse first network from networks list, fallback to first available

Container Labels Reference

Add these labels to your Docker containers to control discovery behavior:

Core Labels

LabelTypeDefaultDescription
nekzus.enableboolean-Explicitly enable (true) or disable (false) discovery
nekzus.app.idstringcontainer nameUnique application identifier
nekzus.app.namestringcontainer nameDisplay name in catalog
nekzus.app.iconstring-Icon URL or emoji
nekzus.app.tagsstringauto-generatedComma-separated tags

Route Labels

LabelTypeDefaultDescription
nekzus.route.pathstring/apps/{app_id}/Proxy path base
nekzus.route.scopesstringaccess:{app_id}Comma-separated required scopes
nekzus.route.strip_prefixbooleantrueStrip path prefix before proxying
nekzus.route.rewrite_htmlbooleantrueRewrite HTML for SPA support
nekzus.route.websocketbooleanfalseEnable WebSocket proxying
nekzus.schemestringauto-detectedProtocol: http or https

Port Discovery Labels

LabelTypeDefaultDescription
nekzus.primary_portinteger-Discover only this specific port
nekzus.discover.all_portsbooleanfalseDiscover all TCP ports (bypass HTTP filter)

System Labels

LabelTypeDescription
nekzus.skipbooleanSkip this container entirely
nekzus.teststringMark as test container (bypasses system container filter)

Docker Compose Example

version: "3.8"

services:
grafana:
image: grafana/grafana:latest
container_name: grafana
ports:
- "3000:3000"
labels:
# Enable discovery
nekzus.enable: "true"

# Application metadata
nekzus.app.id: "grafana"
nekzus.app.name: "Grafana Dashboard"
nekzus.app.icon: "https://grafana.com/favicon.ico"
nekzus.app.tags: "monitoring,metrics,dashboard"

# Route configuration
nekzus.route.path: "/apps/grafana/"
nekzus.route.scopes: "access:grafana,read:metrics"
nekzus.route.websocket: "true"
nekzus.route.strip_prefix: "true"

# Scheme override
nekzus.scheme: "http"
networks:
- monitoring

# Multi-port service example
api-service:
image: my-api:latest
ports:
- "8080:8080"
- "9090:9090"
labels:
nekzus.enable: "true"
nekzus.app.id: "api"
nekzus.app.name: "API Service"
# Only discover port 8080 (skip metrics port 9090)
nekzus.primary_port: "8080"

networks:
monitoring:
driver: bridge

HTTP Port Detection

Docker discovery uses HTTP probing to detect web services on non-standard ports:

  1. Known non-HTTP ports are skipped: SSH (22), MySQL (3306), PostgreSQL (5432), Redis (6379), etc.
  2. HTTP HEAD requests are sent to candidate ports
  3. Any HTTP response (including 4xx/5xx) indicates an HTTP service
  4. Timeout: 2 seconds per probe
Override Port Detection

Use nekzus.primary_port to specify exactly which port to use, or nekzus.discover.all_ports: "true" to discover all TCP ports regardless of protocol.

Confidence Scoring

Docker discovery assigns confidence scores based on container metadata:

ConditionScore
Base score (any container)0.50
Has nekzus.app.id label0.85
Has nekzus.enable: "true"0.95
Well-known image (nginx, grafana, etc.)+0.20
Common web ports (80, 8080, 3000)+0.10
Traefik/Caddy labels present+0.15

Maximum score: 1.0


mDNS Discovery

mDNS (Multicast DNS) discovery finds services advertising themselves via Bonjour/Zeroconf on the local network.

Configuration

discovery:
enabled: true
mdns:
enabled: true
scan_interval: "60s"
services:
- "_http._tcp"
- "_https._tcp"
- "_homeassistant._tcp"

Configuration Options

OptionTypeDefaultDescription
enabledbooleantrueEnable mDNS discovery
scan_intervalduration60sHow often to scan for services
services[]stringsee belowService types to discover

Default Service Types

When no services are specified, the following types are scanned:

  • _http._tcp - HTTP services
  • _https._tcp - HTTPS services
  • _ssh._tcp - SSH services
  • _smb._tcp - SMB/CIFS file shares
  • _printer._tcp - Network printers
  • _workstation._tcp - Workstations

TXT Record Metadata

mDNS services can provide metadata via TXT records:

TXT KeyDescription
app_idApplication identifier
app_nameDisplay name
pathRoute path base
iconIcon URL
tagsComma-separated tags
scopesRequired scopes
schemeProtocol override
nekzus_enableExplicit enable flag

Confidence Scoring (mDNS)

ConditionScore
Base score (any mDNS service)0.70
Has app_id TXT record0.85
Has nekzus_enable: "true"0.95
Known service type (Home Assistant, HomeKit)+0.10

Kubernetes Discovery

Kubernetes discovery finds Services and Ingresses in your cluster, with smart inference for common patterns.

Configuration

discovery:
enabled: true
kubernetes:
enabled: true
kubeconfig: "" # Empty for in-cluster or default kubeconfig
poll_interval: "30s"
namespaces:
- "default"
- "apps"
- "production"

Configuration Options

OptionTypeDefaultDescription
enabledbooleanfalseEnable Kubernetes discovery
kubeconfigstring""Path to kubeconfig (empty for auto-detect)
poll_intervalduration30sHow often to scan
namespaces[]string[""]Namespaces to watch (empty = all)

Labels and Annotations

Kubernetes services and ingresses support the same labels as Docker, applied as Kubernetes labels or annotations:

apiVersion: v1
kind: Service
metadata:
name: grafana
namespace: monitoring
labels:
nekzus.enable: "true"
nekzus.app.id: "grafana"
annotations:
nekzus.app.name: "Grafana Dashboard"
nekzus.app.tags: "monitoring,metrics"
nekzus.route.path: "/apps/grafana/"
spec:
type: ClusterIP
ports:
- port: 3000
selector:
app: grafana
Labels vs Annotations

Use annotations for values that may contain special characters or exceed label length limits. Labels are checked first, then annotations.

Namespace-Level Configuration

Namespaces can provide default configuration that services inherit:

apiVersion: v1
kind: Namespace
metadata:
name: production
labels:
nekzus.enable: "true"
nekzus.app.tags: "production"
nekzus.route.scopes: "access:production"
istio-injection: enabled

Services in this namespace automatically inherit:

  • Enabled status (unless explicitly disabled)
  • Tags (production)
  • Scopes (access:production)
  • Istio detection

Auto-Detection Features

Kubernetes discovery automatically detects services based on several criteria:

Service Type Detection

Service TypeAuto-Discovered When
LoadBalancerHas identifying labels (app.kubernetes.io/name, etc.)
NodePortHas identifying labels
ClusterIPHas frontend/UI component labels

Component Detection

Services with these app.kubernetes.io/component values are auto-discovered:

  • frontend
  • ui
  • web
  • dashboard

Helm Chart Recognition

The following Helm charts are automatically recognized and tagged:

ChartTags Added
grafanamonitoring, grafana
prometheusmonitoring, prometheus
argocdcicd, argocd
lokilogging, loki
tempotracing, tempo
jaegertracing, jaeger
jenkinscicd, jenkins
gitlabcicd, gitlab
harborregistry, harbor
vaultsecrets, vault
consulservice-mesh, consul
linkerdservice-mesh, linkerd

Istio/Service Mesh Detection

Services with Istio sidecar injection are automatically discovered:

  • Namespace label: istio-injection: enabled
  • Service label: istio-injection: enabled
  • Service label: istio.io/rev
  • Service label: service.istio.io/canonical-name
  • Annotation: sidecar.istio.io/inject: "true"

Backend Service Filtering

These chart types are NOT auto-discovered (considered backend services):

  • Databases: postgresql, mysql, mariadb, mongodb, cassandra
  • Caches: redis, memcached
  • Message queues: kafka, rabbitmq, nats
  • Infrastructure: etcd, consul, vault, elasticsearch

Ingress Discovery

Ingresses are discovered when they have:

  • Explicit nekzus.enable: "true" label
  • Standard Kubernetes labels (app.kubernetes.io/name)
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
name: grafana
namespace: monitoring
labels:
nekzus.enable: "true"
annotations:
nekzus.app.name: "Grafana"
spec:
tls:
- hosts:
- grafana.example.com
secretName: grafana-tls
rules:
- host: grafana.example.com
http:
paths:
- path: /
pathType: Prefix
backend:
service:
name: grafana
port:
number: 3000

Confidence Scoring (Kubernetes)

ConditionScore
Base score (any service)0.50
Has nekzus.app.id label0.85
Has nekzus.enable: "true"0.95
Ingress-exposed service0.80
Ingress with explicit enable0.95
Istio/Service Mesh detected+0.20
Recognized Helm chart+0.20
LoadBalancer/NodePort type+0.10
Common web ports+0.10
TLS configured on Ingress+0.05

Skipped Namespaces

The following system namespaces are always skipped unless services explicitly enable discovery:

  • kube-system
  • kube-public
  • kube-node-lease

Proposal Workflow

All discovered services go through a proposal workflow before being added to the catalog.

Proposal States

d2

Proposal Lifecycle

  1. Discovery: Worker finds a service
  2. Deduplication: Manager checks if proposal already exists
  3. Pending: Proposal awaits administrator action
  4. Action:
    • Approve: Creates app and route, removes proposal
    • Dismiss: Marks as dismissed, prevents re-discovery

Proposal Structure

{
"id": "proposal_docker_http_grafana_3000",
"source": "docker",
"detectedScheme": "http",
"detectedHost": "172.17.0.5",
"detectedPort": 3000,
"availablePorts": [
{"port": 3000, "scheme": "http"}
],
"confidence": 0.95,
"suggestedApp": {
"id": "grafana",
"name": "Grafana Dashboard",
"icon": "https://grafana.com/favicon.ico",
"tags": ["monitoring", "docker"],
"endpoints": {
"lan": "http://172.17.0.5:3000"
}
},
"suggestedRoute": {
"routeId": "route:grafana",
"appId": "grafana",
"pathBase": "/apps/grafana/",
"to": "http://172.17.0.5:3000",
"scopes": ["access:grafana"],
"stripPrefix": true
},
"securityNotes": [
"Discovered via Docker API",
"JWT required",
"Private network address"
]
}

Rediscovery

To clear dismissed proposals and trigger a fresh discovery scan:

curl -X POST https://localhost:8443/api/v1/discovery/rediscover \
-H "Authorization: Bearer $TOKEN"

This clears both dismissed and pending proposals, allowing previously dismissed services to be rediscovered.


API Reference

List Proposals

Returns all pending discovery proposals.

GET /api/v1/discovery/proposals
Authorization: Bearer <token>

Response:

[
{
"id": "proposal_docker_http_grafana_3000",
"source": "docker",
"confidence": 0.95,
...
}
]

Approve Proposal

Approves a proposal, creating the app and route.

POST /api/v1/discovery/proposals/{proposalId}/approve
Authorization: Bearer <token>
Content-Type: application/json

{
"port": 3000 // Optional: select specific port from availablePorts
}

Response:

{
"status": "approved",
"id": "proposal_docker_http_grafana_3000",
"app": { ... },
"route": { ... }
}

Dismiss Proposal

Dismisses a proposal, preventing it from reappearing.

POST /api/v1/discovery/proposals/{proposalId}/dismiss
Authorization: Bearer <token>

Response:

{
"status": "dismissed",
"id": "proposal_docker_http_grafana_3000"
}

Trigger Rediscovery

Clears all proposals and triggers a fresh scan.

POST /api/v1/discovery/rediscover
Authorization: Bearer <token>

Response:

{
"status": "success",
"message": "Rediscovery triggered. Discovery workers will scan for new services.",
"dismissedCleared": 5,
"activeCleared": 2
}

Troubleshooting

Docker Discovery Issues

Docker discovery not finding containers

Check Docker socket access:

# Verify socket path
ls -la /var/run/docker.sock

# Test Docker API access
curl --unix-socket /var/run/docker.sock http://localhost/containers/json

Check discovery configuration:

discovery:
enabled: true
docker:
enabled: true
socket_path: "unix:///var/run/docker.sock"

Check logs for errors:

docker logs nekzus 2>&1 | grep -i discovery
Container discovered but wrong port detected

Use explicit port labeling:

labels:
nekzus.primary_port: "8080"

Or discover all ports:

labels:
nekzus.discover.all_ports: "true"
Nekzus container appearing in proposals

The system container filter should exclude Nekzus containers automatically. If not:

  1. Check container name contains nekzus or nekzus
  2. Add explicit skip label:
labels:
nekzus.skip: "true"
Network filtering not working

Verify network names match exactly:

docker network ls

Check configuration:

discovery:
docker:
networks:
- "my-network" # Must match exactly
network_mode: "preferred"

Kubernetes Discovery Issues

Kubernetes discovery not connecting

Check kubeconfig:

# Verify kubectl works
kubectl get services --all-namespaces

# Check kubeconfig path
echo $KUBECONFIG
ls -la ~/.kube/config

For in-cluster deployment:

Ensure the ServiceAccount has proper RBAC permissions:

apiVersion: rbac.authorization.k8s.io/v1
kind: ClusterRole
metadata:
name: nekzus-discovery
rules:
- apiGroups: [""]
resources: ["services", "namespaces"]
verbs: ["get", "list", "watch"]
- apiGroups: ["networking.k8s.io"]
resources: ["ingresses"]
verbs: ["get", "list", "watch"]
Services not being discovered in Kubernetes
  1. Check if namespace is in the watch list
  2. Verify service has identifying labels
  3. Check if it's a system namespace (kube-system)
  4. Add explicit enable label:
labels:
nekzus.enable: "true"
Ingress discovery not working

Ingresses need one of:

  • nekzus.enable: "true" label
  • app.kubernetes.io/name label
metadata:
labels:
nekzus.enable: "true"

General Issues

Proposals keep reappearing after dismissal

Proposals use deterministic IDs based on source, host, and port. If the ID changes, a new proposal is created.

Common causes:

  • Container IP changed (use container name instead)
  • Port changed
  • Discovery configuration changed

Solution:

Use stable identifiers like container names or add explicit labels.

High CPU usage from discovery

Increase poll intervals:

discovery:
docker:
poll_interval: "60s" # Increase from 30s
kubernetes:
poll_interval: "60s"
mdns:
scan_interval: "120s" # Increase from 60s
WebSocket not receiving discovery events
  1. Verify WebSocket connection is established
  2. Check authentication token is valid
  3. Monitor WebSocket for proposal_approved, proposal_dismissed events
ws.onmessage = (event) => {
const msg = JSON.parse(event.data);
if (msg.type === 'discovery' || msg.type === 'proposal_approved') {
console.log('Discovery event:', msg);
}
};

Best Practices

Label Your Containers

Always use explicit labels for production services:

labels:
nekzus.enable: "true"
nekzus.app.id: "my-service"
nekzus.app.name: "My Service"

Use Network Filtering

Isolate discovery to specific networks:

discovery:
docker:
networks:
- "frontend-network"
exclude_networks:
- "bridge"
- "host"

Kubernetes Namespace Organization

Use namespace-level labels for environment-wide settings:

# Namespace
metadata:
labels:
nekzus.enable: "true"
nekzus.app.tags: "staging"

Security Considerations

  1. Review proposals carefully before approving
  2. Use scopes to limit access to services
  3. Check security notes in proposals
  4. Monitor port exposure warnings for Docker services
  5. Use TLS for upstream services when possible

Performance Tuning

For large environments:

  • Increase poll_interval to reduce API load
  • Filter networks/namespaces to reduce scan scope
  • Use explicit labels instead of relying on auto-detection