ReMe/deploy/docker/reme_container.py
jinliyl 4c54c2b650
Some checks failed
CI / Python packages / Build and verify distributions (push) Waiting to run
CI / Python quality / GitHub Actions (push) Waiting to run
CI / Python quality / Pre-commit (push) Waiting to run
CI / Python tests / Unit Tests - py3.11 (push) Waiting to run
CI / Python tests / Unit Tests - py3.12 (push) Waiting to run
CI / Python tests / Unit Tests - py3.13 (push) Waiting to run
CI / Python tests / Unit Tests - py3.14 (push) Waiting to run
CI / Windows / CLI smoke - py3.11 (push) Waiting to run
Deploy / Documentation / Build documentation (push) Waiting to run
Deploy / Documentation / deploy (push) Blocked by required conditions
CI and Release / Docker / Build and test / amd64 (push) Waiting to run
CI and Release / Docker / Build and test / arm64 (push) Waiting to run
CI and Release / Docker / Publish multi-platform tags (push) Blocked by required conditions
Security / CodeQL / Analyze javascript-typescript (push) Waiting to run
Security / CodeQL / Analyze python (push) Waiting to run
CI / ReMe Studio / Studio checks (push) Has been cancelled
feat(docker): add container deployment and multi-platform release workflow (#582)
* feat(docker): add container deployment and multi-platform release workflow

* fix(llm): initialize providers on model access and preserve runtime injection

* test(llm): keep rejected model updates independent of credentials
2026-10-03 23:54:58 +08:00

100 lines
4.1 KiB
Python

"""Container startup and health checks using ReMe's existing CLI contract."""
from __future__ import annotations
import json
import os
from pathlib import Path
import sys
from urllib.request import ProxyHandler, Request, build_opener
HEALTH_STATE_PATH = Path("/tmp/reme-health.json")
_ENV_OVERRIDES = {
"REME_CONFIG": "config",
"REME_WORKSPACE_DIR": "workspace_dir",
"REME_HOST": "service.host",
"REME_PORT": "service.port",
"REME_TIMEZONE": "timezone",
}
def start_command(arguments: list[str], environment: dict[str, str]) -> list[str]:
"""Add container defaults only to `start`; explicit CLI arguments win."""
from reme.config import deep_merge_config, parse_kwargs
defaults = parse_kwargs(
"log_to_file=false",
*[f"{key}={json.dumps(environment[name])}" for name, key in _ENV_OVERRIDES.items() if environment.get(name)],
)
# Docker environment values are strings; the HTTP service expects an integer port.
if environment.get("REME_PORT"):
defaults["service"]["port"] = int(environment["REME_PORT"])
overrides = deep_merge_config(defaults, parse_kwargs(*arguments))
return ["reme", "start", *[f"{key}={json.dumps(value, ensure_ascii=False)}" for key, value in overrides.items()]]
def write_health_state(command: list[str], state_path: Path = HEALTH_STATE_PATH) -> None:
"""Record only the effective HTTP address, never credentials or user data."""
from reme.components.service.cli_service import prepare_start_config
from reme.config import parse_kwargs
from reme.constants import REME_DEFAULT_HOST, REME_DEFAULT_PORT, normalize_connect_host
from reme.plugin import resolve_plugin_runtime
config = resolve_plugin_runtime(prepare_start_config(parse_kwargs(*command[2:]))).config
service = config.get("service") or {}
if service.get("backend") != "http":
return
host = normalize_connect_host(service.get("host") or REME_DEFAULT_HOST)
if host == "::":
host = "::1"
port = int(service.get("port", REME_DEFAULT_PORT))
if not 1 <= port <= 65535:
raise ValueError("service.port must be between 1 and 65535")
host = f"[{host}]" if ":" in host else host
with state_path.open("w", encoding="utf-8") as state_file:
os.chmod(state_path, 0o600)
json.dump({"url": f"http://{host}:{port}/health_check"}, state_file)
def healthcheck(state_path: Path = HEALTH_STATE_PATH) -> int:
"""Require both a successful Job and a healthy component snapshot."""
try:
state = json.loads(state_path.read_text(encoding="utf-8"))
request = Request(state["url"], data=b"{}", headers={"Content-Type": "application/json"}, method="POST")
# A deployment's outbound proxy must not intercept its local probe.
with build_opener(ProxyHandler({})).open(request, timeout=4) as response:
payload = json.load(response)
healthy = payload.get("metadata", {}).get("health", {}).get("healthy")
if payload.get("success") is True and healthy is True:
return 0
except (OSError, ValueError, KeyError, TypeError, AttributeError):
pass
print("ReMe HTTP health check failed; inspect the container logs and POST /health_check", file=sys.stderr)
return 1
def main() -> int:
"""Prepare startup, then replace this process so signals reach ReMe."""
arguments = sys.argv[1:]
if arguments == ["--healthcheck"]:
return healthcheck()
HEALTH_STATE_PATH.unlink(missing_ok=True)
Path(os.environ.get("HOME", "/tmp/reme-home")).mkdir(parents=True, exist_ok=True)
if not arguments:
arguments = ["start"]
start_actions = {"start", "-start", "--start"}
if len(arguments) >= 2 and arguments[0] == "reme" and arguments[1] in start_actions:
arguments = arguments[1:]
if arguments[0] in start_actions:
from reme.utils import load_env
load_env()
arguments = start_command(arguments[1:], dict(os.environ))
write_health_state(arguments)
os.execvp(arguments[0], arguments)
return 0
if __name__ == "__main__":
raise SystemExit(main())