Introduction
Have you ever experienced this?
Your VPS reboots, and suddenly the website is down, all APIs return 502, and the database connection fails—even though every service has restart: always configured. What went wrong?
The problem is startup order. PostgreSQL isn’t ready yet when Django tries to connect. Redis hasn’t loaded when Celery workers start. Nginx proxies to upstreams that haven’t come up, so the frontend gets 502 errors.
Traditional solutions involve writing After= and Requires= directives, or adding depends_on in Docker Compose. But these only express static, pairwise dependencies—they can’t handle complex multi-hop dependency chains, and they can’t perceive runtime state.
This article introduces a new approach: using a local LLM to automatically build a VPS service dependency graph and orchestrate intelligent restarts based on that graph. No manual dependency configuration needed—the AI learns dynamically through probing, and restarts services in topological order.
Why Existing Solutions Fall Short
systemd Limitations
systemd supports After=, Requires=, Wants=, but has several issues:
- Manual configuration required: Each service needs its own unit file, and dependency relationships are scattered
- Only pairwise relationships:
A after BandB after Cdon’t automatically deriveA after C - Can’t perceive runtime dependencies: If Nginx’s proxy upstream IP changes, systemd doesn’t know
- Can’t handle conditional dependencies: Development and production environments have different dependencies
Docker Compose Limitations
Docker Compose’s depends_on is even simpler:
services:
web:
depends_on:
- db
- redis
db:
image: postgres:16
Problems:
depends_ononly waits for container startup, not health check completion (unless you configurecondition: service_healthy)- Can’t express dynamically discovered dependencies (e.g., third-party services injected via environment variables)
- No dependency relationships between multiple Compose projects
Manual Script Limitations
Many ops people write startup scripts:
systemctl start postgresql
sleep 5
systemctl start redis
sleep 3
systemctl start django
...
This is clearly unmaintainable—every new service requires script changes, and getting the order wrong means debugging again.
Core Approach: AI-Driven Dependency Discovery and Orchestration
The entire solution has three phases:
┌─────────────┐ ┌─────────────┐ ┌─────────────┐
│ Discovery │ → │ Graph Build │ → │ Orchestrate │
└─────────────┘ └─────────────┘ └─────────────┘
│ │ │
Scan ports/processes Topological sort Start/stop in order
Probe connections Cycle detection Health verification
Parse configs Visualization Rollback strategy
Phase 1: Dependency Discovery
Without relying on any preset configuration, the AI agent passively learns service dependencies through:
1. Process-Level Probing
import psutil
import socket
def discover_process_dependencies():
"""Scan all process network connections to infer dependencies"""
deps = {}
for proc in psutil.process_iter(['pid', 'name', 'cmdline']):
try:
connections = psutil.net_connections('inet')
proc_conns = [c for c in connections if c.pid == proc.info['pid']]
deps[proc.info['pid']] = {
'name': proc.info['name'],
'listening_ports': [c.laddr.port for c in proc_conns if c.status == 'LISTEN'],
'connected_to': [c.raddr for c in proc_conns if c.status == 'ESTABLISHED']
}
except (psutil.NoSuchProcess, psutil.AccessDenied):
pass
return deps
2. Configuration Parsing
Read common service config files to extract upstream dependencies:
- PostgreSQL: Check
listen_addressesinpostgresql.conf - Nginx: Parse
upstreamblocks andproxy_passdirectives - Docker Compose: Parse
depends_on,links, network configs - Application config: Read database/cache URLs from
.env,settings.py,config.yaml
3. LLM-Assisted Analysis
Feed the probe results to a local LLM (e.g., Ollama + Llama 3) for implicit dependency understanding:
You are a VPS operations expert. Here are the currently running services and their network profiles:
Service: nginx (PID 1234)
Listening ports: 80, 443
Established connections → 127.0.0.1:8000, 127.0.0.1:8001
Service: django-app (PID 5678)
Listening ports: 8000
Established connections → 127.0.0.1:5432, 127.0.0.1:6379
Service: postgresql (PID 9012)
Listening ports: 5432
Established connections: none
Service: redis (PID 3456)
Listening ports: 6379
Established connections: none
Analyze the dependency relationships between these services and output a JSON dependency graph.
The LLM returns:
{
"dependencies": [
{"from": "django-app", "to": "postgresql", "type": "data", "confidence": 0.95},
{"from": "django-app", "to": "redis", "type": "cache", "confidence": 0.90},
{"from": "nginx", "to": "django-app", "type": "proxy", "confidence": 0.98}
],
"startup_order": ["postgresql", "redis", "django-app", "nginx"],
"shutdown_order": ["nginx", "django-app", "redis", "postgresql"]
}
Phase 2: Graph Construction
Convert the LLM output into a persistent dependency graph stored as JSON:
{
"version": "1.0",
"updated_at": "2026-09-18T03:00:00Z",
"services": {
"postgresql": {
"type": "database",
"ports": [5432],
"startup_cost": "slow",
"health_check": "pg_isready -h 127.0.0.1 -p 5432",
"dependencies": []
},
"redis": {
"type": "cache",
"ports": [6379],
"startup_cost": "fast",
"health_check": "redis-cli ping",
"dependencies": []
},
"django-app": {
"type": "application",
"ports": [8000],
"startup_cost": "medium",
"health_check": "curl -sf http://127.0.0.1:8000/healthz",
"dependencies": ["postgresql", "redis"]
},
"nginx": {
"type": "proxy",
"ports": [80, 443],
"startup_cost": "fast",
"health_check": "curl -sf http://127.0.0.1/health",
"dependencies": ["django-app"]
}
}
}
Phase 3: Intelligent Orchestration
Based on the dependency graph, implement safe start/stop orchestration:
#!/usr/bin/env python3
"""
VPS Service Smart Restart Orchestrator
Uses topological sorting based on dependency graphs for correct start/stop order
"""
import json
import subprocess
import time
import sys
from pathlib import Path
from collections import deque
GRAPH_PATH = Path("/etc/vps-ops/service-dependency-graph.json")
LOG_FILE = Path("/var/log/vps-ops/restart.log")
def load_graph():
with open(GRAPH_PATH) as f:
return json.load(f)
def topological_sort(services: dict) -> list:
"""Kahn's algorithm for topological sort, returns startup order"""
in_degree = {name: 0 for name in services}
adj = {name: [] for name in services}
for name, info in services.items():
for dep in info.get("dependencies", []):
if dep in adj:
adj[dep].append(name)
in_degree[name] += 1
queue = deque([n for n, d in in_degree.items() if d == 0])
result = []
while queue:
node = queue.popleft()
result.append(node)
for neighbor in adj[node]:
in_degree[neighbor] -= 1
if in_degree[neighbor] == 0:
queue.append(neighbor)
if len(result) != len(services):
raise ValueError("Cycle detected in dependency graph")
return result
def reverse_order(startup_order: list) -> list:
"""Shutdown order = reverse of startup order"""
return list(reversed(startup_order))
def wait_for_health(service_name: str, info: dict, timeout: int = 60):
"""Wait for service health check to pass"""
check_cmd = info.get("health_check", "")
if not check_cmd:
cost = info.get("startup_cost", "medium")
delays = {"fast": 3, "medium": 10, "slow": 30}
time.sleep(delays.get(cost, 10))
return True
for i in range(timeout):
result = subprocess.run(check_cmd, shell=True, capture_output=True)
if result.returncode == 0:
log(f" ✅ {service_name} health check passed")
return True
time.sleep(1)
log(f" ⚠️ {service_name} health check timed out ({timeout}s)")
return False
def start_service(service_name: str, info: dict):
"""Start a single service"""
log(f"🚀 Starting {service_name} ...")
cmd = info.get("start_command", f"systemctl start {service_name}")
result = subprocess.run(cmd, shell=True, capture_output=True, text=True)
if result.returncode != 0:
log(f" ❌ {service_name} failed to start: {result.stderr}")
return False
if wait_for_health(service_name, info):
log(f" ✅ {service_name} started successfully")
return True
return False
def stop_service(service_name: str, info: dict):
"""Gracefully stop a single service"""
log(f"🛑 Stopping {service_name} ...")
cmd = info.get("stop_command", f"systemctl stop {service_name}")
result = subprocess.run(cmd, shell=True, capture_output=True, text=True)
if result.returncode != 0:
log(f" ⚠️ {service_name} stop error: {result.stderr}")
time.sleep(2) # Wait for connections to gracefully close
def full_restart(force: bool = False):
"""Execute full intelligent restart"""
graph = load_graph()
services = graph["services"]
startup_order = topological_sort(services)
shutdown_order = reverse_order(startup_order)
log(f"📋 Restart plan:")
log(f" Stop order: {' → '.join(shutdown_order)}")
log(f" Start order: {' → '.join(startup_order)}")
if force:
log("⚡ Force mode: skipping health check waits")
# Stop phase
log("\n── Stopping phase ──")
for name in shutdown_order:
stop_service(name, services[name])
# Start phase
log("\n── Starting phase ──")
failed = []
for name in startup_order:
if not start_service(name, services[name]):
failed.append(name)
if failed:
log(f"\n❌ The following services failed to start: {', '.join(failed)}")
sys.exit(1)
else:
log(f"\n✅ All {len(startup_order)} services started successfully")
def log(msg: str):
timestamp = time.strftime("%Y-%m-%d %H:%M:%S")
line = f"[{timestamp}] {msg}"
print(line)
LOG_FILE.parent.mkdir(parents=True, exist_ok=True)
with open(LOG_FILE, "a") as f:
f.write(line + "\n")
if __name__ == "__main__":
force = "--force" in sys.argv
full_restart(force=force)
Hands-On: Deploying a Dependency-Aware VPS from Scratch
Step 1: Install Prerequisites
# Python dependencies
pip install psutil ollama requests
# System tools
apt install postgresql-client redis-tools curl
Step 2: Create the Dependency Discovery Script
#!/usr/bin/env python3
"""
VPS Service Dependency Auto-Discover
Scans currently running services and generates a dependency graph draft
"""
import psutil
import json
import subprocess
from pathlib import Path
def get_listening_services():
"""Get all services listening on ports"""
services = {}
for conn in psutil.net_connections(kind='inet'):
if conn.status == 'LISTEN':
try:
proc = psutil.Process(conn.pid)
name = proc.name()
pid = conn.pid
if pid not in services:
services[pid] = {
'name': name,
'pid': pid,
'ports': set(),
'connections': []
}
services[pid]['ports'].add(conn.laddr.port)
except (psutil.NoSuchProcess, psutil.AccessDenied):
pass
return services
def get_outgoing_connections():
"""Get all established outgoing connections"""
outgoing = {}
for conn in psutil.net_connections(kind='inet'):
if conn.status == 'ESTABLISHED' and conn.pid:
try:
pid = conn.pid
if pid not in outgoing:
outgoing[pid] = []
outgoing[pid].append({
'remote_ip': conn.raddr.ip,
'remote_port': conn.raddr.port
})
except (psutil.NoSuchProcess, psutil.AccessDenied):
pass
return outgoing
def infer_dependencies(listening, outgoing):
"""Infer dependencies from network connections"""
port_to_service = {}
for pid, info in listening.items():
for port in info['ports']:
port_to_service[port] = info['name']
dependencies = {}
for pid, conns in outgoing.items():
svc_name = listening.get(pid, {}).get('name', f'process-{pid}')
deps = []
for conn in conns:
target_svc = port_to_service.get(conn['remote_port'])
if target_svc and target_svc != svc_name:
deps.append({
'target': target_svc,
'type': 'network',
'port': conn['remote_port']
})
if deps:
dependencies[svc_name] = deps
return dependencies
def detect_service_type(name: str) -> str:
"""Infer service type from process name"""
database_keywords = ['postgres', 'mysql', 'mongodb', 'redis', 'mariadb', 'sqlite']
cache_keywords = ['redis', 'memcached', 'varnish']
proxy_keywords = ['nginx', 'caddy', 'traefik', 'haproxy', 'envoy']
queue_keywords = ['celery', 'rabbitmq', 'kafka', 'squarego']
name_lower = name.lower()
if any(k in name_lower for k in database_keywords):
return 'database'
if any(k in name_lower for k in cache_keywords):
return 'cache'
if any(k in name_lower for k in proxy_keywords):
return 'proxy'
if any(k in name_lower for k in queue_keywords):
return 'queue'
return 'application'
def generate_graph():
listening = get_listening_services()
outgoing = get_outgoing_connections()
deps = infer_dependencies(listening, outgoing)
graph = {
"version": "1.0",
"generated_at": Path(__import__('time').time()),
"services": {}
}
all_pids = set(listening.keys()) | set(outgoing.keys())
for pid in all_pids:
info = listening.get(pid, {})
name = info.get('name', f'unknown-{pid}')
svc_type = detect_service_type(name)
graph["services"][name] = {
"type": svc_type,
"ports": sorted(list(info.get('ports', set()))),
"dependencies": [d['target'] for d in deps.get(name, [])],
"startup_cost": "slow" if svc_type in ('database',) else
"medium" if svc_type == 'application' else "fast"
}
# Remove self-references
for name, info in graph["services"].items():
info["dependencies"] = [d for d in info["dependencies"] if d != name]
return graph
if __name__ == "__main__":
graph = generate_graph()
output = Path("/etc/vps-ops/service-dependency-graph.json")
output.parent.mkdir(parents=True, exist_ok=True)
with open(output, 'w') as f:
json.dump(graph, f, indent=2, ensure_ascii=False)
print(f"✅ Dependency graph generated: {output}")
print(json.dumps(graph, indent=2, ensure_ascii=False))
Step 3: LLM Refinement
The auto-discovered graph may not be perfectly accurate. Use LLM to refine:
import ollama
def refine_with_llm(raw_graph: dict) -> dict:
"""Refine the dependency graph using LLM"""
prompt = f"""You are a VPS operations expert. Here is a draft dependency graph discovered through automated probing:
{json.dumps(raw_graph, indent=2)}
Please check and fix:
1. Remove obviously incorrect dependencies (e.g., self-references)
2. Add implicit dependencies (e.g., Django typically needs PostgreSQL + Redis)
3. Add health_check commands for each service
4. Ensure the startup order is reasonable
Output the corrected complete JSON only, no other content."""
response = ollama.chat(model="llama3.2", messages=[{"role": "user", "content": prompt}])
refined = json.loads(response["message"]["content"])
return refined
Step 4: Scheduled Refresh + Manual Trigger
# Add to cron, re-discover every 6 hours
0 */6 * * * /usr/local/bin/vps-dep-discover.py
# Auto-execute smart orchestration after system reboot
# Create /etc/systemd/system/vps-smart-restart.service
Advanced Features
Graded Restart
Not all services need to restart simultaneously. The dependency graph supports grouped restarts:
def grouped_restart(graph: dict, group: str = "critical"):
"""Restart only services in the specified group"""
if group == "critical":
# Only restart databases and caches
targets = [s for s, info in graph["services"].items()
if info["type"] in ("database", "cache")]
elif group == "app":
# Only restart the application layer
targets = [s for s, info in graph["services"].items()
if info["type"] in ("application",)]
else:
targets = list(graph["services"].keys())
# Restart relevant services and their downstream dependencies in order
ordered = topological_sort(graph["services"])
ordered = [s for s in ordered if s in targets or
any(d in targets for d in graph["services"][s].get("dependencies", []))]
return ordered
Dependency Change Detection
After each graph refresh, detect changes and alert:
def detect_dependency_changes(old_graph: dict, new_graph: dict):
changes = []
for svc in new_graph["services"]:
old_deps = set(old_graph["services"].get(svc, {}).get("dependencies", []))
new_deps = set(new_graph["services"][svc].get("dependencies", []))
added = new_deps - old_deps
removed = old_deps - new_deps
if added:
changes.append(f"📈 {svc} added dependencies: {added}")
if removed:
changes.append(f"📉 {svc} removed dependencies: {removed}")
return changes
Emergency Manual Override
When the AI judgment is wrong, allow manual editing of the graph:
# Edit the dependency graph directly
vi /etc/vps-ops/service-dependency-graph.json
# Add comments to explain manual overrides
# MANUAL_OVERRIDES: nginx depends on django-app v2 (2026-09-15 config change)
Results Comparison
| Scenario | Traditional Approach | AI Dependency Orchestration |
|---|---|---|
| Post-reboot recovery time | 3-5 minutes (multiple retries) | 60-90 seconds (single successful attempt) |
| New service dependency config | Manual unit/Compose writing | Auto-discovery + AI refinement |
| Dependency change awareness | Completely unaware | Scheduled refresh + change alerts |
| Failure diagnosis | Check logs one by one | Graph visualization + impact analysis |
| Emergency partial restart | Manual order determination | Topological sort guarantees correctness |
Summary
The AI-driven VPS service dependency graph approach offers three core values:
- Zero-configuration startup: No manual dependency configuration needed—the AI learns automatically
- Dynamic adaptation: The graph updates automatically when services are added, removed, or modified—no manual intervention
- Safety and reliability: Topological sorting guarantees correct start/stop order, preventing cascading failures
- Observability: The dependency graph itself serves as living infrastructure documentation
This solution is particularly well-suited for:
- VPS instances running multiple interdependent services
- Frequently changing service stacks (development/testing environments)
- Teams with high operator turnover (dependency knowledge doesn’t leave with people)
- Any scenario where you need clear visibility into VPS health status
AI isn’t meant to replace an ops engineer’s judgment—it’s meant to free them from tedious manual configuration so they can focus on higher-value work.
