PortReeve coordinates a project-defined independently runnable stack for a canonical stack root. The root is the exact real path of an existing directory and need not be a Git repository; it may contain multiple child repositories. Stack identity is project plus canonical stack root, and only one activation may be current for a root at a time. Registered roots may be siblings but may not overlap as equal, ancestor, or descendant paths. The project remains the authority for how its processes and containers are launched. PortReeve stores coordination state; it does not store commands, environment secrets, Compose files, or health-check logic.
The checked-in definition is portreeve.stack.json at the stack root:
{
"version": 1,
"project": "caregiver",
"components": {
"api": {
"docker": { "service": "api" },
"endpoints": {
"http": {
"allocation": { "preferredPort": 8080 },
"docker": { "containerPort": 8080 }
}
}
},
"website": {
"endpoints": {
"http": { "allocation": { "preferredPort": 3000 } }
},
"dependencies": {
"backend": { "component": "api", "endpoint": "http" }
}
}
}
}The repository includes a complete mixed process and Docker launcher example that shows how one project-owned launcher consumes the returned leases and addresses.
The schema is strict. Unknown fields are rejected. Component and endpoint names are
stable logical identities. An omitted dependency endpoint means default. Endpoint
defaults are TCP, published, and required. Use publish: false for an endpoint that
PortReeve should understand but should not assign a host port; a dependency cannot
target an unpublished endpoint. preferredPort permits later fallback allocation;
exactPort does not. They are mutually exclusive.
Docker metadata is declarative coordination metadata. An endpoint with a Docker container port must belong to a component with a Docker service. Applying a definition never runs Docker; it supplies the facts later used by activation evidence.
Run portreeve stacks apply from the stack root or any directory beneath it. Implicit
selection walks upward from the current real directory to the nearest
portreeve.stack.json; child Git repository boundaries do not stop the search. Use
--stack-root <path> to select the standard file at another root, or --file <path> to
select an explicit file whose parent is the stack root. The two selectors are mutually
exclusive, and apply never reconstructs a missing definition from PortReeve's database.
The CLI and JavaScript client send the same definition over the private socket. PortReeve
normalizes defaults, canonicalizes object-key order, hashes the JSON, and records the
SHA-256 value as an immutable definition revision. Reapplying equivalent content is
idempotent. A changed definition creates or reuses its revision and updates the stack's
current revision atomically. While an activation is starting, confirmed, or
degraded, an equivalent re-apply remains idempotent but changed content is refused.
Applying links every published endpoint to a sticky canonical claim. A prior standalone
service claim is adopted as component plus endpoint default without changing its
assigned port. An existing conflicting exact assignment or an ephemeral matching claim
refuses the whole apply operation.
Inspect the definition together with its latest generation, activation, and fresh provider evidence for that registered stack:
portreeve stacks status --jsonStatus uses an explicit --stack-root when supplied. Otherwise it first uses the
nearest enclosing definition file. If that file is missing, it asks PortReeve for the
single registered stack root enclosing the current real directory, which keeps a known
stack inspectable while its project-owned definition is repaired. If neither source
resolves a stack, status reports an ordinary state difference.
Use stacks show STACK_ID when only the registered definition is needed.
The desktop Stacks tab can create or edit the fixed portreeve.stack.json through
structured fields. New drafts infer only the selected root's basename as the editable
project name; users add every component, endpoint, Docker hint, allocation policy, and
dependency explicitly. The preview is the exact concise JSON submitted for saving.
Directory selection, file inspection, schema validation, exclusive creation, atomic replacement, and exact-byte conflict detection remain in the trusted Electron main process. The sandboxed renderer receives an opaque document ID and a display basename, not a full path or general filesystem/socket authority. A successful write survives an apply failure and can be retried while the visible draft still matches the saved bytes. See Desktop application for conflict, invalid-file, navigation, and trust details.
Desktop Save and Apply has the same coordination boundary as CLI apply: it never prepares a generation, starts providers, or replaces project launcher orchestration.
Preparation creates or reuses one immutable allocation generation containing every published endpoint:
portreeve stacks prepare STACK_ID --jsonPreparation is durable and does not start a lease deadline. Exact ports fail without a partial generation. Preferred ports fall back through the configured automatic range. If fresh listener evidence shows that an assigned port was taken by another owner, the old generation becomes stale and the launcher must prepare again before deriving environment variables or Compose overrides.
When the launcher is ready to start providers, begin one activation:
portreeve stacks begin GENERATION_ID --jsonAdd --docker-component api for each component the launcher will place in Docker.
Omitted components remain process-backed, so an activation may mix both kinds. Docker
selection is per activation and does not change logical endpoint identity.
Beginning atomically creates pending leases for all non-skipped endpoints. Only one
activation may be live for a canonical stack root. The JSON response contains private
lease tokens; keep them out of source control and process arguments. Use a mode-0600
JSON file with stacks renew --leases-file, or the JavaScript client, to renew the
whole pending batch during startup.
Providers bind their assigned ports and confirm individually. Process endpoints use
fresh process-tree evidence. Each Docker lease includes the exact required labels;
after the launcher starts the labeled container and publishes
127.0.0.1:HOST_PORT:CONTAINER_PORT, it confirms with:
portreeve stacks confirm-docker ACTIVATION_ID \
--lease-file .portreeve/private/api-http.json \
--container-id CONTAINER_IDThe container ID is only a lookup key. PortReeve freshly verifies running state, every
stack/component/revision/generation/activation/endpoint label, the exact publication,
and the declared container port. A listener found by lsof is retained as corroborating
evidence, but its absence does not reject a Linux Docker Engine publication implemented
through kernel NAT. PortReeve never applies process lineage to Docker's backend.
Required endpoints must confirm. Optional endpoints may be named with
--skip-endpoint component.endpoint; after all required endpoints confirm, a skipped or
failed optional endpoint makes the activation degraded. A required dependency on an
optional provider endpoint must promote it with --required-endpoint component.endpoint
for that activation. When a component or endpoint name itself contains a dot, pass a
JSON object such as
--required-endpoint '{"component":"api.v2","endpoint":"http.internal"}'.
Activation states are starting, confirmed, degraded, failed, lost, and
ended.
Endpoint states are leased, confirmed, skipped, failed, and released.
Abandoning or expiring a required endpoint fails the activation and cancels every
remaining unconfirmed lease as one batch. A still-valid allocation generation may be
reused for the next attempt.
stacks generation and stacks activation provide token-free inspection. A replacement
launcher can run stacks reconcile ACTIVATION_ID after a launcher crash. PortReeve
freshly checks every confirmed process or Docker provider. A surviving or unobservable
provider keeps the activation live; only conclusive absence of every provider marks it
lost, releases its stored run evidence, and permits another activation to reuse a
still-valid generation. Stored launcher and provider PIDs are never liveness authority.
After the launcher has stopped its providers, stacks end uses the same process and
Docker evidence evaluator. It refuses active or unobservable providers and unresolved
listeners, and never signals a process or stops a container. A lost activation may be
ended explicitly for lifecycle clarity.
If Docker is absent or inaccessible, health omits docker-evidence-v1; process-only
activations continue normally. Inventory marks fresh Docker publications as
docker-managed. Both normal reclamation and unsafe-evict return
launcher-action-required with the container IDs and send no process signal. The
trusted launcher stops the container and retries.
Stack pruning removes obsolete coordination state without reclaiming ports or stopping providers:
portreeve stacks prune --dry-run
portreeve stacks prune --yesThe default minimum age is seven days. Only an old stack whose canonical stack root is
missing can become a candidate. Dry-run also reports blockers such as pending leases or
activations, confirmed runs, live listeners, matching running containers, and
unavailable Docker evidence. A naked interactive command prints the plan and prompts;
noninteractive execution requires --yes, and --json is not consent.
Execution rechecks the stack root and all provider evidence. It skips a stack if the
root, listener, container, or database-owned live work reappears. Successful pruning
atomically removes the inactive definition, generations, activations, and associated
endpoint claims while retaining claim history and a final stack.pruned identity and
summary event.
Dependencies are address references, not startup-order edges. PortReeve resolves each consumer from one activation generation and returns two non-overlapping maps: its own published endpoints and its declared dependency aliases. It never reveals unrelated components. Circular address references are permitted because every address was prepared before activation.
portreeve stacks resolve ACTIVATION_ID --component website --jsonEach resolved endpoint contains the host publication plus a nullable Docker-network fact. The host fact is always the generation's loopback allocation. A Docker-network fact exists only when the checked-in provider component declares a Docker service and that endpoint declares a fixed container port. These are address facts, not application health or Docker ownership evidence.
A trusted launcher may render and atomically write a reduced endpoint document:
portreeve stacks snapshot ACTIVATION_ID \
--component website \
--gateway-host host.docker.internal \
--file /private/runtime/endpoints.jsonThe launcher chooses the platform gateway. PortReeve substitutes it for loopback while retaining the allocated host ports, but does not claim that gateway as an independently owned listener. The strict document contains only revision, generation, activation, consumer, and scoped TCP addresses. It excludes stack-root paths, claims, lease tokens, runs, process and Docker identifiers, the daemon socket, and mutation authority.
Distribute the file read-only instead of exposing PortReeve's socket. The official
JavaScript reader accepts an explicit path or PORTREEVE_ENDPOINTS_FILE and can reject
a document whose expected generation or activation no longer matches. This generic
snapshot capability does not provide Docker Sandbox orchestration or integration.