From ea635b5f01a31970271c4c0b3711080367ec8342 Mon Sep 17 00:00:00 2001 From: usmannasir Date: Sat, 29 Nov 2025 04:56:23 +0400 Subject: [PATCH] Fix n8n container health check to use fuzzy name matching The container health check was failing because Docker Compose v1 and v2 use different naming conventions: - v1: project_service_1 (underscores) - v2: project-service-1 (hyphens) Changes: 1. Replaced hardcoded container name formatting with fuzzy matching 2. Added find_container_by_service() helper method for dynamic lookup 3. Updated monitor_deployment() to use dynamic container discovery 4. Container names are now found by normalizing and matching patterns This fixes "Containers failed to reach healthy state" errors during n8n deployment from CyberPanel UI. Ticket References: XKTFREZUR, XCGF2HQUH --- plogical/DockerSites.py | 114 +++++++++++++++++++++++++++------------- 1 file changed, 77 insertions(+), 37 deletions(-) diff --git a/plogical/DockerSites.py b/plogical/DockerSites.py index 6c3603ea4..47d1307a5 100644 --- a/plogical/DockerSites.py +++ b/plogical/DockerSites.py @@ -911,41 +911,59 @@ services: ##### N8N Container - def check_container_health(self, container_name, max_retries=3, delay=80): + def check_container_health(self, service_name, max_retries=3, delay=80): """ Check if a container is running, accepting healthy, unhealthy, and starting states Total wait time will be 4 minutes (3 retries * 80 seconds) + + Uses fuzzy matching to find containers since Docker Compose naming varies by version: + - Docker Compose v1: project_service_1 (underscores) + - Docker Compose v2: project-service-1 (hyphens) """ try: - # Format container name to match Docker's naming convention - formatted_name = f"{self.data['ServiceName']}-{container_name}-1" - logging.writeToFile(f'Checking container health for: {formatted_name}') - + logging.writeToFile(f'Checking container health for service: {service_name}') + for attempt in range(max_retries): client = docker.from_env() - container = client.containers.get(formatted_name) - + + # Find container by searching all containers for a name containing the service name + # This handles both v1 (underscores) and v2 (hyphens) naming conventions + all_containers = client.containers.list(all=True) + container = None + + # Normalize service name for matching (handle both - and _) + service_pattern = service_name.lower().replace(' ', '').replace('-', '').replace('_', '') + + for c in all_containers: + container_pattern = c.name.lower().replace('-', '').replace('_', '') + if service_pattern in container_pattern: + container = c + logging.writeToFile(f'Found matching container: {c.name} for service: {service_name}') + break + + if container is None: + logging.writeToFile(f'No container found matching service: {service_name}, attempt {attempt + 1}/{max_retries}') + time.sleep(delay) + continue + if container.status == 'running': health = container.attrs.get('State', {}).get('Health', {}).get('Status') - + # Accept healthy, unhealthy, and starting states as long as container is running if health in ['healthy', 'unhealthy', 'starting'] or health is None: - logging.writeToFile(f'Container {formatted_name} is running with status: {health}') + logging.writeToFile(f'Container {container.name} is running with health status: {health}') return True else: health_logs = container.attrs.get('State', {}).get('Health', {}).get('Log', []) if health_logs: last_log = health_logs[-1] logging.writeToFile(f'Container health check failed: {last_log.get("Output", "")}') - - logging.writeToFile(f'Container {formatted_name} status: {container.status}, health: {health}, attempt {attempt + 1}/{max_retries}') + + logging.writeToFile(f'Container {container.name} status: {container.status}, health: {health}, attempt {attempt + 1}/{max_retries}') time.sleep(delay) - - return False - - except docker.errors.NotFound: - logging.writeToFile(f'Container {formatted_name} not found') + return False + except Exception as e: logging.writeToFile(f'Error checking container health: {str(e)}') return False @@ -1068,12 +1086,39 @@ services: logging.writeToFile(f"Cleanup failed: {str(e)}") return False + def find_container_by_service(self, service_name): + """ + Find a container by service name using fuzzy matching. + Returns the container object or None if not found. + """ + try: + client = docker.from_env() + all_containers = client.containers.list(all=True) + + # Normalize service name for matching + service_pattern = service_name.lower().replace(' ', '').replace('-', '').replace('_', '') + + for c in all_containers: + container_pattern = c.name.lower().replace('-', '').replace('_', '') + if service_pattern in container_pattern: + return c + return None + except Exception as e: + logging.writeToFile(f'Error finding container: {str(e)}') + return None + def monitor_deployment(self): try: - # Format container names - n8n_container_name = f"{self.data['ServiceName']}-{self.data['ServiceName']}-1" - db_container_name = f"{self.data['ServiceName']}-{self.data['ServiceName']}-db-1" - + # Find containers dynamically using fuzzy matching + n8n_container = self.find_container_by_service(self.data['ServiceName']) + db_container = self.find_container_by_service(f"{self.data['ServiceName']}-db") + + if not n8n_container or not db_container: + raise DockerDeploymentError("Could not find n8n or database containers") + + n8n_container_name = n8n_container.name + db_container_name = db_container.name + logging.writeToFile(f'Monitoring containers: {n8n_container_name} and {db_container_name}') # Check container health @@ -1081,7 +1126,7 @@ services: result, status = ProcessUtilities.outputExecutioner(command, None, None, None, 1) # Only raise error if container is exited - if "exited" in status: + if "exited" in status.lower(): # Get container logs command = f"docker logs {n8n_container_name}" result, logs = ProcessUtilities.outputExecutioner(command, None, None, None, 1) @@ -1096,19 +1141,16 @@ services: # Check if database container is ready command = f"docker exec {db_container_name} pg_isready -U postgres" result, output = ProcessUtilities.outputExecutioner(command, None, None, None, 1) - + if "accepting connections" in output: db_ready = True break - - # Check container status - command = f"docker inspect --format='{{{{.State.Status}}}}' {db_container_name}" - result, db_status = ProcessUtilities.outputExecutioner(command, None, None, None, 1) - - # Only raise error if database container is in a failed state - if db_status == 'exited': - raise DockerDeploymentError(f"Database container is in {db_status} state") - + + # Refresh container status + db_container = self.find_container_by_service(f"{self.data['ServiceName']}-db") + if db_container and db_container.status == 'exited': + raise DockerDeploymentError(f"Database container exited") + retry_count += 1 time.sleep(2) logging.writeToFile(f'Waiting for database to be ready, attempt {retry_count}/{max_retries}') @@ -1117,13 +1159,11 @@ services: raise DockerDeploymentError("Database failed to become ready within timeout period") # Check n8n container status - command = f"docker inspect --format='{{{{.State.Status}}}}' {n8n_container_name}" - result, n8n_status = ProcessUtilities.outputExecutioner(command, None, None, None, 1) - - # Only raise error if n8n container is in a failed state - if n8n_status == 'exited': - raise DockerDeploymentError(f"n8n container is in {n8n_status} state") + n8n_container = self.find_container_by_service(self.data['ServiceName']) + if n8n_container and n8n_container.status == 'exited': + raise DockerDeploymentError(f"n8n container exited") + n8n_status = n8n_container.status if n8n_container else 'unknown' logging.writeToFile(f'Deployment monitoring completed successfully. n8n status: {n8n_status}, database ready: {db_ready}') return True