# Docker Container Orchestration Design ## Goal Make the `DockerRuntimeOrchestrator` fully functional: apply container configs (memory, CPU, ports, env vars) when starting containers, generate correct Traefik routing labels, support replicas, implement blue/green and rolling deployment strategies, and monitor container health via Docker event stream. ## Scope - Docker single-host only (Swarm and K8s are future `RuntimeOrchestrator` implementations) - Replicas managed by the orchestrator as independent containers - Traefik integration for path-based and subdomain-based routing - Docker event stream for infrastructure-level health monitoring - UI changes for new config fields, replica management, and deployment progress --- ## Network Topology Three network tiers with lazy creation: ``` cameleer-infra — server, postgres, clickhouse (databases isolated) cameleer-traefik — server, traefik, all app containers (ingress + agent SSE) cameleer-env-{slug} — app containers within one environment (inter-app only) ``` - **Server** joins `cameleer-infra` + `cameleer-traefik` - **App containers** join `cameleer-traefik` + `cameleer-env-{envSlug}` - **Traefik** joins `cameleer-traefik` only - **Databases** join `cameleer-infra` only App containers reach the server for SSE/heartbeats via the `cameleer-traefik` network. They never touch databases directly. ### Network isolation The `cameleer-traefik` network has ICC enabled (required for Traefik routing and agent-server communication). All app containers are technically reachable from each other on this network. The security boundary is at the application level (auth tokens, environment-specific credentials). The `cameleer-env-{slug}` networks provide **intentional service discovery isolation** — apps only discover and communicate with services in their own environment via Docker DNS. Cross-environment communication requires knowing the target container's IP, which apps have no reason to discover. > **Future option:** Per-environment Traefik networks (each env gets its own network with Traefik and server attached) would provide full network-level isolation. This can be added based on customer security requirements without changing the orchestrator interface. ### Network Manager Wraps Docker network operations. `ensureNetwork(name)` creates a bridge network if it doesn't exist (idempotent). `connectContainer(containerId, networkName)` attaches a container to a second network. Called by `DeploymentExecutor` before container creation. --- ## Configuration Model ### Three-layer merge ``` Global defaults (application.yml) → Environment defaults (environments.default_container_config) → App overrides (apps.container_config) ``` App overrides environment, environment overrides global. Missing keys fall through. ### Environment-level settings (`defaultContainerConfig`) | Key | Type | Default | Description | |-----|------|---------|-------------| | `memoryLimitMb` | int | 512 | Default memory limit | | `memoryReserveMb` | int | null | Memory reservation | | `cpuShares` | int | 512 | CPU shares | | `cpuLimit` | float | null | CPU core limit | | `routingMode` | string | `"path"` | `path` or `subdomain` | | `routingDomain` | string | from global | Domain for URL generation | | `serverUrl` | string | from global | Server URL for agent callbacks | | `sslOffloading` | boolean | true | Traefik terminates TLS | ### App-level settings (`containerConfig`) | Key | Type | Default | Description | |-----|------|---------|-------------| | `memoryLimitMb` | int | from env | Override memory limit | | `memoryReserveMb` | int | from env | Override memory reservation | | `cpuShares` | int | from env | Override CPU shares | | `cpuLimit` | float | from env | Override CPU core limit | | `appPort` | int | 8080 | Main HTTP port for Traefik | | `exposedPorts` | int[] | [] | Additional ports (debug, JMX) | | `customEnvVars` | map | {} | App-specific environment variables | | `stripPathPrefix` | boolean | true | Traefik strips `/{env}/{app}` prefix | | `sslOffloading` | boolean | from env | Override SSL offloading | | `replicas` | int | 1 | Number of container replicas | | `deploymentStrategy` | string | `"blue-green"` | `blue-green` or `rolling` | ### ConfigMerger Pure function: `resolve(globalDefaults, envConfig, appConfig) → ResolvedContainerConfig` `ResolvedContainerConfig` is a typed Java record with all fields resolved to concrete values. No more scattered `@Value` fields in `DeploymentExecutor` for container-level settings. --- ## Traefik Label Generation ### TraefikLabelBuilder Pure function: takes app slug, env slug, resolved config. Returns `Map`. ### Path-based routing (`routingMode: "path"`) Service name derived as `{envSlug}-{appSlug}`. ``` traefik.enable=true traefik.http.routers.{svc}.rule=PathPrefix(`/{envSlug}/{appSlug}/`) traefik.http.routers.{svc}.entrypoints=websecure traefik.http.services.{svc}.loadbalancer.server.port={appPort} managed-by=cameleer-server cameleer.app={appSlug} cameleer.environment={envSlug} ``` If `stripPathPrefix` is true: ``` traefik.http.middlewares.{svc}-strip.stripprefix.prefixes=/{envSlug}/{appSlug} traefik.http.routers.{svc}.middlewares={svc}-strip ``` ### Subdomain-based routing (`routingMode: "subdomain"`) ``` traefik.http.routers.{svc}.rule=Host(`{appSlug}-{envSlug}.{routingDomain}`) ``` No strip-prefix needed for subdomain routing. ### SSL offloading If `sslOffloading` is true: ``` traefik.http.routers.{svc}.tls=true traefik.http.routers.{svc}.tls.certresolver=default ``` If false, Traefik passes through TLS to the container (requires the app to terminate TLS itself). ### Replicas All replicas of the same app get identical Traefik labels. Traefik automatically load-balances across containers with the same service name on the same network. --- ## Deployment Status Model ### New fields on `deployments` table | Column | Type | Description | |--------|------|-------------| | `target_state` | varchar | `RUNNING` or `STOPPED` | | `deployment_strategy` | varchar | `BLUE_GREEN` or `ROLLING` | | `replica_states` | jsonb | Array of `{index, containerId, status}` | | `deploy_stage` | varchar | Current stage for progress tracking (null when stable) | ### Status values `STOPPED`, `STARTING`, `RUNNING`, `DEGRADED`, `STOPPING`, `FAILED` ### State transitions ``` Target: RUNNING STOPPED → STARTING → RUNNING (all replicas healthy) → DEGRADED (some replicas healthy, some dead) → FAILED (zero healthy / pre-flight failed) RUNNING → DEGRADED (replica dies) DEGRADED → RUNNING (replica recovers via restart policy) DEGRADED → FAILED (all dead, retries exhausted) Target: STOPPED RUNNING/DEGRADED → STOPPING → STOPPED (all replicas stopped and removed) → FAILED (couldn't stop some replicas) ``` ### Aggregate derivation Deployment status is derived from replica states: - All replicas `RUNNING` → deployment `RUNNING` - At least one `RUNNING`, some `DEAD` → deployment `DEGRADED` - Zero `RUNNING` after retries → deployment `FAILED` - All stopped → deployment `STOPPED` --- ## Deployment Flow ### Deploy stages (tracked in `deploy_stage` for progress UI) 1. `PRE_FLIGHT` — Validate config, check JAR exists, verify base image 2. `PULL_IMAGE` — Pull base image if not present locally 3. `CREATE_NETWORK` — Ensure traefik and environment networks exist 4. `START_REPLICAS` — Create and start N containers 5. `HEALTH_CHECK` — Wait for at least one replica to pass health check 6. `SWAP_TRAFFIC` — (blue/green) Stop old deployment replicas 7. `COMPLETE` — Mark deployment RUNNING/DEGRADED ### Pre-flight checks Before touching any running containers: 1. JAR file exists on disk at the path stored in `app_versions` 2. Base image available (pull if missing) 3. Resolved config is valid (memory > 0, appPort > 0, replicas >= 1) 4. No naming conflict with containers from other apps If any check fails → deployment marked `FAILED` immediately, existing deployment untouched. ### Blue/green strategy 1. Start all new replicas alongside the old deployment 2. Wait for health checks on new replicas 3. If healthy: stop and remove all old replicas, mark new deployment RUNNING 4. If unhealthy: remove new replicas, mark new deployment FAILED, old deployment stays Temporarily uses 2x resources during the swap window. ### Rolling strategy 1. For each replica index (0..N-1): a. Stop old replica at index i (if exists) b. Start new replica at index i c. Wait for health check d. If unhealthy: stop, mark deployment FAILED, leave remaining old replicas 2. After all replicas replaced, mark deployment RUNNING Lower peak resources but slower and more complex. ### Container naming `{envSlug}-{appSlug}-{replicaIndex}` (e.g., `staging-payment-gateway-0`) ### Container restart policy `on-failure` with max 3 retries. Docker handles transient failures. After 3 retries exhausted, the container stays dead and `DockerEventMonitor` detects the permanent failure. ### Environment variables injected Base env vars (always set): ``` CAMELEER_EXPORT_TYPE=HTTP CAMELEER_APPLICATION_ID={appSlug} CAMELEER_ENVIRONMENT_ID={envSlug} CAMELEER_DISPLAY_NAME={containerName} CAMELEER_SERVER_URL={resolvedServerUrl} CAMELEER_AUTH_TOKEN={bootstrapToken} ``` Plus all entries from `customEnvVars` in the resolved config. --- ## Docker Event Monitor ### DockerEventMonitor `@Component` that starts a persistent Docker event stream on `@PostConstruct`. - Filters for containers with label `managed-by=cameleer-server` - Listens for events: `die`, `oom`, `stop`, `start` - On `die`/`oom`: looks up deployment by container ID, updates replica status to `DEAD`, recomputes deployment status (RUNNING → DEGRADED → FAILED) - On `start`: updates replica status to `RUNNING` (handles Docker restart policy recoveries) - Reconnects automatically if the stream drops ### Interaction with agent heartbeats - Agent heartbeats: app-level health (is the Camel context running, are routes active) - Docker events: infrastructure-level health (is the container alive, OOM, crash) - Both feed into the same deployment status. Docker events are faster for container crashes. Agent heartbeats catch app-level hangs where the container is alive but the app is stuck. --- ## Database Migration `V7__deployment_orchestration.sql`: ```sql -- New status values and fields for deployments ALTER TABLE deployments ADD COLUMN target_state VARCHAR(20) NOT NULL DEFAULT 'RUNNING'; ALTER TABLE deployments ADD COLUMN deployment_strategy VARCHAR(20) NOT NULL DEFAULT 'BLUE_GREEN'; ALTER TABLE deployments ADD COLUMN replica_states JSONB NOT NULL DEFAULT '[]'; ALTER TABLE deployments ADD COLUMN deploy_stage VARCHAR(30); -- Backfill existing deployments UPDATE deployments SET target_state = CASE WHEN status = 'STOPPED' THEN 'STOPPED' ELSE 'RUNNING' END; ``` The `status` column remains but gains two new values: `DEGRADED` and `STOPPING`. The `DeploymentStatus` enum is updated to match. --- ## UI Changes ### Deployments tab — Overview - **Replicas column** in deployments table: shows `{healthy}/{total}` (e.g., `2/3`) - **Status badge** updated for new states: `DEGRADED` (warning color), `STOPPING` (auto color) - **Deployment progress** shown when `deploy_stage` is not null — horizontal step indicator: ``` ●━━━━●━━━━●━━━━○━━━━○━━━━○ Pre- Pull Start Health Swap flight reps check traffic ``` Completed steps filled, current step highlighted, failed step red. ### Create App page — Resources tab - `appPort` — number input (default 8080) - `replicas` — number input (default 1) - `deploymentStrategy` — select: Blue/Green, Rolling (default Blue/Green) - `stripPathPrefix` — toggle (default true) - `sslOffloading` — toggle (default true) ### Config tab — Resources sub-tab (app detail) Same fields as create page, plus visible in read-only mode when not editing. ### Environment admin page - `routingMode` — select: Path-based, Subdomain (default Path-based) - `routingDomain` — text input - `serverUrl` — text input with placeholder showing global default - `sslOffloading` — toggle (default true) --- ## New/Modified Components Summary ### Core module (cameleer-server-core) - `ResolvedContainerConfig` — new record with all typed fields - `ConfigMerger` — pure function, three-layer merge - `ContainerRequest` — add `cpuLimit`, `exposedPorts`, `restartPolicy`, `additionalNetworks` - `DeploymentStatus` — add `DEGRADED`, `STOPPING` - `Deployment` — add `targetState`, `deploymentStrategy`, `replicaStates`, `deployStage` ### App module (cameleer-server-app) - `DockerRuntimeOrchestrator` — apply full config (memory reserve, CPU limit, exposed ports, restart policy) - `DockerNetworkManager` — new component, lazy network creation + container attachment - `DockerEventMonitor` — new component, persistent event stream listener - `TraefikLabelBuilder` — new utility, generates full Traefik label set - `DeploymentExecutor` — rewrite deploy flow with stages, pre-flight, strategy dispatch - `V7__deployment_orchestration.sql` — migration for new columns ### UI - `AppsTab.tsx` — new fields in create page and config tabs - `EnvironmentsPage.tsx` — routing and SSL fields - `DeploymentProgress` component — step indicator for deploy stages - Status badges updated for DEGRADED/STOPPING