Model: Managed by DockerManager.MANAGEABLE_SERVICES (dynamically built from compose registry)
Location: ushadow/backend/src/services/docker_manager.py
- Represents a Docker Compose service that can be started/stopped
- Lives in docker-compose files (e.g.,
compose/openmemory-compose.yaml) - Discovered and registered automatically by
ComposeServiceRegistry - Single instance per service - only one copy can run at a time
- Start/stop controls the actual Docker container directly
- Has port conflict checking with user dialog for port overrides
- Port overrides saved to:
services.{service_name}.ports.{ENV_VAR}
UI: Services Page (ushadow/frontend/src/pages/ServicesPage.tsx)
- Shows cards with Start/Stop buttons
- Port conflict flow: preflight check → dialog → port override → retry
APIs:
POST /api/services/{name}/start- Start servicePOST /api/services/{name}/stop- Stop serviceGET /api/services/{name}/preflight- Check for port conflictsPOST /api/services/{name}/port-override- Set port override
Hook: useServiceStart from ushadow/frontend/src/hooks/useServiceStart.ts
Model: Instance in ushadow/backend/src/models/instance.py
Manager: InstanceManager in ushadow/backend/src/services/instance_manager.py
- Represents a template + configuration + deployment target
- Can have multiple instances of the same template (e.g., openai-1, openai-2)
- Has lifecycle: PENDING → DEPLOYING → RUNNING → STOPPED → ERROR
- Can be deployed to:
- Local Docker (deployment_target=None)
- Remote unode (deployment_target=hostname)
- Cloud provider (deployment_target="cloud", status="n/a")
Deployment Types:
- Local Docker: Uses
ServiceOrchestrator(compose services) or direct Docker - Remote unode: Creates a
Deploymentrecord, usesDeploymentManager - Cloud: No actual deployment, just config storage
Port Handling:
- For LOCAL deployments via orchestrator: Has port conflict checking code in
instance_manager.py:551-563 - For REMOTE deployments: No port conflict checking (just added in
deployment_manager.py:512-547) - Problem: No user dialog, just logs + auto-remap
UI: Instances Page (ushadow/frontend/src/pages/InstancesPage.tsx)
- Shows cards with Start/Stop buttons (similar to services)
- Start button calls
handleDeployInstance() - Stop button calls
handleUndeployInstance() - No port conflict dialog - just fails or auto-remaps silently
APIs:
POST /api/instances- Create instancePOST /api/instances/{id}/deploy- Deploy/start instancePOST /api/instances/{id}/undeploy- Stop instanceDELETE /api/instances/{id}- Delete instance
Model: Deployment in ushadow/backend/src/models/deployment.py
Manager: DeploymentManager in ushadow/backend/src/services/deployment_manager.py
- Represents a service deployed to a specific unode
- Lower-level runtime record tracking container state
- Created when an instance is deployed to a remote unode
- Stores: container_id, container_name, status, access_url, exposed_port
- Has relationship to Instance:
Instance.deployment_id→Deployment.id - Also has
Deployment.instance_id→Instance.id(bidirectional)
Backends:
DockerDeploymentBackend- Deploys to Docker hostsKubernetesDeploymentBackend- Deploys to K8s clusters
Not visible in UI - only used internally for tracking remote deployments
- Services page: Full preflight + dialog + port override flow
- Instances page (local): Port conflict check + auto-remap to settings (no dialog)
- Instances page (remote): Port conflict check + auto-remap to resolved_service.ports (no dialog)
- Different implementations in 3 places!
Services ask user: "Port 8765 is in use, switch to 8766?" Instances: Just auto-remap (or fail silently before my changes)
- Services:
services.{name}.ports.{ENV_VAR}(service-level, shared) - Instances (local): Also
services.{name}.ports.{ENV_VAR}(conflicts with other instances!) - Instances (remote): Only in
resolved_service.ports(temporary, not persisted)
Instances have a config field but ports aren't stored there per-instance
# /api/instances/{id}/preflight
# Returns same format as services preflight
{
"can_start": false,
"port_conflicts": [
{
"port": 8765,
"env_var": "MEM0_PORT",
"used_by": "Docker: mem0-abc123",
"suggested_port": 8766
}
]
}# /api/instances/{id}/port-override
# Sets port in instance.config (per-instance, not service-level)
instance.config.values["MEM0_PORT"] = 8766
save_instances()async def deploy_instance(instance_id):
# 1. Check ports using existing check_port_conflicts()
conflicts = docker_mgr.check_port_conflicts(service_name)
# 2. If conflicts, return 409 with conflict info
# (Let frontend handle it)
# 3. Apply instance.config port overrides to env vars
# before starting containerSimilar to useServiceStart but for instances:
export function useInstanceDeploy(
onSuccess?: (instanceId: string) => void,
onError?: (instanceId: string) => void
) {
// Call preflight check
// Show port conflict dialog if needed
// Call port override API
// Retry deploy
}// Replace handleDeployInstance with:
const instanceDeploy = useInstanceDeploy(...)
onClick={() => instanceDeploy.startInstance(instance.id)}
// Render port conflict dialog
<PortConflictDialog
isOpen={instanceDeploy.portConflictDialog.isOpen}
conflicts={instanceDeploy.portConflictDialog.conflicts}
onResolve={instanceDeploy.resolvePortConflict}
onDismiss={instanceDeploy.dismissPortConflict}
/>check_port_conflicts()indocker_manager.pyPortConflictDialogcomponent (can reuse for instances)
- Port conflict checking logic (3 implementations)
- Preflight check flow (services only)
- Port override storage (inconsistent)
- Preflight check pattern (services + instances)
- Port conflict resolution dialog (same UI)
- Port override API pattern (adapt for instance config)
- Add instance preflight endpoint (
/api/instances/{id}/preflight) - Add instance port override endpoint (
/api/instances/{id}/port-override) - Remove auto-remap logic from deployment_manager.py (let frontend handle)
- Create useInstanceDeploy hook (mirror useServiceStart)
- Add PortConflictDialog to InstancesPage
- Store port overrides in instance.config (not service-level settings)
This unifies the UX while respecting the difference that instances are per-config vs services are singleton.