Skip to content

Commit f82fcfe

Browse files
committed
Fix deploy flow: build full spec from YAML, resolve ${VAR} in volumes
The deploy endpoint was only sending image + env to workers, missing ports, volumes, network_mode, cap_add, privileged, and command. Now builds the complete spec from the YAML definition. Volume paths now support ${VAR} substitution using env vars, so services like Storj can ask users for host paths via the deploy form. Also updates Storj: remove auth token requirement (eliminated mid-2025), add IDENTITY_DIR and STORAGE_DIR as user-input env vars, add UDP port for QUIC, update guide and signup URL.
1 parent 74ece2c commit f82fcfe

4 files changed

Lines changed: 107 additions & 31 deletions

File tree

app/main.py

Lines changed: 48 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -690,8 +690,54 @@ async def api_deploy(request: Request, slug: str, body: DeployRequest, worker_id
690690
image = docker_conf.get("image")
691691
if not image:
692692
raise HTTPException(status_code=400, detail=f"Service '{slug}' has no Docker image")
693-
spec = {"image": image, "env": body.env or {}, "hostname": body.hostname}
694-
result = await _proxy_worker_deploy(worker_id, slug, spec) # type: ignore[arg-type]
693+
694+
# Build full env: YAML defaults + {hostname} substitution + user overrides
695+
import re
696+
import socket
697+
698+
hn = body.hostname or socket.gethostname()
699+
env: dict[str, str] = {}
700+
for var in docker_conf.get("env", []):
701+
default = var.get("default", "")
702+
if default and "{hostname}" in str(default):
703+
default = str(default).replace("{hostname}", hn)
704+
env[var["key"]] = str(default)
705+
env.update(body.env or {})
706+
707+
# Ports
708+
ports: dict[str, int] = {}
709+
for mapping in docker_conf.get("ports", []):
710+
if ":" in str(mapping):
711+
parts = str(mapping).split(":")
712+
ports[parts[0]] = int(parts[1].split("/")[0]) if "/" in parts[1] else int(parts[1])
713+
714+
# Volumes: resolve ${VAR} in host paths using env
715+
volumes: dict[str, dict[str, str]] = {}
716+
for mapping in docker_conf.get("volumes", []):
717+
if ":" in str(mapping):
718+
parts = str(mapping).split(":")
719+
host_path = re.sub(r"\$\{(\w+)\}", lambda m: env.get(m.group(1), m.group(0)), parts[0])
720+
container_path = parts[1]
721+
mode = parts[2] if len(parts) > 2 else "rw"
722+
volumes[host_path] = {"bind": container_path, "mode": mode}
723+
724+
spec: dict[str, Any] = {
725+
"image": image,
726+
"env": env,
727+
"hostname": body.hostname,
728+
"ports": ports,
729+
"volumes": volumes,
730+
"network_mode": docker_conf.get("network_mode") or None,
731+
"cap_add": docker_conf.get("cap_add") or None,
732+
"privileged": docker_conf.get("privileged", False),
733+
}
734+
735+
# Command: resolve ${VAR} placeholders
736+
raw_command = docker_conf.get("command") or None
737+
if raw_command:
738+
spec["command"] = re.sub(r"\$\{(\w+)\}", lambda m: env.get(m.group(1), m.group(0)), raw_command)
739+
740+
result = await _proxy_worker_deploy(worker_id, slug, spec)
695741
container_id = result.get("container_id", "remote")
696742
await database.save_deployment(slug=slug, container_id=container_id)
697743
await database.record_health_event(slug, "start", f"deployed to worker {worker_id}")

app/orchestrator.py

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -163,12 +163,16 @@ def deploy_service(
163163
container_port, host_port = str(mapping).split(":", 1)
164164
ports[container_port] = int(host_port)
165165

166-
# Volumes: list of "host:container" strings
166+
# Volumes: list of "host:container" strings — resolve ${VAR} in host paths
167167
volumes: dict[str, dict[str, str]] = {}
168168
for mapping in docker_conf.get("volumes", []):
169169
if ":" in str(mapping):
170170
parts = str(mapping).split(":")
171-
host_path = parts[0]
171+
host_path = re.sub(
172+
r"\$\{(\w+)\}",
173+
lambda m: env.get(m.group(1), m.group(0)),
174+
parts[0],
175+
)
172176
container_path = parts[1]
173177
mode = parts[2] if len(parts) > 2 else "rw"
174178
volumes[host_path] = {"bind": container_path, "mode": mode}

docs/guides/storj.md

Lines changed: 27 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,7 @@ Storj is a decentralized cloud storage network where you earn by renting out you
1717
| Payout frequency | Monthly |
1818
| Payment methods | Crypto |
1919

20-
> ~$1.50/TB stored + $2/TB egress + $2.74/TB audit/repair. Earnings scale with disk allocated and node age. First 9 months have held-back escrow. Port 28967 must be forwarded.
20+
> ~$1.50/TB stored + $2/TB egress + $2.74/TB audit/repair. Earnings scale with disk allocated and node age. First 9 months have held-back escrow. Port 28967 (TCP+UDP) must be forwarded.
2121
2222
## Requirements
2323

@@ -31,9 +31,11 @@ Storj is a decentralized cloud storage network where you earn by renting out you
3131

3232
## Setup Instructions
3333

34-
### 1. Create an account
34+
### 1. No account needed
3535

36-
Sign up at [Storj](https://www.storj.io/node). No referral program available.
36+
Storj storage nodes are **permissionless** — no signup, no account, no auth token. You just run the software. The old authorization token requirement was removed in mid-2025.
37+
38+
Documentation has moved to [storj.dev/node](https://storj.dev/node/get-started/setup).
3739

3840
### 2. Create a node identity
3941

@@ -50,25 +52,27 @@ nohup ./identity create storagenode --identity-dir /path/to/identity/ > /tmp/sto
5052

5153
The process outputs progress like `Generated 50000 keys; best difficulty so far: 34`. It's done when it reaches difficulty 36 and exits. Files created: `ca.cert`, `ca.key`, `identity.cert`, `identity.key`.
5254

53-
### 3. Authorize the identity
54-
55-
After identity creation, authorize it with an auth token from the Storj dashboard:
56-
57-
```bash
58-
./identity authorize storagenode <auth-token> --identity-dir /path/to/identity/
59-
```
60-
61-
Verify: `identity.cert` should have 3 certificate entries, `ca.cert` should have 2.
62-
63-
### 4. Port forwarding
55+
### 3. Port forwarding
6456

6557
Forward these ports through your router to the server running the node:
66-
- **TCP 28967** — Storage node traffic (required)
58+
- **TCP+UDP 28967** — Storage node traffic (required, UDP for QUIC)
6759
- **TCP 14002** — Dashboard/monitoring (optional, local access only)
6860

61+
### 4. Get an ERC-20 wallet address
62+
63+
You need an Ethereum-compatible wallet address to receive STORJ token payouts. Any ERC-20 wallet works (MetaMask, Trust Wallet, Ledger, etc.). If you already have an Ethereum address from other DePIN services, you can reuse it. Storj also supports zkSync L2 payouts (same address, lower gas fees).
64+
6965
### 5. Deploy with CashPilot
7066

71-
In the CashPilot web UI, find **Storj** in the service catalog and click **Deploy**. Enter the required credentials and CashPilot will handle the rest.
67+
In the CashPilot web UI, find **Storj** in the service catalog and click **Deploy**. You'll be asked for:
68+
- **Wallet address** — your ERC-20 wallet for payouts
69+
- **Email** — for operator notifications
70+
- **External address** — your public IP or DDNS hostname with port (e.g. `84.54.27.229:28967`)
71+
- **Storage allocation** — how much disk space to offer (e.g. `2TB`)
72+
- **Identity path** — host directory where your identity files are stored
73+
- **Storage path** — host directory on a spinning disk (HDD) for storing data
74+
75+
CashPilot will handle the container creation with proper volume mounts.
7276

7377
## Docker Configuration
7478

@@ -81,12 +85,16 @@ In the CashPilot web UI, find **Storj** in the service catalog and click **Deplo
8185
|----------|-------|:--------:|:------:|-------------|
8286
| `WALLET` | Wallet address | Yes | No | ERC-20 wallet address for STORJ token payouts (or zkSync) |
8387
| `EMAIL` | Email | Yes | No | Email address for operator notifications |
84-
| `ADDRESS` | External address | Yes | No | External IP or DDNS hostname with port (e.g. mynode.ddns.net:28967) |
88+
| `ADDRESS` | External address | Yes | No | Public IP or DDNS hostname with port (e.g. mynode.ddns.net:28967) |
8589
| `STORAGE` | Storage allocation | Yes | No | Maximum disk space to allocate (e.g. 2TB) (default: `1TB`) |
90+
| `IDENTITY_DIR` | Identity directory | Yes | No | Host path where identity files are stored (ca.cert, identity.cert, etc.) |
91+
| `STORAGE_DIR` | Storage directory | Yes | No | Host path on a spinning disk (HDD) for Storj data storage |
8692

8793
### Important Notes
8894

95+
- **No signup required**: Storj nodes are permissionless since mid-2025. No auth token, no account creation.
8996
- **Escrow period**: First 9 months of operation have held-back escrow (75% of storage fees held, released gradually). This incentivizes long-term operation.
90-
- **One node per IP**: Storj recommends one node per public IP for optimal satellite allocation.
97+
- **One node per public IP**: Storj recommends one node per public IP for optimal satellite allocation. Nodes on the same /24 subnet share data allocation.
9198
- **Uptime matters**: Nodes with poor uptime get less data. Aim for 99.5%+ uptime.
92-
- **Disk selection**: Always use spinning disks (HDD). The data stored is cold storage — IOPS don't matter, capacity does.
99+
- **Disk selection**: Always use spinning disks (HDD). Storj data is cold storage — IOPS don't matter, capacity does. SSDs offer no advantage and cost more per TB.
100+
- **QUIC**: UDP port 28967 enables the QUIC protocol for faster transfers. Forward both TCP and UDP.

services/storage/storj.yml

Lines changed: 26 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -9,10 +9,11 @@ description: >
99
$1.50/TB stored per month plus $2/TB egress. Payments are in STORJ token or via
1010
zkSync L2. Requires at least 550GB of available disk space and a stable internet
1111
connection. One of the most mature and truly passive storage income services.
12+
No account or signup needed -- nodes are permissionless since mid-2025.
1213
short_description: Decentralized storage node - ~$1.50/TB stored + $2/TB egress
1314

1415
referral:
15-
signup_url: "https://www.storj.io/node"
16+
signup_url: "https://storj.dev/node/get-started/setup"
1617

1718
docker:
1819
image: storj/storagenode
@@ -22,7 +23,7 @@ docker:
2223
label: "Wallet address"
2324
required: true
2425
secret: false
25-
description: "ERC-20 wallet address for STORJ token payouts (or zkSync)"
26+
description: "ERC-20 wallet address for STORJ token payouts (e.g. 0x1234...abcd). Reuse any Ethereum wallet you already have"
2627
- key: EMAIL
2728
label: "Email"
2829
required: true
@@ -32,17 +33,34 @@ docker:
3233
label: "External address"
3334
required: true
3435
secret: false
35-
description: "External IP or DDNS hostname with port (e.g. mynode.ddns.net:28967)"
36+
description: "Your public IP or DDNS hostname with port (e.g. 84.54.27.229:28967). Port 28967 must be forwarded (TCP+UDP)"
3637
- key: STORAGE
3738
label: "Storage allocation"
3839
required: true
3940
secret: false
40-
description: "Maximum disk space to allocate (e.g. 2TB)"
41+
description: "Maximum disk space to allocate (e.g. 2TB). Use spinning disks (HDD), not SSD"
4142
default: "1TB"
42-
ports: ["28967:28967/tcp", "14002:14002"]
43-
volumes: ["/path/to/identity:/app/identity", "/path/to/storage:/app/config"]
43+
- key: IDENTITY_DIR
44+
label: "Identity directory"
45+
required: true
46+
secret: false
47+
description: "Host path where identity files are stored (must contain ca.cert, ca.key, identity.cert, identity.key)"
48+
- key: STORAGE_DIR
49+
label: "Storage directory"
50+
required: true
51+
secret: false
52+
description: "Host path on a spinning disk (HDD) for Storj data storage. Needs at least 550GB free"
53+
ports: ["28967:28967/tcp", "28967:28967/udp", "14002:14002"]
54+
volumes:
55+
- "${IDENTITY_DIR}:/app/identity"
56+
- "${STORAGE_DIR}:/app/config"
4457
command: ""
4558
network_mode: ""
59+
notes: >
60+
No signup or auth token required (permissionless since mid-2025).
61+
Identity must be pre-generated using the identity binary (proof-of-work to difficulty 36, takes hours).
62+
Use spinning disks for storage. Port 28967 TCP+UDP must be forwarded.
63+
Local dashboard available at port 14002.
4664
4765
requirements:
4866
residential_ip: false
@@ -67,10 +85,10 @@ earnings:
6785

6886
cashout:
6987
method: redirect
70-
dashboard_url: "https://www.storj.io/dcs/dashboard"
88+
dashboard_url: "http://localhost:14002"
7189
min_amount: 4.0
7290
currency: STORJ
73-
notes: "Payouts sent monthly in STORJ tokens to configured wallet address."
91+
notes: "Payouts sent monthly in STORJ tokens to configured wallet address. Dashboard at port 14002."
7492

7593
platforms: [docker, windows, linux]
7694

0 commit comments

Comments
 (0)