Skip to main content
Version: 3.18.0

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.

info

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.


Integration with Consul

Prerequisites​

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:

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.

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.

Add the Consul server to the source-controlled APISIX config.yaml configuration file used by the deployment:

config.yaml
discovery:
consul:
servers:
- http://consul.example:8500

Reload or restart APISIX through the deployment workflow so every instance receives the updated configuration.

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.