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
)
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): Service port. 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. - 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 and waits for a 200 response.
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¶
Priority order:
- Direct Parameter:
port=8080in.with_registration() - Environment Variable: Value of
SERVICEKIT_PORT(or custom env var) - Default: 8000
Important: SERVICEKIT_PORT should match the container's internal port, not the host-mapped port.
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).
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: Using host-mapped port instead of container port
# WRONG
ports:
- "8001:8000"
environment:
SERVICEKIT_PORT: "8001" # ❌ Host port
# CORRECT
ports:
- "8001:8000"
environment:
SERVICEKIT_PORT: "8000" # ✅ Container port
Why: Services communicate via Docker's internal network using container ports, not host-mapped ports.
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