Skip to content

Consul — Service Discovery & Service Mesh Guide

DodaTech Updated 2026-06-24 6 min read

In this tutorial, you'll learn about Consul. We cover key concepts, practical examples, and best practices to help you understand and apply this topic effectively.

HashiCorp Consul is a service networking solution that provides service discovery, health checking, KV storage, and a secure service mesh (Connect) across any runtime platform and cloud.

What You'll Learn

Why It Matters

In dynamic environments with auto-scaling and container Orchestration, IP addresses change constantly. Applications cannot use hardcoded IPs. Consul provides a DNS-based service registry where services register themselves and clients discover them by name. DodaTech uses Consul for service discovery across 300+ Microservices, enabling seamless communication regardless of underlying infrastructure changes.

Real-World Use

DodaZIP's API Gateway discovers upstream services via Consul DNS. When a new instance of the user-service starts, it registers with Consul, and the gateway automatically routes traffic to it. When an instance fails health checks, Consul removes it from service — without any configuration changes.

flowchart TD
    A[Service A] --> B[Consul Agent]
    C[Service B] --> D[Consul Agent]
    E[API Gateway] --> F[Consul DNS :8600]
    F --> G["user-service.service.consul"]
    G --> B
    G --> D
    B --> H[Health Check: TCP 8080]
    D --> I[Health Check: HTTP /health]
    H --> J{Passing?}
    J -->|YES| K[Registered]
    J -->|NO| L[Deregistered]
    style B fill:#E03875,color:#fff
    style D fill:#E03875,color:#fff
â„šī¸ Info

Prerequisites: Basic Linux administration. Understanding of DNS and networking concepts.

Installation

# Install Consul
wget -O- https://apt.releases.hashicorp.com/gpg | gpg --dearmor | sudo tee /usr/share/keyrings/hashicorp-archive-keyring.gpg
echo "deb [signed-by=/usr/share/keyrings/hashicorp-archive-keyring.gpg] https://apt.releases.hashicorp.com $(lsb_release -cs) main" | sudo tee /etc/apt/sources.list.d/hashicorp.list
sudo apt update && sudo apt install consul

# Start Consul in dev mode
consul agent -dev -ui

# Expected output:
# ==> Starting Consul agent...
# ==> Consul agent running!
#     Version: '1.19.0'
#     Node name: 'consul-server'
#     Datacenter: 'dc1'
#     Server: true (bootstrap: true)
#     Client Addr: [127.0.0.1] (HTTP: 8500, HTTPS: -1, gRPC: 8502, DNS: 8600)
#     Cluster Addr: 127.0.0.1 (LAN: 8301, WAN: 8302)

# Verify
consul members

# Expected output:
# Node            Address         Status  Type    Build   Protocol  DC
# consul-server   127.0.0.1:8301  alive   server  1.19.0  2         dc1

Server Configuration

# /etc/consul.d/consul.hcl
datacenter = "dc1"
data_dir = "/opt/consul"

server = true
bootstrap_expect = 3

ui_config {
  enabled = true
}

client_addr = "0.0.0.0"
bind_addr = "{{ GetInterfaceIP \"eth0\" }}"

advertise_addr = "{{ GetInterfaceIP \"eth0\" }}"

retry_join = ["consul-01.dodatech.com", "consul-02.dodatech.com", "consul-03.dodatech.com"]

encrypt = "aPu1gZxV7VJqVfL5kH4wYQ=="

connect {
  enabled = true
}

ports {
  grpc = 8502
}

telemetry {
  prometheus_retention_time = "30s"
}

acl = {
  enabled = true
  default_policy = "deny"
  enable_token_persistence = true
}

Service Registration

# /etc/consul.d/user-service.hcl
service {
  name = "user-service"
  id   = "user-service-01"
  port = 8080

  tags = ["api", "production", "v2"]

  meta = {
    version    = "2.5.0"
    commit_sha = "a1b2c3d"
    owner      = "platform-team"
  }

  connect {
    sidecar_service {
      port = 20000
      proxy {
        upstreams {
          destination_name = "postgres"
          local_bind_port  = 5432
        }
        upstreams {
          destination_name = "redis"
          local_bind_port  = 6379
        }
      }
    }
  }

  check {
    id       = "user-service-http"
    name     = "HTTP Health Check"
    http     = "http://localhost:8080/health"
    method   = "GET"
    interval = "10s"
    timeout  = "2s"
    success_threshold = 1
    failure_threshold = 3
    deregister_critical_service_after = "5m"
  }

  check {
    id       = "user-service-ttl"
    name     = "TTL Check"
    ttl      = "30s"
    notes    = "Application must report health via API"
    deregister_critical_service_after = "10m"
  }
}

Service Discovery

# DNS-based discovery
dig @127.0.0.1 -p 8600 user-service.service.consul

# Expected output:
# ; <<>> DiG 9.18 <<>> user-service.service.consul
# ;; ANSWER SECTION:
# user-service.service.consul. 0 IN A 10.0.1.50
# user-service.service.consul. 0 IN A 10.0.1.51
# user-service.service.consul. 0 IN A 10.0.1.52

# HTTP API discovery
curl http://localhost:8500/v1/catalog/service/user-service

# Expected output:
# [
#   {
#     "ID": "a1b2c3d4-...", "#     "Node": "web-01"", "#     "Address": "10.0.1.50"", "#     "ServiceID": "user-service-01"", "#     "ServiceName": "user-service"", "#     "ServicePort": 8080", "#     "ServiceTags": ["api"", "production", "v2"]
#   }
# ]

# Health filtering (only passing instances)
curl http://localhost:8500/v1/health/service/user-service?passing

# Register service via API
curl -X PUT -d '{
  "Name": "cache-service",
  "Port": 6379,
  "Check": {
    "TCP": "localhost:6379",
    "Interval": "10s"
  }
}' http://localhost:8500/v1/agent/service/register

KV Store

# Write a key
consul kv put config/dodazip/api_url https://api.dodatech.com

# Expected output:
# Success! Data written to: config/dodazip/api_url

# Write multiple keys in a hierarchy
consul kv put config/dodazip/database/host postgres.dodatech.com
consul kv put config/dodazip/database/port 5432
consul kv put config/dodazip/database/name dodazip

# Get a key
consul kv get config/dodazip/api_url

# Expected output:
# https://api.dodatech.com

# Get all keys with prefix
consul kv get -keys config/dodazip/

# Expected output:
# config/dodazip/api_url
# config/dodazip/database/host
# config/dodazip/database/port
# config/dodazip/database/name

# Delete a key
consul kv delete config/dodazip/api_url

Consul-Template

# /etc/consul-template.d/config.hcl
template {
  source      = "/etc/consul-template/templates/nginx.conf.ctmpl"
  destination = "/etc/nginx/conf.d/upstream.conf"
  command     = "nginx -s reload"
  perms       = 0644
}

template {
  source      = "/etc/consul-template/templates/prometheus.yml.ctmpl"
  destination = "/etc/prometheus/prometheus.yml"
  command     = "systemctl reload prometheus"
  perms       = 0644
}
# /etc/consul-template/templates/nginx.conf.ctmpl
upstream user-service {
    least_conn;
    {{ range service "user-service.passing" }}
    server {{ .Address }}:{{ .Port }};
    {{ end }}
}

upstream api-gateway {
    {{ range service "api-gateway.passing" }}
    server {{ .Address }}:{{ .Port }} max_fails=3 fail_timeout=30s;
    {{ end }}
}
# Run consul-template
consul-template -config /etc/consul-template.d/config.hcl

# Expected output:
# 2026/06/24 10:00:00 [INFO] consul-template v0.37.0
# 2026/06/24 10:00:00 [INFO] Quiescence: min=10s, max=20s
# 2026/06/24 10:00:01 [INFO] rendered /etc/nginx/conf.d/upstream.conf

Service Mesh with Connect

# Intentions for service-to-service authorization
# /etc/consul.d/intentions.hcl
Kind = "service-intentions"

List = [
  {
    Name = "api-gateway]
    Sources = [
      {
        Name   = "user-service]
        Action = "allow"
      },
      {
        Name   = "auth-service"
        Action = "allow"
      }
    ]
  },
  {
    Name = "postgres"
    Sources = [
      {
        Name   = "user-service]
        Action = "allow"
      },
      {
        Name   = "auth-service"
        Action = "allow"
      }
    ]
  }
]
# Create intention via CLI
consul intention create api-gateway user-service

# Expected output:
# Created: api-gateway => user-service (allow)

# Verify intentions
consul intention check api-gateway user-service

# Expected output:
# api-gateway can access user-service (allowed)

Common Configuration Mistakes

  1. Not setting deregister_critical_service_after: Without this, failed services remain in the catalog indefinitely as critical, cluttering the service list. Set to 5-10 minutes for auto-cleanup.

  2. Running servers without Bootstrap_expect: Without this, servers cannot form a quorum and the cluster remains unstable. Set to the expected server count (3 or 5).

  3. Mixing LAN and WAN gossip encryption keys: Different datacenters need different gossip keys but must share the same Replication token. Keep keys organized in Vault or a secrets manager.

  4. Forgetting to set up ACLs: Without ACLs, any client can register or deregister any service. Enable ACLs with default_policy = "deny" and generate tokens per service.

  5. Not using health check passing filters: Discovered services may include unhealthy instances. Always use /v1/health/service/name?passing or the .passing template filter.

Practice Questions

  1. What is the difference between Consul and etcd? Answer: Consul provides service discovery, health checking, DNS interface, KV store, and service mesh. etcd is a distributed key-value store without discovery or health checking.

  2. How does Consul perform health checks? Answer: Consul supports script, HTTP, TCP, TTL, Docker, and gRPC health checks. Checks run on the Consul client agent, which reports status to the server cluster.

  3. What is Consul Connect? Answer: Connect provides automatic mTLS between services using sidecar proxies. It encrypts all service-to-service traffic and enforces access control via intentions.

  4. How does Consul-Template work? Answer: Consul-Template watches Consul for changes to services or KV keys. When changes occur, it renders templates (using Go template syntax) and optionally runs a command.

Challenge

Deploy a production Consul cluster: set up a 3-server cluster with Raft consensus, configure ACLs with default deny, register a user-service with HTTP health check and TTL health check, register a postgres service with TCP check, set up Consul-Template to generate NGINX upstream configuration dynamically, configure service mesh with Connect sidecar proxies, create intentions allowing only api-gateway to call user-service, and set up Consul DNS forwarding for Kubernetes pods.

Built by the developers of Doda Browser, DodaZIP, and Durga Antivirus Pro.

Built by the developers of DodaTech

Doda Browser, DodaZIP & Durga Antivirus Pro