hamidou-ie/symfony-eureka

Symfony bundle to register/heartbeat services into Netflix Eureka

Installs: 11

Dependents: 0

Suggesters: 0

Security: 0

Stars: 1

Watchers: 0

Forks: 0

Open Issues: 0

Type:symfony-bundle

pkg:composer/hamidou-ie/symfony-eureka

0.1.1 2026-02-24 11:04 UTC

This package is auto-updated.

Last update: 2026-02-24 11:14:56 UTC


README

Eureka (Spring Cloud Netflix) client packaged as a Symfony Bundle.

This bundle lets you:

  • register your service instance in Eureka (register)
  • send a one-shot heartbeat (heartbeat)
  • deregister your instance (deregister)
  • do service discovery (fetchInstance, fetchInstances)
  • customize how an instance is picked via a discovery strategy (default: random)
  • use a fallback local registry/cache via an InstanceProvider when Eureka is unavailable

Note: this package provides a Eureka client.

Requirements

  • PHP >= 8.2
  • Symfony ^6.4 || ^7.0 || ^8.0
  • A running Eureka server (e.g. Spring Cloud Netflix Eureka)

HTTP transport is provided by symfony/http-client.

Installation

composer require hamidou-ie/symfony-eureka

Enable the bundle

If Symfony Flex does not enable it automatically, add it to config/bundles.php:

<?php

return [
    // ...
    HamidouIe\SymfonyEureka\EurekaBundle::class => ['all' => true],
];

Configuration

You can configure the bundle:

  1. via environment variables (.env, OS env, secrets)
  2. via config/packages/eureka.yaml (which can override env)

Minimal .env example

EUREKA_ENABLED=1
EUREKA_SERVER_URL=http://localhost:8761/eureka
EUREKA_APP_NAME=my-service

EUREKA_INSTANCE_HOSTNAME=localhost
EUREKA_INSTANCE_IP=127.0.0.1
EUREKA_INSTANCE_PORT=8000
EUREKA_PREFER_IP=0

EUREKA_LEASE_RENEWAL_INTERVAL=30
EUREKA_LEASE_EXPIRATION=90

Full .env example

# toggle
EUREKA_ENABLED=1

# Eureka base URL
EUREKA_SERVER_URL=http://localhost:8761/eureka

# app name (will be uppercased in the payload)
EUREKA_APP_NAME=my-service

# instance identity
EUREKA_INSTANCE_HOSTNAME=my-service.local
EUREKA_INSTANCE_IP=127.0.0.1
EUREKA_INSTANCE_PORT=8000
EUREKA_PREFER_IP=0

# optional: explicit instance id; otherwise it is computed
EUREKA_INSTANCE_ID=
EUREKA_INSTANCE_SUFFIX=

# status
EUREKA_STATUS=UP
EUREKA_OVERRIDDEN_STATUS=UNKNOWN

# public URLs (optional)
# If not set, defaults are computed as:
# - homePageUrl:   http://{host}:{port}/
# - statusPageUrl: http://{host}:{port}/info
# - healthCheckUrl: http://{host}:{port}/health
EUREKA_HOME_PAGE_URL=
EUREKA_STATUS_PAGE_URL=
EUREKA_HEALTH_CHECK_URL=

# VIP (optional) - if empty, defaults to strtolower(appName)
EUREKA_VIP_ADDRESS=
EUREKA_SECURE_VIP_ADDRESS=

# secure port (optional)
EUREKA_SECURE_PORT=443
EUREKA_SECURE_PORT_ENABLED=0

# country (optional)
EUREKA_COUNTRY_ID=1

# lease/heartbeat
EUREKA_LEASE_RENEWAL_INTERVAL=30
EUREKA_LEASE_EXPIRATION=90

# Basic auth (if your Eureka is protected)
EUREKA_BASIC_USER=
EUREKA_BASIC_PASSWORD=

Running multiple instances of the same app

Eureka identifies instances by app + instanceId. If you run multiple instances, make sure each instance has a unique instanceId.

This bundle computes instanceId like this when EUREKA_INSTANCE_ID is empty:

{hostname-or-ip}:{appName}:{suffix}

  • suffix defaults to EUREKA_INSTANCE_PORT
  • hostname-or-ip depends on EUREKA_PREFER_IP

So, running multiple instances is already fine if each instance uses a different port.

If you run multiple instances on the same host and same port (rare, but can happen behind proxies), set one of:

  • EUREKA_INSTANCE_SUFFIX (example: blue, canary, node-1)
  • or EUREKA_INSTANCE_ID (explicit unique value)

Example config/packages/eureka.yaml

eureka:
  enabled: '%env(bool:EUREKA_ENABLED)%'
  server_url: '%env(EUREKA_SERVER_URL)%'
  app_name: '%env(EUREKA_APP_NAME)%'

  instance:
    hostname: '%env(EUREKA_INSTANCE_HOSTNAME)%'
    ip: '%env(EUREKA_INSTANCE_IP)%'
    port: '%env(int:EUREKA_INSTANCE_PORT)%'
    prefer_ip: '%env(bool:EUREKA_PREFER_IP)%'
    id: '%env(default::EUREKA_INSTANCE_ID)%'
    suffix: '%env(default::EUREKA_INSTANCE_SUFFIX)%'

    status: '%env(EUREKA_STATUS)%'
    overridden_status: '%env(EUREKA_OVERRIDDEN_STATUS)%'
    home_page_url: '%env(default::EUREKA_HOME_PAGE_URL)%'
    status_page_url: '%env(default::EUREKA_STATUS_PAGE_URL)%'
    health_check_url: '%env(default::EUREKA_HEALTH_CHECK_URL)%'
    vip_address: '%env(default::EUREKA_VIP_ADDRESS)%'
    secure_vip_address: '%env(default::EUREKA_SECURE_VIP_ADDRESS)%'

    secure_port:
      port: '%env(int:EUREKA_SECURE_PORT)%'
      enabled: '%env(bool:EUREKA_SECURE_PORT_ENABLED)%'

    country_id: '%env(int:EUREKA_COUNTRY_ID)%'

  lease:
    renewal_interval: '%env(int:EUREKA_LEASE_RENEWAL_INTERVAL)%'
    expiration: '%env(int:EUREKA_LEASE_EXPIRATION)%'

  basic_auth:
    user: '%env(default::EUREKA_BASIC_USER)%'
    password: '%env(default::EUREKA_BASIC_PASSWORD)%'

Symfony Console commands

The bundle provides 3 commands:

  • app:eureka:register — register this instance
  • app:eureka:heartbeat — send a one-shot heartbeat
  • app:eureka:deregister — deregister this instance

Examples:

php bin/console app:eureka:register
php bin/console app:eureka:heartbeat
php bin/console app:eureka:deregister

If EUREKA_ENABLED=0, these commands will skip the HTTP calls.

Using it in code

Inject HamidouIe\SymfonyEureka\Service\EurekaClient and call its methods.

use HamidouIe\SymfonyEureka\Service\EurekaClient;

final class MyService
{
    public function __construct(private EurekaClient $eureka) {}

    public function boot(): void
    {
        $this->eureka->register();
        // then schedule periodic heartbeats (see “Keep the instance UP”)
    }
}

Check whether the instance is registered

if (!$eureka->isRegistered()) {
    $eureka->register();
}

Service discovery

Fetch all instances for a service

$instances = $eureka->fetchInstances('the-service');

This method:

  • calls GET {EUREKA_SERVER_URL}/apps/{APPNAME}
  • parses the JSON payload
  • returns a list of HamidouIe\SymfonyEureka\Dto\EurekaInstance
  • filters out instances whose status is not UP

Pick a single instance (load-balancing)

$instance = $eureka->fetchInstance('the-service');

if ($instance !== null) {
    $baseUrl = $instance->homePageUrl;
}

Discovery strategy (instance selection)

By default, the bundle uses a random strategy: RandomStrategy.

To customize instance selection, implement:

HamidouIe\SymfonyEureka\Discovery\DiscoveryStrategyInterface

Example (simple round-robin, adapt to your needs):

namespace App\Eureka;

use HamidouIe\SymfonyEureka\Discovery\DiscoveryStrategyInterface;
use HamidouIe\SymfonyEureka\Dto\EurekaInstance;

final class RoundRobinStrategy implements DiscoveryStrategyInterface
{
    private int $idx = 0;

    public function pickInstance(array $instances): ?EurekaInstance
    {
        if ($instances === []) {
            return null;
        }

        $i = $this->idx % count($instances);
        $this->idx++;

        return $instances[$i];
    }
}

Then alias the interface in config/services.yaml:

services:
  App\Eureka\RoundRobinStrategy: ~

    HamidouIe\SymfonyEureka\Discovery\DiscoveryStrategyInterface:
    alias: App\Eureka\RoundRobinStrategy

Local registry / caching (fallback when Eureka is down)

When Eureka is unreachable or returns an unexpected payload, the bundle can ask a local provider for instances.

Implement:

HamidouIe\SymfonyEureka\Discovery\InstanceProviderInterface

Example (pseudo-cache):

namespace App\Eureka;

use HamidouIe\SymfonyEureka\Discovery\InstanceProviderInterface;
use HamidouIe\SymfonyEureka\Dto\EurekaInstance;

final class RedisInstanceProvider implements InstanceProviderInterface
{
    public function getInstances(string $appName): array
    {
        // load cached instances from Redis / DB / file…
        return [
            new EurekaInstance(
                instanceId: null,
                hostName: 'fallback.local',
                ipAddr: '127.0.0.1',
                port: 8080,
                portEnabled: true,
                homePageUrl: 'http://fallback.local:8080/',
                statusPageUrl: null,
                healthCheckUrl: null,
                status: 'UP',
            ),
        ];
    }
}

Register it (the interface will be injected if it exists):

services:
  App\Eureka\RedisInstanceProvider: ~

    HamidouIe\SymfonyEureka\Discovery\InstanceProviderInterface:
    alias: App\Eureka\RedisInstanceProvider

Keep the instance “UP” (periodic heartbeats)

The bundle provides a one-shot heartbeat() and the app:eureka:heartbeat command, but it does not (yet) provide a long-running start() loop.

Practical options:

Option A — Cron (simple)

  • call php bin/console app:eureka:heartbeat every EUREKA_LEASE_RENEWAL_INTERVAL seconds

Option B — Process manager (prod)

  • supervisor/systemd/k8s: run a worker/command that heartbeats in a loop

Option C — Dev quick loop (PowerShell)

while ($true) { php bin/console app:eureka:heartbeat --env=dev; Start-Sleep -Seconds 30 }

Troubleshooting

My service does not appear in Eureka

  • Check EUREKA_ENABLED=1
  • Check EUREKA_SERVER_URL (ensure the trailing /eureka is correct for your server)
  • Ensure EUREKA_INSTANCE_HOSTNAME / EUREKA_INSTANCE_IP / EUREKA_INSTANCE_PORT are reachable from the Eureka server

Heartbeat returns 404

This usually means the instance is not registered (or the instanceId does not match).

  • run app:eureka:register
  • verify your instance id settings (EUREKA_INSTANCE_ID / EUREKA_INSTANCE_SUFFIX)

Eureka is protected (Basic Auth)

  • set EUREKA_BASIC_USER / EUREKA_BASIC_PASSWORD

License

MIT