Skip to main content
Version: 3.18.0

Integrate with Netflix Eureka

Netflix Eureka is a REST-based service registry that tracks application instances and their availability. Through service discovery, APISIX periodically retrieves the Eureka registry and builds upstream nodes from instances whose status is UP. Routes can therefore follow registry changes without maintaining a static node list in APISIX.

This guide builds a standalone Eureka server with Spring Cloud Netflix and starts two sample services. It then registers the services through the Eureka REST API and configures APISIX to discover and load balance across them.

info

This walkthrough runs APISIX, Eureka, and the sample services in Docker on one network. Production deployments should use a highly available Eureka deployment and addresses that every APISIX instance can reach.

If all services run in Kubernetes, you typically do not need Eureka because Kubernetes provides service discovery through Services and DNS.

Prerequisites​

Start Eureka​

Netflix recommends running Eureka 2.x through Spring Cloud Netflix. Build a local image from the Spring Cloud Netflix starter so that the server dependencies and build images use fixed versions.

Create the project directory:

mkdir -p eureka-server/src/main/java/com/example/eureka
cd eureka-server

Create the Maven project file:

cat > pom.xml <<'EOF'
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<groupId>com.example</groupId>
<artifactId>eureka-server</artifactId>
<version>1.0.0</version>
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>4.0.8</version>
<relativePath />
</parent>
<properties>
<java.version>17</java.version>
<spring-cloud.version>2025.1.3</spring-cloud.version>
</properties>
<dependencyManagement>
<dependencies>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-dependencies</artifactId>
<version>${spring-cloud.version}</version>
<type>pom</type>
<scope>import</scope>
</dependency>
</dependencies>
</dependencyManagement>
<dependencies>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-netflix-eureka-server</artifactId>
</dependency>
</dependencies>
<build>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
</plugin>
</plugins>
</build>
</project>
EOF

Create the server application:

cat > src/main/java/com/example/eureka/EurekaServerApplication.java <<'EOF'
package com.example.eureka;

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.cloud.netflix.eureka.server.EnableEurekaServer;

@SpringBootApplication
@EnableEurekaServer
public class EurekaServerApplication {
public static void main(String[] args) {
SpringApplication.run(EurekaServerApplication.class, args);
}
}
EOF

Create the multi-stage container image definition. It builds the application and copies the result into a Java runtime image:

cat > Dockerfile <<'EOF'
FROM maven:3.9.12-eclipse-temurin-17@sha256:a0603aab698040d9c94259f379ec0487da1678560748d6c7508483034033c53d AS build
WORKDIR /app
COPY pom.xml .
COPY src src
RUN mvn --batch-mode --no-transfer-progress -Dmaven.test.skip=true package

FROM eclipse-temurin:17.0.20_8-jre-jammy@sha256:ec72ba5962b45ae4e7f96bfb5ebf6eeb34a488b967f937c8e14f0aaec688954f
WORKDIR /app
COPY --from=build /app/target/eureka-server-1.0.0.jar eureka-server.jar
EXPOSE 8761
ENTRYPOINT ["java", "-jar", "/app/eureka-server.jar"]
EOF

Build the image:

docker build -t apisix-eureka-server:2025.1.3 .

Start a standalone Eureka server on the APISIX quickstart network. Port 8761 is bound to the loopback interface for local API access:

docker run -d \
--name eureka \
--network apisix-quickstart-net \
-p 127.0.0.1:8761:8761 \
-e SERVER_PORT=8761 \
-e EUREKA_CLIENT_REGISTER_WITH_EUREKA=false \
-e EUREKA_CLIENT_FETCH_REGISTRY=false \
-e EUREKA_SERVER_RESPONSE_CACHE_UPDATE_INTERVAL_MS=1000 \
apisix-eureka-server:2025.1.3

The client settings disable the peer-registration behavior that Eureka enables by default. The shorter response-cache interval keeps this local walkthrough responsive when an instance status changes.

Verify that the registry API is available:

curl --retry 30 --retry-delay 1 --retry-all-errors \
-i "http://127.0.0.1:8761/eureka/apps"

An HTTP/1.1 200 response confirms that Eureka is ready.

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 Eureka​

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 the WEB application:

curl "http://127.0.0.1:8761/eureka/apps/WEB" -X POST \
-H "Content-Type: application/json" \
--data-binary @- <<EOF
{
"instance": {
"instanceId": "web1",
"hostName": "$WEB1_IP",
"ipAddr": "$WEB1_IP",
"app": "WEB",
"status": "UP",
"port": {
"\$": 80,
"@enabled": true
},
"leaseInfo": {
"renewalIntervalInSecs": 30,
"durationInSecs": 3600
},
"dataCenterInfo": {
"name": "MyOwn",
"@class": "com.netflix.appinfo.InstanceInfo\$DefaultDataCenterInfo"
}
}
}
EOF

Register web2 as the second instance:

curl "http://127.0.0.1:8761/eureka/apps/WEB" -X POST \
-H "Content-Type: application/json" \
--data-binary @- <<EOF
{
"instance": {
"instanceId": "web2",
"hostName": "$WEB2_IP",
"ipAddr": "$WEB2_IP",
"app": "WEB",
"status": "UP",
"port": {
"\$": 80,
"@enabled": true
},
"leaseInfo": {
"renewalIntervalInSecs": 30,
"durationInSecs": 3600
},
"dataCenterInfo": {
"name": "MyOwn",
"@class": "com.netflix.appinfo.InstanceInfo\$DefaultDataCenterInfo"
}
}
}
EOF

Applications normally use a Eureka client to register and renew their leases. This walkthrough calls the REST API directly so that the discovery behavior can be tested without changing the sample services. Each sample registration requests a one-hour lease to keep the instance available while you complete the walkthrough.

Verify that Eureka returns both instances with an UP status:

curl --retry 5 --retry-delay 1 --retry-all-errors \
-fsS "http://127.0.0.1:8761/eureka/apps/WEB" \
-H "Accept: application/json" | \
jq '[.application.instance[] | {
instanceId,
ipAddr,
port: .port["$"],
status
}]'

Connect Eureka to APISIX​

This walkthrough uses the APISIX Docker Quickstart. Because the Quickstart does not mount a source configuration file, append the Eureka 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:
eureka:
host:
- http://eureka:8761
prefix: /eureka/
fetch_interval: 5
...
EOF

❶ host: Eureka server addresses that APISIX queries. APISIX tries another configured address when a request fails.

❷ prefix: Base path of the Eureka registry API.

❸ fetch_interval: Interval in seconds between complete registry fetches. The default is 30; this walkthrough uses 5 to make status changes visible sooner.

Reload APISIX for the 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 discover the WEB application from Eureka:

Create the route through the Admin API:

curl -i "http://127.0.0.1:9180/apisix/admin/routes/eureka-web-route" -X PUT \
--data-binary @- <<'EOF'
{
"uri": "/eureka/web/*",
"upstream": {
"service_name": "WEB",
"discovery_type": "eureka",
"type": "roundrobin"
}
}
EOF

An HTTP/1.1 201 Created response verifies that the route was created.

Verify Service Discovery​

Verify that the responding APISIX worker discovered both service instances:

curl -fsS "http://127.0.0.1:9090/v1/discovery/eureka/dump" | \
jq --arg web1 "$WEB1_IP" --arg web2 "$WEB2_IP" -e '
([.services.WEB[] | select(.port == 80) | .host] | sort) ==
([$web1, $web2] | sort)
'

The command returns true when the responding worker's node set contains both service addresses. Wait one fetch interval plus a short margin for the other workers to update:

sleep 6

Send several requests to the route:

for _ in $(seq 1 10); do
curl "http://127.0.0.1:9080/eureka/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

Set web1 to OUT_OF_SERVICE in Eureka:

curl "http://127.0.0.1:8761/eureka/apps/WEB/web1/status?value=OUT_OF_SERVICE" \
-X PUT

Poll the APISIX Control API until the responding worker's discovered WEB 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/eureka/dump" | \
jq -r '.services.WEB | map("\(.host):\(.port)") | join(",")')" || nodes=""
[ "$nodes" = "$WEB2_IP:80" ] && break
sleep 1
done

[ "$nodes" = "$WEB2_IP:80" ]

A successful command confirms that the status change reached an APISIX worker. Each APISIX worker maintains its own Eureka discovery cache, so wait one additional fetch interval plus a short margin for the other workers to update:

sleep 6

Send several requests and verify that every response comes from web2:

all_web2=true

for _ in $(seq 1 10); do
response="$(curl -fsS "http://127.0.0.1:9080/eureka/web/")" || {
all_web2=false
break
}

if [ "$response" != "Application 2 is running" ]; then
all_web2=false
break
fi

echo "$response"
done

[ "$all_web2" = true ]

Each response should be:

Application 2 is running

Restore web1 to service after verification:

curl "http://127.0.0.1:8761/eureka/apps/WEB/web1/status?value=UP" \
-X DELETE

Persist the Discovery Configuration​

The in-container edit used in this walkthrough is ephemeral. For a long-running deployment, store the Eureka settings in the deployment's source of truth. Replace the example address with a Eureka endpoint that every APISIX instance can reach.

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

config.yaml
discovery:
eureka:
host:
- http://eureka.example:8761
prefix: /eureka/
fetch_interval: 5

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 Eureka, APISIX integrates with HashiCorp Consul, Nacos, and other service discovery platforms.