#!/usr/bin/env python3 """Render a dynamic Neko docker-compose file from the current environment.""" from __future__ import annotations import argparse import json import os from pathlib import Path def env(name: str, default: str) -> str: value = os.environ.get(name) return value if value not in {None, ""} else default def env_enabled(name: str, default: bool = False) -> bool: raw = os.environ.get(name) if raw in {None, ""}: return default return raw.strip().lower() in {"1", "true", "yes", "on", "enable", "enabled"} def q(value: str) -> str: return json.dumps(str(value), ensure_ascii=False) def instance_value(prefix: str, idx: int, default: str) -> str: return env(f"{prefix}_{idx}", default) def default_udp_range(idx: int) -> str: if idx == 1: return "52000-52099" if idx == 2: return "52101-52199" if idx == 3: return "52201-52299" start = 52400 + ((idx - 4) * 100) return f"{start}-{start + 99}" def render(count: int) -> str: image = env("NEKO_IMAGE", "ghcr.io/m1k1o/neko/chromium:3.0.0") shm = env("NEKO_SHM_SIZE", "512mb") screen = env("NEKO_DESKTOP_SCREEN", "1280x720@15") user_pass = env("NEKO_USER_PASS", "12345678") admin_pass = env("NEKO_ADMIN_PASS", "12345678") icelite = env("NEKO_WEBRTC_ICELITE", "0") nat1to1 = env("NEKO_WEBRTC_NAT1TO1", "") max_fps = env("NEKO_MAX_FPS", "15") video_bitrate = env("NEKO_VIDEO_BITRATE", "1200") plugins_enabled = env("NEKO_PLUGINS_ENABLED", "true") start_url = env("NEKO_START_URL", "https://accounts.google.com/ServiceLogin?continue=https://www.youtube.com/") render_mode = env("NEKO_BROWSER_RENDER_MODE", "auto") supervisor_conf = env("NEKO_BROWSER_SUPERVISOR_CONF", "chromium.conf") base_profile = env("NEKO_PROFILE_HOST_DIR", str(Path.cwd() / "data" / "chromium-profile-v2")) http_base = int(env("NEKO_HTTP_PORT", "9200")) mux_base = int(env("NEKO_WEBRTC_TCPMUX", "52300")) managed_policies = env_enabled("NEKO_MANAGED_POLICIES", True) external_extensions = env_enabled("NEKO_EXTERNAL_EXTENSIONS", False) policy_dir = "./policies" if managed_policies else "./no-managed-policies" lines = [ "# Generated by tools/render_neko_compose.py - do not hand-edit", "services:", ] for idx in range(1, count + 1): http_port = instance_value("NEKO_HTTP_PORT", idx, str(http_base + idx - 1)) tcp_mux = instance_value("NEKO_WEBRTC_TCPMUX", idx, str(mux_base + idx - 1)) udp_mux = instance_value("NEKO_WEBRTC_UDPMUX", idx, str(mux_base + idx - 1)) udp_range = instance_value("NEKO_WEBRTC_UDP_RANGE", idx, default_udp_range(idx)) epr = instance_value("NEKO_WEBRTC_EPR", idx, udp_range) profile = instance_value("NEKO_PROFILE_HOST_DIR", idx, base_profile if idx == 1 else f"{base_profile}-{idx}") lines.extend( [ f" neko{idx}:", f" image: {q(image)}", " restart: unless-stopped", f" shm_size: {q(shm)}", " cap_add:", " - SYS_ADMIN", f" container_name: live-neko-{idx}", " environment:", ' DISPLAY: ":99.0"', f" NEKO_DESKTOP_SCREEN: {q(screen)}", ' NEKO_SERVER_BIND: ":8080"', ' NEKO_MEMBER_PROVIDER: "multiuser"', f" NEKO_MEMBER_MULTIUSER_USER_PASSWORD: {q(user_pass)}", f" NEKO_MEMBER_MULTIUSER_ADMIN_PASSWORD: {q(admin_pass)}", f" NEKO_WEBRTC_ICELITE: {q(icelite)}", f" NEKO_WEBRTC_NAT1TO1: {q(nat1to1)}", f" NEKO_MAX_FPS: {q(max_fps)}", f" NEKO_VIDEO_BITRATE: {q(video_bitrate)}", f" NEKO_PLUGINS_ENABLED: {q(plugins_enabled)}", ' NEKO_PLUGINS_DIR: "/etc/neko/plugins/"', f" NEKO_START_URL: {q(start_url)}", f" NEKO_BROWSER_RENDER_MODE: {q(render_mode)}", f" NEKO_MANAGED_POLICIES: {q('1' if managed_policies else '0')}", f" NEKO_WEBRTC_EPR: {q(epr)}", f" NEKO_WEBRTC_TCPMUX: {q(tcp_mux)}", f" NEKO_WEBRTC_UDPMUX: {q(udp_mux)}", " ports:", f" - {q(f'{http_port}:8080')}", f" - {q(f'{tcp_mux}:{tcp_mux}/tcp')}", f" - {q(f'{udp_mux}:{udp_mux}/udp')}", f" - {q(f'{udp_range}:{udp_range}/udp')}", " volumes:", f" - {q(f'{profile}:/home/neko/.config/chromium')}", f" - {q(f'{profile}:/home/neko/.config/google-chrome')}", f" - {q(f'{policy_dir}:/etc/chromium/policies/managed:ro')}", f" - {q(f'{policy_dir}:/etc/chromium-browser/policies/managed:ro')}", f" - {q(f'{policy_dir}:/etc/opt/chrome/policies/managed:ro')}", ' - "./tampermonkey-seed:/etc/neko/tampermonkey-seed:ro"', ' - "./provisioning:/var/www/provisioning:ro"', ' - "./browser-launch.sh:/etc/neko/browser-launch.sh:ro"', f" - {q(f'./{supervisor_conf}:/etc/neko/supervisord/{supervisor_conf}:ro')}", ] ) if external_extensions: lines[-3:-3] = [ ' - "./external-extensions:/opt/google/chrome/extensions:ro"', ' - "./external-extensions:/usr/share/chromium/extensions:ro"', ' - "./external-extensions:/usr/share/chromium-browser/extensions:ro"', ] return "\n".join(lines) + "\n" def main() -> int: parser = argparse.ArgumentParser(description="Render Neko docker compose") parser.add_argument("--count", type=int, required=True) parser.add_argument("--output", required=True) args = parser.parse_args() count = max(2, min(64, args.count)) output = Path(args.output) output.parent.mkdir(parents=True, exist_ok=True) output.write_text(render(count), encoding="utf-8") return 0 if __name__ == "__main__": raise SystemExit(main())