Integrate with HashiCorp Consul
HashiCorp Consul maintains a service catalog that records service instances and their health. Through service discovery, APISIX can watch the healthy instances registered under a Consul service name and use them as dynamic upstream nodes.
This guide starts a single Consul server and two sample services, registers health checks for both services, and configures APISIX to discover and load balance across the healthy instances.
This walkthrough runs APISIX, Consul, and the sample services in Docker on one network. Production deployments should use addresses that the APISIX instances can reach.
If all services run in Kubernetes, you typically do not need Consul because Kubernetes provides service discovery through Services and DNS.

Prerequisites
- Install Docker.
- Install cURL and jq to send requests and inspect Consul responses.
- Follow the Getting Started Tutorial to start APISIX with Docker.
- To configure APISIX with ADC, install ADC.
Start Consul
Start a single Consul server on the APISIX quickstart network. Port 8500 is bound to the loopback interface for local API access:
docker run \
--name consul \
-d \
-p 127.0.0.1:8500:8500 \
--network apisix-quickstart-net \
hashicorp/consul:2.0.3 \
consul agent \
-server \
-bootstrap-expect=1 \
-node=consul-server \
-client 0.0.0.0 \
-log-level info \
-data-dir=/consul/data
The Consul agent listens on all interfaces inside the container so that APISIX can reach it, while the published host port remains restricted to the loopback interface.
This single-server development configuration is intended only for local testing. Follow HashiCorp's deployment guidance when preparing a production Consul cluster.
Start Sample Web Services
Start two NGINX services on the APISIX quickstart network. Each service returns a different response so that load balancing is visible.
Create web1.conf:
cat > web1.conf <<'EOF'
events {
worker_connections 1024;
}
http {
access_log off;
server {
listen 80;
location / {
return 200 "Application 1 is running";
}
}
}
EOF
Create web2.conf:
cat > web2.conf <<'EOF'
events {
worker_connections 1024;
}
http {
access_log off;
server {
listen 80;
location / {
return 200 "Application 2 is running";
}
}
}
EOF
Start web1:
docker run -d \
--name web1 \
--network apisix-quickstart-net \
-v "$(pwd)/web1.conf:/etc/nginx/nginx.conf:ro" \
nginx:1.30.4-alpine
Start web2:
docker run -d \
--name web2 \
--network apisix-quickstart-net \
-v "$(pwd)/web2.conf:/etc/nginx/nginx.conf:ro" \
nginx:1.30.4-alpine
Register Services in Consul
Save the service-container addresses on the APISIX quickstart network:
export WEB1_IP="$(
docker inspect \
--format '{{(index .NetworkSettings.Networks "apisix-quickstart-net").IPAddress}}' \
web1
)"
export WEB2_IP="$(
docker inspect \
--format '{{(index .NetworkSettings.Networks "apisix-quickstart-net").IPAddress}}' \
web2
)"
Register web1 as the first instance of svc-a. The HTTP check lets Consul report the instance as healthy only while NGINX responds:
curl "http://127.0.0.1:8500/v1/agent/service/register" -X PUT \
-H "Content-Type: application/json" \
--data-binary @- <<EOF
{
"ID": "svc-a1",
"Name": "svc-a",
"Address": "$WEB1_IP",
"Port": 80,
"Check": {
"HTTP": "http://$WEB1_IP/",
"Interval": "5s",
"Timeout": "2s"
}
}
EOF
Register web2 as the second instance:
curl "http://127.0.0.1:8500/v1/agent/service/register" -X PUT \
-H "Content-Type: application/json" \
--data-binary @- <<EOF
{
"ID": "svc-a2",
"Name": "svc-a",
"Address": "$WEB2_IP",
"Port": 80,
"Check": {
"HTTP": "http://$WEB2_IP/",
"Interval": "5s",
"Timeout": "2s"
}
}
EOF
❶ Address and Port: Network location that APISIX uses for the discovered upstream node.
❷ Check: HTTP health check that Consul runs every five seconds. APISIX discovers only instances whose checks are passing.
Verify that Consul reports both service instances as passing:
curl "http://127.0.0.1:8500/v1/health/service/svc-a?passing=true" | \
jq '[.[] | {
ID: .Service.ID,
Address: .Service.Address,
Port: .Service.Port,
Status: .Checks[-1].Status
}]'
The response should contain entries for svc-a1 and svc-a2, both with a passing status.
Connect Consul to APISIX
This walkthrough uses the APISIX Docker Quickstart. Because the Quickstart does not mount a source configuration file, append the Consul configuration inside the running container. This change is intended only for local evaluation and disappears when the container is replaced:
docker exec -i apisix-quickstart sh -c \
'sed -i "/^\.\.\.$/d" /usr/local/apisix/conf/config.yaml &&
cat >> /usr/local/apisix/conf/config.yaml' <<'EOF'
discovery:
consul:
servers:
- http://consul:8500
...
EOF
The servers array contains the Consul HTTP API addresses that APISIX watches for service and health updates.
Reload APISIX for configuration changes to take effect:
docker exec apisix-quickstart apisix reload
For a long-running deployment, keep the same discovery settings in the deployment's source of truth. See Persist the Discovery Configuration after completing the walkthrough.
Create a Route in APISIX
Create a route and configure the upstream to use Consul for service discovery of svc-a:
- Admin API
- ADC
Create the route through the Admin API:
curl -i "http://127.0.0.1:9180/apisix/admin/routes/consul-web-route" -X PUT \
--data-binary @- <<'EOF'
{
"uri": "/consul/web/*",
"upstream": {
"service_name": "svc-a",
"discovery_type": "consul",
"type": "roundrobin"
}
}
EOF
An HTTP/1.1 201 Created response verifies that the route was created.
Create adc.yaml with the route configuration:
services:
- name: consul-web
routes:
- name: consul-web-route
uris:
- /consul/web/*
upstream:
service_name: svc-a
discovery_type: consul
type: roundrobin
ADC reconciles services as desired state. The label selector limits this example to its own resources. Preview the scoped changes and confirm that they contain no unintended updates or deletions:
adc diff -f adc.yaml \
--include-resource-type service \
--label-selector docs-example=consul-service-discovery
Synchronize the reviewed service configuration:
adc sync -f adc.yaml \
--include-resource-type service \
--label-selector docs-example=consul-service-discovery
Verify Service Discovery
Verify that APISIX discovered both service instances:
curl -fsS "http://127.0.0.1:9090/v1/discovery/consul/dump" | \
jq --arg web1 "$WEB1_IP" --arg web2 "$WEB2_IP" -e '
([.services["svc-a"][] | select(.port == 80) | .host] | sort) ==
([$web1, $web2] | sort)
'
The command returns true when the discovered node set contains both service addresses. Send several requests to the route:
for _ in $(seq 1 10); do
curl "http://127.0.0.1:9080/consul/web/"
echo
done
Each request should return one of the following responses. You may see both responses, and their order can vary:
Application 1 is running
Application 2 is running
Stop web1 to verify that Consul removes an unhealthy instance from APISIX discovery:
docker stop web1
Poll the APISIX Control API until the discovered svc-a node set contains only web2. The command makes up to 20 attempts at one-second intervals and exits with a nonzero status if discovery does not converge:
for _ in $(seq 1 20); do
nodes="$(curl --max-time 2 -fsS \
"http://127.0.0.1:9090/v1/discovery/consul/dump" | \
jq -r '.services["svc-a"] | map("\(.host):\(.port)") | join(",")')" || nodes=""
[ "$nodes" = "$WEB2_IP:80" ] && break
sleep 1
done
[ "$nodes" = "$WEB2_IP:80" ]
A successful command confirms that APISIX removed web1 from its discovered nodes. Send another request to verify that traffic reaches web2:
curl "http://127.0.0.1:9080/consul/web/"
The response should be:
Application 2 is running
Restart web1 after verification:
docker start web1
Persist the Discovery Configuration
The in-container edit used in this walkthrough is ephemeral. For a long-running deployment, store the Consul settings in the deployment's source of truth. Replace the example address with a Consul endpoint that every APISIX instance can reach.
- Host or Docker
- Kubernetes (Helm)
Add the Consul server to the source-controlled APISIX config.yaml configuration file used by the deployment:
discovery:
consul:
servers:
- http://consul.example:8500
Reload or restart APISIX through the deployment workflow so every instance receives the updated configuration.
For an APISIX deployment managed with the official Helm chart, add the Consul server to the source-controlled values.yaml. The chart renders this configuration into config.yaml:
apisix:
discovery:
enabled: true
registry:
consul:
servers:
- http://consul.example:8500
This fragment configures APISIX only. The Consul cluster and registered services must be reachable from the APISIX pods. Apply the values through the release's normal deployment workflow. See Helm Chart for the helm upgrade workflow.
Next Steps
Use the service discovery Control API endpoints to inspect discovered services and troubleshoot registry updates. See the Control API reference for more information.
In addition to Consul, APISIX also supports the integration with Eureka, Nacos, and other service discovery platforms.