mirror of
https://github.com/kossakovsky/n8n-install.git
synced 2026-03-07 22:33:11 +00:00
- Updated the stop_existing_containers function to conditionally include the Supabase compose file only if Supabase is enabled, improving clarity and functionality. - Added logging to inform users about the presence or absence of the Supabase compose file, enhancing the shutdown process. - Included checks for the existence of the Supabase compose file to provide context when Supabase is disabled, ensuring better user feedback.
335 lines
15 KiB
Python
Executable File
335 lines
15 KiB
Python
Executable File
#!/usr/bin/env python3
|
|
"""
|
|
start_services.py
|
|
|
|
This script starts the Supabase stack first, waits for it to initialize, and then starts
|
|
the local AI stack. Both stacks use the same Docker Compose project name ("localai")
|
|
so they appear together in Docker Desktop.
|
|
"""
|
|
|
|
import os
|
|
import subprocess
|
|
import shutil
|
|
import time
|
|
import argparse
|
|
import platform
|
|
import sys
|
|
from dotenv import dotenv_values, load_dotenv
|
|
|
|
def is_supabase_enabled():
|
|
"""Check if 'supabase' is in COMPOSE_PROFILES in the environment."""
|
|
# Relies on load_dotenv() being called in main()
|
|
compose_profiles = os.environ.get("COMPOSE_PROFILES", "")
|
|
return "supabase" in compose_profiles.split(',')
|
|
|
|
def run_command(cmd, cwd=None, env=None):
|
|
"""Run a shell command and print it."""
|
|
print("Running:", " ".join(cmd))
|
|
subprocess.run(cmd, cwd=cwd, check=True, env=env)
|
|
|
|
def clone_supabase_repo():
|
|
"""Clone the Supabase repository using sparse checkout if not already present."""
|
|
if not is_supabase_enabled():
|
|
print("Supabase is not enabled, skipping clone.")
|
|
return
|
|
if not os.path.exists("supabase"):
|
|
print("Cloning the Supabase repository...")
|
|
run_command([
|
|
"git", "clone", "--filter=blob:none", "--no-checkout",
|
|
"https://github.com/supabase/supabase.git"
|
|
])
|
|
os.chdir("supabase")
|
|
run_command(["git", "sparse-checkout", "init", "--cone"])
|
|
run_command(["git", "sparse-checkout", "set", "docker"])
|
|
run_command(["git", "checkout", "master"])
|
|
os.chdir("..")
|
|
else:
|
|
print("Supabase repository already exists, updating...")
|
|
os.chdir("supabase")
|
|
run_command(["git", "pull"])
|
|
os.chdir("..")
|
|
|
|
def prepare_supabase_env():
|
|
"""Copy .env to .env in supabase/docker."""
|
|
if not is_supabase_enabled():
|
|
print("Supabase is not enabled, skipping env preparation.")
|
|
return
|
|
env_path = os.path.join("supabase", "docker", ".env")
|
|
env_example_path = os.path.join(".env")
|
|
print("Copying .env in root to .env in supabase/docker...")
|
|
shutil.copyfile(env_example_path, env_path)
|
|
|
|
def stop_existing_containers():
|
|
"""Stop and remove all existing containers for our unified project ('localai')."""
|
|
print("Stopping and removing all existing containers for the unified project 'localai'...")
|
|
|
|
compose_files_args = ["-f", "docker-compose.yml"]
|
|
|
|
# Only include Supabase compose file if Supabase is enabled
|
|
if is_supabase_enabled():
|
|
supabase_docker_dir = os.path.join("supabase", "docker")
|
|
supabase_compose_file = os.path.join(supabase_docker_dir, "docker-compose.yml")
|
|
if os.path.exists(supabase_compose_file):
|
|
print(f"Supabase is enabled and compose file found at {supabase_compose_file}, adding to commands.")
|
|
compose_files_args.extend(["-f", supabase_compose_file])
|
|
else:
|
|
# This case should ideally not happen if Supabase is enabled and cloned properly
|
|
print(f"Supabase is enabled but its compose file was not found at {supabase_compose_file}.")
|
|
else:
|
|
# If Supabase is not enabled, we don't include its compose file.
|
|
# Lingering Supabase containers will be handled by force_stop_lingering_containers()
|
|
print("Supabase is not enabled, its compose file will not be used for stop/rm Docker Compose commands.")
|
|
# We also check if the Supabase compose file path exists to provide more context,
|
|
# as it might exist from a previous run where Supabase was enabled.
|
|
supabase_compose_file_if_exists = os.path.join("supabase", "docker", "docker-compose.yml")
|
|
if os.path.exists(supabase_compose_file_if_exists):
|
|
print(f"Note: Supabase compose file exists at {supabase_compose_file_if_exists} but is not being used as Supabase is disabled.")
|
|
|
|
base_cmd_prefix = ["docker", "compose", "-p", "localai"] + compose_files_args
|
|
|
|
# Prepare an environment for stop/rm that has COMPOSE_PROFILES explicitly removed
|
|
# to ensure these commands affect all services in the specified compose files.
|
|
env_for_stop_rm = os.environ.copy()
|
|
env_for_stop_rm.pop("COMPOSE_PROFILES", None) # Remove COMPOSE_PROFILES if it exists
|
|
|
|
# Explicitly stop all services defined in the compose files
|
|
stop_cmd = base_cmd_prefix + ["stop"]
|
|
print("Attempting to stop all services...")
|
|
run_command(stop_cmd, env=env_for_stop_rm)
|
|
|
|
# Explicitly remove all stopped containers
|
|
rm_cmd = base_cmd_prefix + ["rm", "-f"]
|
|
print("Attempting to remove all stopped services...")
|
|
run_command(rm_cmd, env=env_for_stop_rm)
|
|
|
|
# Force stop containers that might be running outside of compose management
|
|
force_stop_lingering_containers()
|
|
|
|
def force_stop_lingering_containers():
|
|
"""Force stop and remove specific containers that might be lingering."""
|
|
print("Force stopping and removing any lingering service containers...")
|
|
|
|
# Define service container patterns that we want to ensure are stopped
|
|
# These are containers that might be running but not managed by the current compose setup
|
|
container_patterns = [
|
|
"qdrant", "langfuse", "grafana", "crawl4ai", "letta", "clickhouse",
|
|
"node-exporter", "minio", "cadvisor", "open-webui", "ollama",
|
|
"prometheus", "searxng", "supabase-", "localai-langfuse-",
|
|
"localai-clickhouse-", "localai-minio-", "realtime-dev.supabase-"
|
|
]
|
|
|
|
try:
|
|
# Get list of all running containers
|
|
result = subprocess.run(
|
|
["docker", "ps", "--format", "{{.Names}}"],
|
|
capture_output=True, text=True, check=True
|
|
)
|
|
running_containers = result.stdout.strip().split('\n')
|
|
|
|
containers_to_stop = []
|
|
for container in running_containers:
|
|
if container and any(pattern in container for pattern in container_patterns):
|
|
containers_to_stop.append(container)
|
|
|
|
if containers_to_stop:
|
|
print(f"Found lingering containers: {', '.join(containers_to_stop)}")
|
|
# Stop containers
|
|
subprocess.run(["docker", "stop"] + containers_to_stop, check=True)
|
|
print("Stopped lingering containers.")
|
|
# Remove containers
|
|
subprocess.run(["docker", "rm", "-f"] + containers_to_stop, check=True)
|
|
print("Removed lingering containers.")
|
|
else:
|
|
print("No lingering containers found.")
|
|
|
|
except subprocess.CalledProcessError as e:
|
|
print(f"Error during force stop: {e}")
|
|
except Exception as e:
|
|
print(f"Unexpected error during force stop: {e}")
|
|
|
|
def start_supabase():
|
|
"""Start the Supabase services (using its compose file)."""
|
|
if not is_supabase_enabled():
|
|
print("Supabase is not enabled, skipping start.")
|
|
return
|
|
print("Starting Supabase services...")
|
|
# Pass os.environ.copy() to ensure it uses the correct COMPOSE_PROFILES
|
|
run_command([
|
|
"docker", "compose", "-p", "localai", "-f", "supabase/docker/docker-compose.yml", "up", "-d"
|
|
], env=os.environ.copy())
|
|
|
|
def start_local_ai():
|
|
"""Start the local AI services (using its compose file)."""
|
|
print("Starting local AI services...")
|
|
cmd = ["docker", "compose", "-p", "localai"]
|
|
cmd.extend(["-f", "docker-compose.yml", "up", "-d"])
|
|
# Pass os.environ.copy() to ensure it uses the correct COMPOSE_PROFILES
|
|
run_command(cmd, env=os.environ.copy())
|
|
|
|
def generate_searxng_secret_key():
|
|
"""Generate a secret key for SearXNG based on the current platform."""
|
|
print("Checking SearXNG settings...")
|
|
|
|
# Define paths for SearXNG settings files
|
|
settings_path = os.path.join("searxng", "settings.yml")
|
|
settings_base_path = os.path.join("searxng", "settings-base.yml")
|
|
|
|
# Check if settings-base.yml exists
|
|
if not os.path.exists(settings_base_path):
|
|
print(f"Warning: SearXNG base settings file not found at {settings_base_path}")
|
|
return
|
|
|
|
# Check if settings.yml exists, if not create it from settings-base.yml
|
|
if not os.path.exists(settings_path):
|
|
print(f"SearXNG settings.yml not found. Creating from {settings_base_path}...")
|
|
try:
|
|
shutil.copyfile(settings_base_path, settings_path)
|
|
print(f"Created {settings_path} from {settings_base_path}")
|
|
except Exception as e:
|
|
print(f"Error creating settings.yml: {e}")
|
|
return
|
|
else:
|
|
print(f"SearXNG settings.yml already exists at {settings_path}")
|
|
|
|
print("Generating SearXNG secret key...")
|
|
|
|
# Detect the platform and run the appropriate command
|
|
system = platform.system()
|
|
|
|
try:
|
|
if system == "Windows":
|
|
print("Detected Windows platform, using PowerShell to generate secret key...")
|
|
# PowerShell command to generate a random key and replace in the settings file
|
|
ps_command = [
|
|
"powershell", "-Command",
|
|
"$randomBytes = New-Object byte[] 32; " +
|
|
"(New-Object Security.Cryptography.RNGCryptoServiceProvider).GetBytes($randomBytes); " +
|
|
"$secretKey = -join ($randomBytes | ForEach-Object { \"{0:x2}\" -f $_ }); " +
|
|
"(Get-Content searxng/settings.yml) -replace 'ultrasecretkey', $secretKey | Set-Content searxng/settings.yml"
|
|
]
|
|
subprocess.run(ps_command, check=True)
|
|
|
|
elif system == "Darwin": # macOS
|
|
print("Detected macOS platform, using sed command with empty string parameter...")
|
|
# macOS sed command requires an empty string for the -i parameter
|
|
openssl_cmd = ["openssl", "rand", "-hex", "32"]
|
|
random_key = subprocess.check_output(openssl_cmd).decode('utf-8').strip()
|
|
sed_cmd = ["sed", "-i", "", f"s|ultrasecretkey|{random_key}|g", settings_path]
|
|
subprocess.run(sed_cmd, check=True)
|
|
|
|
else: # Linux and other Unix-like systems
|
|
print("Detected Linux/Unix platform, using standard sed command...")
|
|
# Standard sed command for Linux
|
|
openssl_cmd = ["openssl", "rand", "-hex", "32"]
|
|
random_key = subprocess.check_output(openssl_cmd).decode('utf-8').strip()
|
|
sed_cmd = ["sed", "-i", f"s|ultrasecretkey|{random_key}|g", settings_path]
|
|
subprocess.run(sed_cmd, check=True)
|
|
|
|
print("SearXNG secret key generated successfully.")
|
|
|
|
except Exception as e:
|
|
print(f"Error generating SearXNG secret key: {e}")
|
|
print("You may need to manually generate the secret key using the commands:")
|
|
print(" - Linux: sed -i \"s|ultrasecretkey|$(openssl rand -hex 32)|g\" searxng/settings.yml")
|
|
print(" - macOS: sed -i '' \"s|ultrasecretkey|$(openssl rand -hex 32)|g\" searxng/settings.yml")
|
|
print(" - Windows (PowerShell):")
|
|
print(" $randomBytes = New-Object byte[] 32")
|
|
print(" (New-Object Security.Cryptography.RNGCryptoServiceProvider).GetBytes($randomBytes)")
|
|
print(" $secretKey = -join ($randomBytes | ForEach-Object { \"{0:x2}\" -f $_ })")
|
|
print(" (Get-Content searxng/settings.yml) -replace 'ultrasecretkey', $secretKey | Set-Content searxng/settings.yml")
|
|
|
|
def check_and_fix_docker_compose_for_searxng():
|
|
"""Check and modify docker-compose.yml for SearXNG first run."""
|
|
docker_compose_path = "docker-compose.yml"
|
|
if not os.path.exists(docker_compose_path):
|
|
print(f"Warning: Docker Compose file not found at {docker_compose_path}")
|
|
return
|
|
|
|
try:
|
|
# Read the docker-compose.yml file
|
|
with open(docker_compose_path, 'r') as file:
|
|
content = file.read()
|
|
|
|
# Default to first run
|
|
is_first_run = True
|
|
|
|
# Check if Docker is running and if the SearXNG container exists
|
|
try:
|
|
# Check if the SearXNG container is running
|
|
container_check = subprocess.run(
|
|
["docker", "ps", "--filter", "name=searxng", "--format", "{{.Names}}"],
|
|
capture_output=True, text=True, check=True
|
|
)
|
|
searxng_containers = container_check.stdout.strip().split('\n')
|
|
|
|
# If SearXNG container is running, check inside for uwsgi.ini
|
|
if any(container for container in searxng_containers if container):
|
|
container_name = next(container for container in searxng_containers if container)
|
|
print(f"Found running SearXNG container: {container_name}")
|
|
|
|
# Check if uwsgi.ini exists inside the container
|
|
container_check = subprocess.run(
|
|
["docker", "exec", container_name, "sh", "-c", "[ -f /etc/searxng/uwsgi.ini ] && echo 'found' || echo 'not_found'"],
|
|
capture_output=True, text=True, check=False
|
|
)
|
|
|
|
if "found" in container_check.stdout:
|
|
print("Found uwsgi.ini inside the SearXNG container - not first run")
|
|
is_first_run = False
|
|
else:
|
|
print("uwsgi.ini not found inside the SearXNG container - first run")
|
|
is_first_run = True
|
|
else:
|
|
print("No running SearXNG container found - assuming first run")
|
|
except Exception as e:
|
|
print(f"Error checking Docker container: {e} - assuming first run")
|
|
|
|
if is_first_run and "cap_drop: - ALL" in content:
|
|
print("First run detected for SearXNG. Temporarily removing 'cap_drop: - ALL' directive...")
|
|
# Temporarily comment out the cap_drop line
|
|
modified_content = content.replace("cap_drop: - ALL", "# cap_drop: - ALL # Temporarily commented out for first run")
|
|
|
|
# Write the modified content back
|
|
with open(docker_compose_path, 'w') as file:
|
|
file.write(modified_content)
|
|
|
|
print("Note: After the first run completes successfully, you should re-add 'cap_drop: - ALL' to docker-compose.yml for security reasons.")
|
|
elif not is_first_run and "# cap_drop: - ALL # Temporarily commented out for first run" in content:
|
|
print("SearXNG has been initialized. Re-enabling 'cap_drop: - ALL' directive for security...")
|
|
# Uncomment the cap_drop line
|
|
modified_content = content.replace("# cap_drop: - ALL # Temporarily commented out for first run", "cap_drop: - ALL")
|
|
|
|
# Write the modified content back
|
|
with open(docker_compose_path, 'w') as file:
|
|
file.write(modified_content)
|
|
|
|
except Exception as e:
|
|
print(f"Error checking/modifying docker-compose.yml for SearXNG: {e}")
|
|
|
|
def main():
|
|
load_dotenv(".env") # Load/Reload .env into os.environ
|
|
|
|
if is_supabase_enabled():
|
|
clone_supabase_repo()
|
|
prepare_supabase_env()
|
|
|
|
# Generate SearXNG secret key and check docker-compose.yml
|
|
generate_searxng_secret_key()
|
|
check_and_fix_docker_compose_for_searxng()
|
|
|
|
stop_existing_containers()
|
|
|
|
# Start Supabase first
|
|
if is_supabase_enabled():
|
|
start_supabase()
|
|
|
|
# Give Supabase some time to initialize
|
|
print("Waiting for Supabase to initialize...")
|
|
time.sleep(10)
|
|
|
|
# Then start the local AI services
|
|
start_local_ai()
|
|
|
|
if __name__ == "__main__":
|
|
main()
|