Service Registration¶
Servicekit provides automatic service registration with an orchestrator for service discovery in Docker Compose and Kubernetes environments.
Quick Start¶
Basic Registration¶
The simplest approach with auto-detected hostname:
from servicekit.api import BaseServiceBuilder, ServiceInfo
app = (
BaseServiceBuilder(info=ServiceInfo(id="my-service", display_name="My Service"))
.with_registration() # Reads SERVICEKIT_ORCHESTRATOR_URL from environment
.build()
)
Set the environment variable:
export SERVICEKIT_ORCHESTRATOR_URL=http://orchestrator:9000/services/$register
fastapi run your_file.py
Docker Compose¶
The recommended approach for multi-service deployments:
services:
orchestrator:
image: your-orchestrator:latest
ports:
- "9000:9000"
my-service:
image: your-service:latest
environment:
SERVICEKIT_ORCHESTRATOR_URL: http://orchestrator:9000/services/$register
# Hostname auto-detected from container name
depends_on:
- orchestrator
Custom ServiceInfo¶
For services with additional metadata:
from servicekit.api import ServiceInfo
class CustomServiceInfo(ServiceInfo):
"""Extended service info with custom fields."""
deployment_env: str = "production"
team: str = "platform"
capabilities: list[str] = []
app = (
BaseServiceBuilder(
info=CustomServiceInfo(
id="my-service",
display_name="My Service",
version="1.0.0",
deployment_env="staging",
team="data-science",
capabilities=["ml-inference", "analytics"],
)
)
.with_registration()
.build()
)
Configuration Options¶
The .with_registration() method accepts these parameters:
.with_registration(
orchestrator_url=None, # Direct value or env var
host=None, # Direct value, auto-detect, or env var
port=None, # Direct value, env var, or 8000
orchestrator_url_env="SERVICEKIT_ORCHESTRATOR_URL",
host_env="SERVICEKIT_HOST",
port_env="SERVICEKIT_PORT",
max_retries=5, # Number of registration attempts
retry_delay=2.0, # Seconds between retries
fail_on_error=False, # True: report unhealthy and SIGTERM on failure
timeout=10.0, # HTTP request timeout
enable_keepalive=True, # Enable periodic ping to keep service alive
keepalive_interval=10.0, # Seconds between keepalive pings
auto_deregister=True, # Automatically deregister on shutdown
service_key=None, # Service key for authentication
service_key_env="SERVICEKIT_REGISTRATION_KEY", # Env var for service key
local_port=None, # Port the app binds locally (readiness probe)
local_port_env="PORT", # Env var for the local bind port
)
ServiceInfo ID Field¶
The id field is required on ServiceInfo and must follow slug format:
- Lowercase letters, numbers, and hyphens only
- Must start with a letter
- No consecutive hyphens
- No trailing or leading hyphens
Valid examples: my-service, chap-ewars, prediction-service, service1
Invalid examples: My-Service (uppercase), my_service (underscore), 1-service (starts with number)
# Valid
ServiceInfo(id="my-service", display_name="My Service")
# Invalid - will raise ValidationError
ServiceInfo(id="My-Service", display_name="My Service") # uppercase
ServiceInfo(id="my_service", display_name="My Service") # underscore
Parameters¶
- orchestrator_url (
str | None): Orchestrator registration endpoint URL. If None, reads from environment variable. - host (
str | None): Service hostname. If None, auto-detects viasocket.gethostname()or reads from environment variable. - port (
int | None): Port advertised to the orchestrator. If None, reads from environment variable or defaults to 8000. - orchestrator_url_env (
str): Environment variable name for orchestrator URL. Default:SERVICEKIT_ORCHESTRATOR_URL. - host_env (
str): Environment variable name for hostname override. Default:SERVICEKIT_HOST. - port_env (
str): Environment variable name for port override. Default:SERVICEKIT_PORT. - max_retries (
int): Maximum number of registration attempts. Default: 5. - retry_delay (
float): Delay in seconds between retry attempts. Default: 2.0. - fail_on_error (
bool): If True, a registration failure (including a readiness timeout) marks the service unhealthy and raisesSIGTERM, so the process shuts down gracefully through the normal lifespan cleanup. If False, log a warning and keep serving. Default: False. - timeout (
float): HTTP request timeout in seconds. Default: 10.0. - enable_keepalive (
bool): Enable periodic pings to keep service registered. Default: True. - keepalive_interval (
float): Seconds between keepalive pings. Default: 10.0. - auto_deregister (
bool): Automatically deregister service on shutdown. Default: True. - service_key (
str | None): Service key for authentication. If provided, sent asX-Service-Keyheader. Default: None. - local_port (
int | None): Port the app binds locally, probed by the readiness gate. Only needed when it differs from the advertised port and is neither inPORTnor 8000. Default: None. - local_port_env (
str): Environment variable name for the local bind port. Default:PORT(exported byrun_app). - service_key_env (
str): Environment variable name for service key. Default:SERVICEKIT_REGISTRATION_KEY.
How It Works¶
Registration Flow¶
- Service Starts: FastAPI application initializes during lifespan startup
- Hostname Resolution: Determines service hostname (see resolution order below)
- Port Resolution: Determines service port (see resolution order below)
- URL Construction: Builds service URL as
http://<hostname>:<port> - Payload Creation: Serializes ServiceInfo to JSON (supports custom subclasses)
- Registration Request: Sends POST to orchestrator endpoint
- Retry on Failure: Retries with delay if request fails
- Keepalive Started: If enabled, background task starts pinging orchestrator
- Service Runs: Service handles requests while staying alive via pings
- Shutdown: On graceful shutdown, stops keepalive and optionally deregisters
- Logging: Logs all registration, ping, and deregistration events
Readiness gate: before registering, the service polls its own health endpoint on 127.0.0.1 and waits for a 200
response. It probes the local bind port, which may differ from the advertised port (see Port Resolution),
and only accepts responses from its own app instance, so another server on a probed port is never mistaken for it. Because /health now returns 503 while any health check is unhealthy, a service whose database (or other check)
is down waits instead of registering as available. See the health checks guide for the status
code semantics.
Keepalive and TTL¶
Services can be configured to send periodic "ping" requests to the orchestrator to indicate they're still alive. The orchestrator tracks a Time-To-Live (TTL) for each service and automatically removes services that haven't pinged within the TTL window.
How it works:
- Initial Registration: Service registers and receives response with:
id: Unique ULID identifier for this servicettl_seconds: How long until service expires (default: 30 seconds)-
ping_url: Endpoint to send keepalive pings (automatically provided by orchestrator) -
Keepalive Loop: Background task automatically sends PUT requests to
ping_urlevery N seconds: - Default interval: 10 seconds (configurable via
keepalive_interval) - Each ping resets the service's expiration time
-
Failures are logged but don't crash the service
-
TTL Expiration: Orchestrator runs cleanup every 5 seconds:
- Removes services that haven't pinged within TTL window
-
Logs expired services for monitoring
-
Graceful Shutdown: On service shutdown:
- Keepalive task stops (no more pings)
- Service explicitly deregisters (if
auto_deregister=True) - Immediate removal from registry
Configuration examples:
# Default: keepalive enabled, auto-deregister on shutdown
.with_registration()
# Disable keepalive (rely on manual health checks)
.with_registration(enable_keepalive=False)
# Custom keepalive interval (faster pings)
.with_registration(keepalive_interval=5.0)
# Don't deregister on shutdown (let TTL expire naturally)
.with_registration(auto_deregister=False)
Registration Payload¶
The service sends this payload to the orchestrator:
{
"id": "my-service",
"url": "http://my-service:8000",
"info": {
"id": "my-service",
"display_name": "My Service",
"version": "1.0.0",
...
}
}
For custom ServiceInfo subclasses:
{
"id": "ml-service",
"url": "http://ml-service:8000",
"info": {
"id": "ml-service",
"display_name": "ML Service",
"version": "2.0.0",
"deployment_env": "production",
"team": "data-science",
"capabilities": ["ml-inference", "feature-extraction"],
"priority": 5
}
}
Registration Response¶
The orchestrator responds with registration details, including the ping endpoint:
{
"id": "my-service",
"status": "registered",
"service_url": "http://my-service:8000",
"message": "Service registered successfully",
"ttl_seconds": 30,
"ping_url": "http://orchestrator:9000/services/my-service/$ping"
}
Key fields:
- id: Service identifier (matches the id field from ServiceInfo)
- ttl_seconds: Time-to-live in seconds (service must ping within this window)
- ping_url: Endpoint for keepalive pings (automatically used by the service)
Important: The service ID is defined by the service itself via ServiceInfo.id, not assigned by the orchestrator. This makes registration idempotent - re-registering the same service updates the existing entry rather than creating a new one.
Hostname Resolution¶
Priority order:
- Direct Parameter:
host="my-service"in.with_registration() - Auto-Detection:
socket.gethostname()(returns Docker container name or hostname) - Environment Variable: Value of
SERVICEKIT_HOST(or custom env var) - Error: Triggers a graceful shutdown if
fail_on_error=True, otherwise logs a warning
Docker Behavior: In Docker Compose, socket.gethostname() returns the service name or container ID, making auto-detection work seamlessly.
Port Resolution¶
Two ports are involved, and they differ whenever a container port is published under another host port or the service sits behind a proxy.
Advertised port (the port in the URL sent to the orchestrator), in priority order:
- Direct Parameter:
port=8080in.with_registration() - Environment Variable: Value of
SERVICEKIT_PORT(or custom env var) - Default: 8000
Set it to the port the orchestrator uses to reach the service: the container port when both share a Docker network,
the published host port when the orchestrator runs on the host (e.g. host.docker.internal:18701 for -p 18701:8000).
Local port (the port the readiness gate probes on 127.0.0.1):
- Direct Parameter:
local_port=8000in.with_registration(), used exclusively when set - Otherwise each of these is tried in turn until the app answers:
PORT(orlocal_port_env), the advertised port, 8000
run_app exports the port uvicorn binds as PORT, so services started through it are always probed on the right port.
Services started with their own uvicorn --port N command are found as long as N is the advertised port or 8000;
otherwise set PORT=N or local_port=N.
Examples¶
Environment Variables (Production)¶
from servicekit.api import BaseServiceBuilder, ServiceInfo
app = (
BaseServiceBuilder(info=ServiceInfo(id="production-service", display_name="Production Service"))
.with_logging()
.with_health()
.with_registration() # Reads from environment
.build()
)
docker-compose.yml:
services:
my-service:
image: my-service:latest
environment:
SERVICEKIT_ORCHESTRATOR_URL: http://orchestrator:9000/services/$register
# SERVICEKIT_HOST auto-detected
# SERVICEKIT_PORT defaults to 8000
Direct Configuration (Testing)¶
app = (
BaseServiceBuilder(info=ServiceInfo(id="test-service", display_name="Test Service"))
.with_registration(
orchestrator_url="http://localhost:9000/services/$register",
host="test-service",
port=8080,
)
.build()
)
Custom Environment Variable Names¶
app = (
BaseServiceBuilder(info=ServiceInfo(id="my-service", display_name="My Service"))
.with_registration(
orchestrator_url_env="MY_APP_ORCHESTRATOR_URL",
host_env="MY_APP_HOST",
port_env="MY_APP_PORT",
)
.build()
)
Environment:
export MY_APP_ORCHESTRATOR_URL=http://orchestrator:9000/services/$register
export MY_APP_HOST=my-service
export MY_APP_PORT=8000
Fail-Fast Mode¶
For critical services that must register:
app = (
BaseServiceBuilder(info=ServiceInfo(id="critical-service", display_name="Critical Service"))
.with_registration(
fail_on_error=True, # Shut down gracefully if registration fails
max_retries=10,
retry_delay=1.0,
)
.build()
)
Custom Retry Strategy¶
app = (
BaseServiceBuilder(info=ServiceInfo(id="my-service", display_name="My Service"))
.with_registration(
max_retries=10, # More attempts
retry_delay=1.0, # Faster retries
timeout=5.0, # Shorter timeout
)
.build()
)
Docker Compose¶
Basic Setup¶
services:
orchestrator:
image: orchestrator:latest
ports:
- "9000:9000"
service-a:
image: my-service:latest
environment:
SERVICEKIT_ORCHESTRATOR_URL: http://orchestrator:9000/services/$register
depends_on:
- orchestrator
With Health Checks¶
Wait for orchestrator to be healthy before starting services:
services:
orchestrator:
image: orchestrator:latest
ports:
- "9000:9000"
healthcheck:
test: ["CMD", "curl", "-f", "http://localhost:9000/health"]
interval: 10s
timeout: 5s
retries: 3
service-a:
image: my-service:latest
environment:
SERVICEKIT_ORCHESTRATOR_URL: http://orchestrator:9000/services/$register
depends_on:
orchestrator:
condition: service_healthy # Wait for healthy status
Custom Port Mapping¶
When container port differs from host port:
services:
service-a:
image: my-service:latest
ports:
- "8001:8000" # Host:Container
environment:
SERVICEKIT_ORCHESTRATOR_URL: http://orchestrator:9000/services/$register
SERVICEKIT_PORT: "8000" # Use container port, not host port
Why: Other services connect using the internal Docker network, so they use the container port (8000), not the host-mapped port (8001).
When the orchestrator runs on the Docker host instead, advertise the published host port:
docker run -p 18701:8000 \
-e SERVICEKIT_ORCHESTRATOR_URL='http://host.docker.internal:8000/v2/services/$register' \
-e SERVICEKIT_HOST=host.docker.internal \
-e SERVICEKIT_PORT=18701 \
--add-host host.docker.internal:host-gateway \
my-service:latest
The readiness gate still probes the app on its container port (8000) before registering host.docker.internal:18701.
Multiple Services¶
services:
orchestrator:
image: orchestrator:latest
ports:
- "9000:9000"
service-a:
image: my-service:latest
ports:
- "8000:8000"
environment:
SERVICEKIT_ORCHESTRATOR_URL: http://orchestrator:9000/services/$register
service-b:
image: my-service:latest
ports:
- "8001:8000"
environment:
SERVICEKIT_ORCHESTRATOR_URL: http://orchestrator:9000/services/$register
Both services register with different hostnames (service-a, service-b) but same internal port (8000).
Kubernetes¶
ConfigMap for Orchestrator URL¶
apiVersion: v1
kind: ConfigMap
metadata:
name: registration-config
data:
orchestrator-url: "http://orchestrator-service:9000/services/$register"
Deployment with Registration¶
apiVersion: apps/v1
kind: Deployment
metadata:
name: my-service
spec:
replicas: 3
template:
metadata:
labels:
app: my-service
spec:
containers:
- name: my-service
image: my-service:latest
env:
- name: SERVICEKIT_ORCHESTRATOR_URL
valueFrom:
configMapKeyRef:
name: registration-config
key: orchestrator-url
- name: SERVICEKIT_HOST
valueFrom:
fieldRef:
fieldPath: metadata.name # Pod name
Service Discovery¶
apiVersion: v1
kind: Service
metadata:
name: my-service
spec:
selector:
app: my-service
ports:
- port: 8000
targetPort: 8000
Error Handling¶
Retry Behavior¶
By default, registration retries 5 times with 2-second delays:
Example timeline: - Attempt 1: Immediate - Attempt 2: After 2 seconds - Attempt 3: After 4 seconds (2s + 2s) - Attempt 4: After 6 seconds - Attempt 5: After 8 seconds
Fail on Error¶
Registration is deferred: it runs in a background task once the app is serving, so startup itself never blocks on the orchestrator and never raises.
Default (fail_on_error=False): Service keeps serving even if registration fails
Fail-Fast (fail_on_error=True): The process must not keep serving unregistered
With fail_on_error=True, a registration failure or a readiness timeout:
- Sets
app.state.registration_failed = True, so the built-inregistrationhealth check reportsunhealthyand/healthdegrades accordingly. - Logs
registration.fatalat critical level. - Raises
SIGTERMin the process, so the server performs its normal graceful shutdown: keepalive stops, jobs drain, shutdown hooks run, the database is disposed.
No exception is raised out of startup, and no cleanup step is skipped.
When to use fail-fast: - Critical services that require orchestrator awareness - Production environments with strict registration requirements - Services that cannot function without being registered
Structured Logging¶
All registration events are logged with structured data:
{
"event": "registration.starting",
"orchestrator_url": "http://orchestrator:9000/services/$register",
"service_url": "http://my-service:8000",
"host_source": "auto-detected",
"port_source": "default",
"max_retries": 5
}
Success:
{
"event": "registration.success",
"service_url": "http://my-service:8000",
"attempt": 1,
"status_code": 200
}
Failure:
{
"event": "registration.attempt_failed",
"service_url": "http://my-service:8000",
"attempt": 1,
"max_retries": 5,
"error": "Connection refused",
"error_type": "ConnectError"
}
Troubleshooting¶
Registration Fails¶
Check orchestrator is reachable:
Check environment variables:
View registration logs:
Hostname Auto-Detection Issues¶
Problem: Auto-detection fails or returns unexpected value
Solution: Override with environment variable
Debug hostname detection:
docker compose exec my-service hostname
docker compose exec my-service python -c "import socket; print(socket.gethostname())"
Port Mismatch¶
Problem: Orchestrator cannot reach service at registered URL
Common mistake: Advertising a port the orchestrator cannot use. SERVICEKIT_PORT must be the port as seen from
the orchestrator.
# Orchestrator on the same Docker network: advertise the container port
ports:
- "8001:8000"
environment:
SERVICEKIT_PORT: "8000"
# Orchestrator on the Docker host: advertise the published host port
ports:
- "8001:8000"
environment:
SERVICEKIT_HOST: host.docker.internal
SERVICEKIT_PORT: "8001"
Readiness timeout (registration.readiness_timeout listing the probed ports): the app answers on none of them.
Set PORT or local_port to the port the app binds.
Orchestrator URL Missing¶
Problem: No orchestrator URL configured
Error: registration.missing_orchestrator_url
Solution: Set environment variable
Or use direct configuration:
Service Not Appearing in Registry¶
Check orchestrator logs:
Check service startup logs:
Verify orchestrator endpoint:
Production Considerations¶
High Availability¶
Use multiple orchestrator replicas:
services:
orchestrator:
image: orchestrator:latest
deploy:
replicas: 3
service-a:
environment:
SERVICEKIT_ORCHESTRATOR_URL: http://orchestrator:9000/services/$register
Retry Strategy¶
Adjust retries for production reliability:
.with_registration(
max_retries=10, # More attempts
retry_delay=1.0, # Faster retries
timeout=30.0, # Longer timeout
fail_on_error=True, # Shut down gracefully in production
)
Security¶
Service Key Authentication: Configure a service key to authenticate with the orchestrator. The key is sent as an X-Service-Key header with all registration requests (register, keepalive, deregister).
# Using environment variable (recommended)
.with_registration() # Reads from SERVICEKIT_REGISTRATION_KEY
# Using direct parameter (testing)
.with_registration(service_key="my-secret-key")
# Using custom environment variable
.with_registration(service_key_env="MY_APP_REGISTRATION_KEY")
Docker Compose with service key:
services:
my-service:
image: my-service:latest
environment:
SERVICEKIT_ORCHESTRATOR_URL: http://orchestrator:9000/services/$register
SERVICEKIT_REGISTRATION_KEY: ${REGISTRATION_SECRET}
TLS: Use HTTPS for orchestrator communication
Monitoring¶
Monitor registration health: - Track registration success/failure rates - Alert on repeated registration failures - Monitor orchestrator availability - Log all registration attempts for audit
Related Examples¶
examples/registration/- Complete registration demo with orchestratorcore_api/- Basic CRUD servicemonitoring/- Prometheus metricsauth/- Environment-based authentication
See Also¶
- Health Checks - Configure health endpoints
- Monitoring - Prometheus metrics
- Authentication - API key authentication