#!/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 re import subprocess import shutil import time import platform import yaml from dotenv import dotenv_values def is_supabase_enabled(): """Check if 'supabase' is in COMPOSE_PROFILES in .env file.""" env_values = dotenv_values(".env") compose_profiles = env_values.get("COMPOSE_PROFILES", "") return "supabase" in compose_profiles.split(',') def is_dify_enabled(): """Check if 'dify' is in COMPOSE_PROFILES in .env file.""" env_values = dotenv_values(".env") compose_profiles = env_values.get("COMPOSE_PROFILES", "") return "dify" in compose_profiles.split(',') def get_gpu_devices_compose_files(): """Return GPU pinning override files for services with *_GPU_DEVICES set in .env.""" env_values = dotenv_values(".env") profiles = [p.strip() for p in (env_values.get("COMPOSE_PROFILES") or "").split(',')] files = [] for var, profile, compose_file in [ ("OLLAMA_GPU_DEVICES", "gpu-nvidia", "docker-compose.ollama-gpu-devices.yml"), ("INVOKEAI_GPU_DEVICES", "invokeai-nvidia", "docker-compose.invokeai-gpu-devices.yml"), ("COMFYUI_GPU_DEVICES", "comfyui-nvidia", "docker-compose.comfyui-gpu-devices.yml"), ]: if not env_values.get(var): continue if profile in profiles and os.path.exists(compose_file): files.append(compose_file) else: print(f"Warning: {var} is set but GPU pinning is NOT applied " f"(requires the {profile} profile and {compose_file}).") return files def get_open_webui_postgres_compose_files(): """Return the Open WebUI PostgreSQL override when OPEN_WEBUI_DATABASE=postgres. Without it Open WebUI keeps its SQLite database in the open-webui volume, which is what existing installations stay on (issue #105). """ env_values = dotenv_values(".env") # Compose trims whitespace around profile names, so "n8n, open-webui" # really does start open-webui. Strip here or the guard below - and the # error it prints - are both skipped while the service runs on SQLite. profiles = [p.strip() for p in (env_values.get("COMPOSE_PROFILES") or "").split(',')] compose_file = "docker-compose.open-webui-postgres.yml" if env_values.get("OPEN_WEBUI_DATABASE") == "postgres" and "open-webui" in profiles: if os.path.exists(compose_file): return [compose_file] # Never fail silently here: without the override Open WebUI starts on # SQLite and shows an empty interface, which is indistinguishable from # data loss for the user. print(f"ERROR: OPEN_WEBUI_DATABASE=postgres but {compose_file} is missing. " f"Open WebUI will start on SQLite and appear EMPTY. " f"Restore the file or set OPEN_WEBUI_DATABASE=sqlite in .env.") return [] def get_neo4j_compose_files(): """Return the Neo4j Bolt port override while the neo4j profile is active. It publishes 7687 on caddy; the base file no longer does, so the port is not open on installs without Neo4j (issue #134). """ env_values = dotenv_values(".env") profiles = [p.strip() for p in (env_values.get("COMPOSE_PROFILES") or "").split(',')] compose_file = "docker-compose.neo4j.yml" if "neo4j" in profiles: if os.path.exists(compose_file): return [compose_file] print(f"WARNING: the neo4j profile is active but {compose_file} is missing - " f"Bolt (7687) is NOT published, so Neo4j Browser cannot connect. " f"Restore it with: git checkout -- {compose_file}") return [] def get_all_profiles(compose_file): """Get all profile names from a docker-compose file.""" if not os.path.exists(compose_file): return [] with open(compose_file, 'r') as f: compose_config = yaml.safe_load(f) profiles = set() if 'services' in compose_config: for service_name, service_config in compose_config.get('services', {}).items(): if service_config and 'profiles' in service_config: for profile in service_config['profiles']: profiles.add(profile) return list(profiles) def run_command(cmd, cwd=None): """Run a shell command and print it.""" print("Running:", " ".join(cmd)) subprocess.run(cmd, cwd=cwd, check=True) 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") # Discard any local postgres image pin so 'git pull' stays conflict-free; # pin_supabase_postgres_image() re-applies it below after the pull. subprocess.run(["git", "checkout", "HEAD", "--", "docker/docker-compose.yml"], check=False) run_command(["git", "pull"]) os.chdir("..") # Keep supabase-db on the Postgres major version matching existing data (issue #64). pin_supabase_postgres_image() # Pins for supabase/postgres keyed by the data's major version. When Supabase # upstream bumps the postgres image to a new MAJOR version, existing data volumes # on an older major can no longer be read. Map the on-disk major to a known-good # image tag so 'make update' keeps existing databases working instead of silently # upgrading them into an unhealthy state. SUPABASE_POSTGRES_PINS = { "15": "supabase/postgres:15.8.1.085", } def pin_supabase_postgres_image(): """Keep supabase-db on the Postgres major version matching existing data. Supabase upstream periodically bumps supabase/postgres to a new major version (e.g. 15 -> 17). PostgreSQL's on-disk format is not compatible across majors, so a silent bump leaves supabase-db unhealthy on an existing data volume. This reads the major version of the data already on disk and, if upstream now wants a different major, pins the image in the freshly pulled compose file back to a compatible tag. Fresh installs (no data yet) are left on upstream. """ compose_path = os.path.join("supabase", "docker", "docker-compose.yml") pg_version_path = os.path.join("supabase", "docker", "volumes", "db", "data", "PG_VERSION") # No existing data (fresh install) or no compose yet: use upstream as-is. if not (os.path.exists(compose_path) and os.path.exists(pg_version_path)): return with open(pg_version_path) as f: data_major = f.read().strip() with open(compose_path) as f: content = f.read() match = re.search(r"image:\s*supabase/postgres:(\S+)", content) if not match: # Data exists (checked above) but the image line is missing — e.g. an upstream # compose refactor. Don't silently skip the safety pin; warn so the likely # supabase-db breakage is traceable instead of a mysterious unhealthy container. print( f"WARNING: Existing Supabase data is on PG{data_major}, but the " f"supabase/postgres image line was not found in {compose_path}. The version " f"pin could not be applied; supabase-db may start on an incompatible major." ) return upstream_tag = match.group(1) upstream_major = upstream_tag.split(".")[0] # Same major as the data on disk: allow patch-level updates, nothing to pin. if upstream_major == data_major: return pin = SUPABASE_POSTGRES_PINS.get(data_major) if not pin: print( f"WARNING: Supabase upstream now uses postgres major {upstream_major}, " f"but your data volume is on major {data_major} and no compatible pin is " f"known. supabase-db may fail to start; a manual data migration is required." ) return # The regex requires a ':' immediately after "supabase/postgres", so it matches # only the db service's image, not "supabase/postgres-meta" (the meta service). # count=1 is belt-and-suspenders against any future second match. new_content = content.replace(match.group(0), f"image: {pin}", 1) with open(compose_path, "w") as f: f.write(new_content) print( f"Pinned Supabase Postgres to {pin} to match your existing PG{data_major} data " f"(upstream moved to {upstream_tag}). 'make update' will not upgrade the major " f"version automatically; a manual migration is required to move to PG{upstream_major}." ) def prepare_supabase_env(): """Copy .env to supabase/docker/.env, or sync new variables if it already exists.""" if not is_supabase_enabled(): print("Supabase is not enabled, skipping env preparation.") return env_path = os.path.join("supabase", "docker", ".env") root_env_path = ".env" if os.path.exists(env_path): # Sync new variables from root .env that don't exist in supabase .env print(f"Syncing new variables from root .env to {env_path}...") root_env = dotenv_values(root_env_path) supabase_env = dotenv_values(env_path) new_vars = [] for key, value in root_env.items(): if key not in supabase_env and value is not None: # Quote values to handle special characters safely if '$' in value: new_vars.append(f"{key}='{value}'") else: new_vars.append(f'{key}="{value}"') if new_vars: with open(env_path, 'r') as f: existing_content = f.read() sync_header = "# --- Variables synced from root .env ---" with open(env_path, 'a') as f: if sync_header not in existing_content: f.write(f"\n{sync_header}\n") for var in new_vars: f.write(f"{var}\n") print(f"Synced {len(new_vars)} new variable(s) to Supabase env.") else: print("Supabase env is up to date, no new variables to sync.") return print("Copying .env in root to .env in supabase/docker...") shutil.copyfile(root_env_path, env_path) def clone_dify_repo(): """Clone the Dify repository using sparse checkout if not already present.""" if not is_dify_enabled(): print("Dify is not enabled, skipping clone.") return if not os.path.exists("dify"): print("Cloning the Dify repository...") run_command([ "git", "clone", "--filter=blob:none", "--no-checkout", "https://github.com/langgenius/dify.git" ]) os.chdir("dify") run_command(["git", "sparse-checkout", "init", "--cone"]) run_command(["git", "sparse-checkout", "set", "docker"]) # Dify's default branch is 'main' run_command(["git", "checkout", "main"]) os.chdir("..") else: print("Dify repository already exists, updating...") os.chdir("dify") run_command(["git", "pull"]) os.chdir("..") def prepare_dify_env(): """Create dify/docker/.env from env.example and inject selected values from root .env. Mapping (strip DIFY_ prefix from root .env): - DIFY_SECRET_KEY -> SECRET_KEY - DIFY_EXPOSE_NGINX_PORT -> EXPOSE_NGINX_PORT - DIFY_EXPOSE_NGINX_SSL_PORT -> EXPOSE_NGINX_SSL_PORT """ if not is_dify_enabled(): print("Dify is not enabled, skipping env preparation.") return dify_docker_dir = os.path.join("dify", "docker") if not os.path.isdir(dify_docker_dir): print(f"Warning: Dify docker directory not found at {dify_docker_dir}. Have you cloned the repo?") return # Determine env example file name: prefer 'env.example', fallback to '.env.example' env_example_candidates = [ os.path.join(dify_docker_dir, "env.example"), os.path.join(dify_docker_dir, ".env.example"), ] env_example_path = next((p for p in env_example_candidates if os.path.exists(p)), None) if env_example_path is None: print(f"Warning: Could not find env.example in {dify_docker_dir}") return env_path = os.path.join(dify_docker_dir, ".env") print(f"Creating {env_path} from {env_example_path}...") with open(env_example_path, 'r') as f: env_content = f.read() # Load values from root .env root_env = dotenv_values(".env") mapping = { "SECRET_KEY": root_env.get("DIFY_SECRET_KEY", ""), "EXPOSE_NGINX_PORT": root_env.get("DIFY_EXPOSE_NGINX_PORT", ""), "EXPOSE_NGINX_SSL_PORT": root_env.get("DIFY_EXPOSE_NGINX_SSL_PORT", ""), } # Replace or append variables in env_content lines = env_content.splitlines() replaced_keys = set() for i, line in enumerate(lines): for dest_key, value in mapping.items(): if line.startswith(f"{dest_key}=") and value: lines[i] = f"{dest_key}={value}" replaced_keys.add(dest_key) # Append any missing keys with values for dest_key, value in mapping.items(): if value and dest_key not in replaced_keys: lines.append(f"{dest_key}={value}") with open(env_path, 'w') as f: f.write("\n".join(lines) + "\n") def is_postiz_enabled(): """Check if 'postiz' is in COMPOSE_PROFILES in .env file.""" env_values = dotenv_values(".env") compose_profiles = env_values.get("COMPOSE_PROFILES", "") return "postiz" in compose_profiles.split(',') def prepare_postiz_env(): """Generate postiz.env for mounting as /app/.env in Postiz container. The Postiz image uses dotenv-cli (dotenv -e ../../.env) to load config. Always regenerated to reflect current .env values. """ if not is_postiz_enabled(): print("Postiz is not enabled, skipping env preparation.") return print("Generating postiz.env from root .env values...") root_env = dotenv_values(".env") hostname = root_env.get("POSTIZ_HOSTNAME", "") frontend_url = f"https://{hostname}" if hostname else "" env_vars = { "BACKEND_INTERNAL_URL": "http://localhost:3000", "DATABASE_URL": f"postgresql://postgres:{root_env.get('POSTGRES_PASSWORD', '')}@postgres:5432/{root_env.get('POSTIZ_DB_NAME', 'postiz')}?schema=postiz", "DISABLE_REGISTRATION": root_env.get("POSTIZ_DISABLE_REGISTRATION", "false"), "FRONTEND_URL": frontend_url, "IS_GENERAL": "true", "JWT_SECRET": root_env.get("JWT_SECRET", ""), "MAIN_URL": frontend_url, "NEXT_PUBLIC_BACKEND_URL": f"{frontend_url}/api" if frontend_url else "", "NEXT_PUBLIC_UPLOAD_DIRECTORY": "/uploads", "REDIS_URL": "redis://redis:6379", "STORAGE_PROVIDER": "local", "TEMPORAL_ADDRESS": "temporal:7233", "UPLOAD_DIRECTORY": "/uploads", } # Social media API keys — direct pass-through from root .env social_keys = [ "X_API_KEY", "X_API_SECRET", "LINKEDIN_CLIENT_ID", "LINKEDIN_CLIENT_SECRET", "REDDIT_CLIENT_ID", "REDDIT_CLIENT_SECRET", "GITHUB_CLIENT_ID", "GITHUB_CLIENT_SECRET", "BEEHIIVE_API_KEY", "BEEHIIVE_PUBLICATION_ID", "THREADS_APP_ID", "THREADS_APP_SECRET", "FACEBOOK_APP_ID", "FACEBOOK_APP_SECRET", "YOUTUBE_CLIENT_ID", "YOUTUBE_CLIENT_SECRET", "TIKTOK_CLIENT_ID", "TIKTOK_CLIENT_SECRET", "PINTEREST_CLIENT_ID", "PINTEREST_CLIENT_SECRET", "DRIBBBLE_CLIENT_ID", "DRIBBBLE_CLIENT_SECRET", "DISCORD_CLIENT_ID", "DISCORD_CLIENT_SECRET", "DISCORD_BOT_TOKEN_ID", "SLACK_ID", "SLACK_SECRET", "SLACK_SIGNING_SECRET", "MASTODON_URL", "MASTODON_CLIENT_ID", "MASTODON_CLIENT_SECRET", ] for key in social_keys: env_vars[key] = root_env.get(key, "") # Handle case where Docker created postiz.env as a directory if os.path.isdir("postiz.env"): print("Warning: postiz.env exists as a directory (likely created by Docker). Removing...") shutil.rmtree("postiz.env") with open("postiz.env", 'w') as f: for key, value in env_vars.items(): f.write(f'{key}="{value}"\n') print(f"Generated postiz.env with {len(env_vars)} variables.") def remove_legacy_supabase_kong(): """Remove the supabase-kong container Supabase replaced with Envoy. No compose file lists it any more, so 'down' leaves it holding port 8000. Runs after the Supabase git pull and before 'down', so a failed pull or removal leaves the running stack as it is. """ # Decided by the exit code, never by Docker's error wording listing = subprocess.run( ["docker", "ps", "-a", "--filter", "name=supabase-kong", "--format", '{{.Names}}\t{{.Label "com.docker.compose.project"}}/{{.Label "com.docker.compose.service"}}'], capture_output=True, text=True, ) if listing.returncode != 0: print(f"ERROR: could not check for a leftover supabase-kong container: {listing.stderr.strip()}. " "Nothing was stopped; fix Docker and run the same command again.") raise SystemExit(1) # The name filter also matches substrings owners = [labels for name, _, labels in (line.partition("\t") for line in listing.stdout.splitlines()) if name == "supabase-kong"] if not owners: return owner = owners[0] if owner != "localai/kong": print(f"WARNING: a supabase-kong container not created by this stack ({owner if owner != '/' else 'no Compose labels'}) " "exists; if it holds port 8000, Supabase's Envoy gateway cannot start.") return print("Removing the old Supabase Kong gateway container so Envoy can bind port 8000...") try: run_command(["docker", "rm", "-f", "supabase-kong"]) except subprocess.CalledProcessError: print("ERROR: could not remove supabase-kong, which holds port 8000. Nothing was stopped; " "remove it manually with 'docker rm -f supabase-kong' and run the same command again.") raise SystemExit(1) def stop_existing_containers(): """Stop and remove existing containers for our unified project ('localai').""" print("Stopping and removing existing containers for the unified project 'localai'...") # Base command with project name for consistency cmd = ["docker", "compose", "-p", "localai"] # Get all profiles from the main docker-compose.yml to ensure all services can be brought down all_profiles = get_all_profiles("docker-compose.yml") for profile in all_profiles: cmd.extend(["--profile", profile]) cmd.extend(["-f", "docker-compose.yml"]) # Check if the Supabase Docker Compose file exists. If so, include it in the 'down' command. supabase_compose_path = os.path.join("supabase", "docker", "docker-compose.yml") if os.path.exists(supabase_compose_path): cmd.extend(["-f", supabase_compose_path]) # Check if the Dify Docker Compose file exists. If so, include it in the 'down' command. dify_compose_path = os.path.join("dify", "docker", "docker-compose.yaml") if os.path.exists(dify_compose_path): cmd.extend(["-f", dify_compose_path]) for profile in get_all_profiles(dify_compose_path): cmd.extend(["--profile", profile]) # Check if the n8n workers compose file exists. If so, include it in the 'down' command. n8n_workers_compose_path = "docker-compose.n8n-workers.yml" if os.path.exists(n8n_workers_compose_path): cmd.extend(["-f", n8n_workers_compose_path]) ollama_instances_compose_path = "docker-compose.ollama-instances.yml" if os.path.exists(ollama_instances_compose_path): cmd.extend(["-f", ollama_instances_compose_path]) open_webui_pg_compose_path = "docker-compose.open-webui-postgres.yml" if os.path.exists(open_webui_pg_compose_path): cmd.extend(["-f", open_webui_pg_compose_path]) neo4j_compose_path = "docker-compose.neo4j.yml" if os.path.exists(neo4j_compose_path): cmd.extend(["-f", neo4j_compose_path]) # Include user overrides if present override_path = "docker-compose.override.yml" if os.path.exists(override_path): cmd.extend(["-f", override_path]) cmd.extend(["down"]) run_command(cmd) 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...") run_command([ "docker", "compose", "-p", "localai", "-f", "supabase/docker/docker-compose.yml", "up", "-d" ]) def start_dify(): """Start the Dify services (using its compose file).""" if not is_dify_enabled(): print("Dify is not enabled, skipping start.") return print("Starting Dify services...") run_command([ "docker", "compose", "-p", "localai", "--profile", "postgresql", "--profile", "weaviate", "-f", "dify/docker/docker-compose.yaml", "up", "-d", ]) def start_local_ai(): """Start the local AI services (using its compose file).""" print("Starting local AI services...") # Build compose files list compose_files = ["-f", "docker-compose.yml"] # Check if n8n workers compose file exists (generated by generate_n8n_workers.sh) n8n_workers_compose_path = "docker-compose.n8n-workers.yml" if os.path.exists(n8n_workers_compose_path): compose_files.extend(["-f", n8n_workers_compose_path]) # Extra Ollama instances (generated by generate_ollama_instances.sh). # Unlike the GPU pinning overrides this is not gated on a *_GPU_DEVICES # variable, but it does require an Ollama hardware profile, to stay # equivalent to get_ollama_instances_compose() in scripts/utils.sh. ollama_instances_compose_path = "docker-compose.ollama-instances.yml" env_values = dotenv_values(".env") active_profiles = [p.strip() for p in (env_values.get("COMPOSE_PROFILES") or "").split(',')] if any(p in active_profiles for p in ("gpu-nvidia", "gpu-amd", "cpu")): if os.path.exists(ollama_instances_compose_path): compose_files.extend(["-f", ollama_instances_compose_path]) else: try: instance_count = int(env_values.get("OLLAMA_INSTANCE_COUNT") or 1) except ValueError: instance_count = 1 if instance_count > 1: # Say so: .env promises N instances and exactly one will start. print(f"WARNING: OLLAMA_INSTANCE_COUNT={instance_count} but " f"{ollama_instances_compose_path} is missing. Only one Ollama " f"instance will start. Regenerate it with: " f"bash scripts/generate_ollama_instances.sh") # Include GPU pinning overrides when *_GPU_DEVICES is set in .env for gpu_devices_compose_path in get_gpu_devices_compose_files(): compose_files.extend(["-f", gpu_devices_compose_path]) # Include the Open WebUI PostgreSQL override when OPEN_WEBUI_DATABASE=postgres for open_webui_compose_path in get_open_webui_postgres_compose_files(): compose_files.extend(["-f", open_webui_compose_path]) # Publish the Neo4j Bolt port on caddy only while the neo4j profile is active for neo4j_compose_path in get_neo4j_compose_files(): compose_files.extend(["-f", neo4j_compose_path]) # Include user overrides if present (must be last for highest precedence) override_path = "docker-compose.override.yml" if os.path.exists(override_path): compose_files.extend(["-f", override_path]) # Explicitly build services and pull newer base images first. print("Checking for newer base images and building services...") build_cmd = ["docker", "compose", "-p", "localai"] + compose_files + ["build", "--pull"] run_command(build_cmd) # Now, start the services using the newly built images. No --build needed as we just built. print("Starting containers...") up_cmd = ["docker", "compose", "-p", "localai"] + compose_files + ["up", "-d"] run_command(up_cmd) 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 and ensure correct multi-line YAML format correct_cap_drop_block = "cap_drop:\n - ALL" # Note the newline and indentation for the list item modified_content = content.replace("# cap_drop: - ALL # Temporarily commented out for first run", correct_cap_drop_block) # 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(): # Clone and prepare repositories if is_supabase_enabled(): clone_supabase_repo() prepare_supabase_env() if is_dify_enabled(): clone_dify_repo() prepare_dify_env() # Generate SearXNG secret key and check docker-compose.yml generate_searxng_secret_key() check_and_fix_docker_compose_for_searxng() # Generate Postiz env file prepare_postiz_env() remove_legacy_supabase_kong() 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) # Start Dify services if is_dify_enabled(): start_dify() # Give Dify some time to initialize print("Waiting for Dify to initialize...") time.sleep(10) # Then start the local AI services start_local_ai() if __name__ == "__main__": main()