From c35bb82557ea0b911af26ffd07a7ebf1eff21ffc Mon Sep 17 00:00:00 2001 From: GeiserX <9169332+GeiserX@users.noreply.github.com> Date: Sat, 18 Apr 2026 21:55:17 +0200 Subject: [PATCH 1/2] fix: update Storj deployment config and add stop_timeout support - Fix Storj image name (storjlabs/storagenode, not storj/storagenode) - Add stop_timeout field to service schema and orchestrator - Respect per-service stop_timeout on stop/restart (Storj needs 300s) - Add one-time setup instructions to Storj YAML - Improve Storj collector error message with LAN IP hint Closes #24 --- app/collectors/storj.py | 7 +++++- app/orchestrator.py | 47 ++++++++++++++++++++++++-------------- services/_schema.yml | 2 ++ services/storage/storj.yml | 14 +++++++++--- 4 files changed, 49 insertions(+), 21 deletions(-) diff --git a/app/collectors/storj.py b/app/collectors/storj.py index da16a20..7b995e2 100644 --- a/app/collectors/storj.py +++ b/app/collectors/storj.py @@ -62,7 +62,12 @@ async def collect(self) -> EarningsResult: return EarningsResult( platform=self.platform, balance=0.0, - error="Storagenode API not reachable — is port 14002 accessible?", + error=( + "Storagenode API not reachable at " + f"{self.api_url} — if running in Docker, use " + "the node's LAN IP instead of localhost " + "(e.g. http://192.168.1.x:14002)" + ), ) except Exception as exc: logger.error("Storj collection failed: %s", exc) diff --git a/app/orchestrator.py b/app/orchestrator.py index 57902e2..b45db50 100644 --- a/app/orchestrator.py +++ b/app/orchestrator.py @@ -181,6 +181,7 @@ def deploy_service( network_mode = docker_conf.get("network_mode") or None cap_add = docker_conf.get("cap_add") or None privileged = docker_conf.get("privileged", False) + stop_timeout = docker_conf.get("stop_timeout") # Command: resolve ${VAR} placeholders from env dict raw_command = docker_conf.get("command") or None @@ -208,21 +209,24 @@ def deploy_service( logger.warning("Failed to pull image %s: %s (trying local)", image, exc) logger.info("Creating container %s from %s", name, image) - container = client.containers.run( - image=image, - name=name, - environment=env, - ports=ports if ports and network_mode != "host" else None, - volumes=volumes if volumes else None, - network_mode=network_mode, - cap_add=cap_add, - privileged=privileged, - command=command if command else None, - labels=labels, - hostname=hostname or f"cashpilot-{slug}", - detach=True, - restart_policy={"Name": "unless-stopped"}, - ) + run_kwargs: dict[str, Any] = { + "image": image, + "name": name, + "environment": env, + "ports": ports if ports and network_mode != "host" else None, + "volumes": volumes if volumes else None, + "network_mode": network_mode, + "cap_add": cap_add, + "privileged": privileged, + "command": command if command else None, + "labels": labels, + "hostname": hostname or f"cashpilot-{slug}", + "detach": True, + "restart_policy": {"Name": "unless-stopped"}, + } + if stop_timeout: + run_kwargs["stop_timeout"] = int(stop_timeout) + container = client.containers.run(**run_kwargs) logger.info("Container %s started: %s", name, container.short_id) return container.id @@ -295,17 +299,26 @@ def deploy_raw( return container.id +def _get_stop_timeout(slug: str) -> int: + """Return the stop timeout from the catalog, or 30s default.""" + if get_service: + svc = get_service(slug) + if svc: + return int(svc.get("docker", {}).get("stop_timeout", 30)) + return 30 + + def stop_service(slug: str) -> None: """Stop the container for a service.""" container = _find_container(slug) - container.stop(timeout=30) + container.stop(timeout=_get_stop_timeout(slug)) logger.info("Stopped container %s", container.name) def restart_service(slug: str) -> None: """Restart the container for a service.""" container = _find_container(slug) - container.restart(timeout=30) + container.restart(timeout=_get_stop_timeout(slug)) logger.info("Restarted container %s", container.name) diff --git a/services/_schema.yml b/services/_schema.yml index 385363b..91b3eb6 100644 --- a/services/_schema.yml +++ b/services/_schema.yml @@ -31,6 +31,8 @@ # command: "" (optional override) # network_mode: "" (optional, e.g. "host") # cap_add: [] (optional) +# stop_timeout: 30 (optional, seconds to wait before SIGKILL on stop — default 30) +# setup: "" (optional, one-time setup instructions/commands to run before first deploy) # requirements: # residential_ip: true/false diff --git a/services/storage/storj.yml b/services/storage/storj.yml index d5efdb1..9582218 100644 --- a/services/storage/storj.yml +++ b/services/storage/storj.yml @@ -16,7 +16,7 @@ referral: signup_url: "https://storj.dev/node/get-started/setup" docker: - image: storj/storagenode + image: storjlabs/storagenode platforms: [linux/amd64, linux/arm64] env: - key: WALLET @@ -56,11 +56,19 @@ docker: - "${STORAGE_DIR}:/app/config" command: "--operator.wallet-features=zksync-era" network_mode: "" + stop_timeout: 300 + setup: > + Before first run, initialize the storage node config: + docker run --rm -e SETUP="true" + --mount type=bind,source="",destination=/app/identity + --mount type=bind,source="",destination=/app/config + --name storagenode storjlabs/storagenode:latest notes: > - No signup or auth token required (permissionless since mid-2025). + Image is storjlabs/storagenode (not storj/storagenode). + A one-time setup step (SETUP=true) must be run before deploying the main container. Identity must be pre-generated using the identity binary (proof-of-work to difficulty 36, takes hours). Use spinning disks for storage. Port 28967 TCP+UDP must be forwarded. - Local dashboard available at port 14002. + Dashboard at port 14002 — use your server's LAN IP (not localhost) for the collector API URL. requirements: residential_ip: false From 865bb8b2d5b1cbdc99788d7bccbb3cb5f0d0beab Mon Sep 17 00:00:00 2001 From: GeiserX <9169332+GeiserX@users.noreply.github.com> Date: Sat, 18 Apr 2026 22:09:08 +0200 Subject: [PATCH 2/2] fix: validate stop_timeout and add --user to Storj setup Address CodeRabbit review: - Add _parse_stop_timeout() to safely handle malformed values - Add --user $(id -u):$(id -g) to Storj setup command to preserve host ownership --- app/orchestrator.py | 13 +++++++++++-- services/storage/storj.yml | 1 + 2 files changed, 12 insertions(+), 2 deletions(-) diff --git a/app/orchestrator.py b/app/orchestrator.py index b45db50..afcf583 100644 --- a/app/orchestrator.py +++ b/app/orchestrator.py @@ -225,7 +225,7 @@ def deploy_service( "restart_policy": {"Name": "unless-stopped"}, } if stop_timeout: - run_kwargs["stop_timeout"] = int(stop_timeout) + run_kwargs["stop_timeout"] = _parse_stop_timeout(stop_timeout) container = client.containers.run(**run_kwargs) logger.info("Container %s started: %s", name, container.short_id) @@ -299,12 +299,21 @@ def deploy_raw( return container.id +def _parse_stop_timeout(value: Any) -> int: + """Parse a stop_timeout value, returning 30 on invalid input.""" + try: + timeout = int(value) + except (TypeError, ValueError): + return 30 + return timeout if timeout > 0 else 30 + + def _get_stop_timeout(slug: str) -> int: """Return the stop timeout from the catalog, or 30s default.""" if get_service: svc = get_service(slug) if svc: - return int(svc.get("docker", {}).get("stop_timeout", 30)) + return _parse_stop_timeout(svc.get("docker", {}).get("stop_timeout")) return 30 diff --git a/services/storage/storj.yml b/services/storage/storj.yml index 9582218..0c6f794 100644 --- a/services/storage/storj.yml +++ b/services/storage/storj.yml @@ -60,6 +60,7 @@ docker: setup: > Before first run, initialize the storage node config: docker run --rm -e SETUP="true" + --user $(id -u):$(id -g) --mount type=bind,source="",destination=/app/identity --mount type=bind,source="",destination=/app/config --name storagenode storjlabs/storagenode:latest