Skip to content

Docker & downloads

fghj never shells out to docker compose, even though it deliberately mimics Compose in a few places for tooling compatibility — every container it runs carries Compose-style project/service labels, so Docker Desktop’s own UI (and docker ps/compose ls) groups a run’s containers together as if they were a real Compose stack, even though fghjd talks to the Docker Engine API directly.

A few consequences of talking to the Engine API directly rather than shelling out to docker build/docker run:

  • Building requires handing the Docker daemon a tar stream of the build context itself — there’s no “build from this directory” call at the API level.
  • Running a container means constructing the create/start sequence manually, and — because a failed start can leave a container behind in a Created state — fghjd always best-effort force-removes on any error in that sequence, so a retried run never trips over a dangling half-created container blocking the same name.
  • Ports published to the host are always bound to 127.0.0.1 specifically, never 0.0.0.0 — containers are never meant to be reachable from outside your machine.
  • Inspecting a container’s status deliberately returns “not found” rather than an error for a nonexistent container, so callers — the reconciler, route-building, a run’s liveness check — can all treat “doesn’t exist” as ordinary control flow, not an error path.

A volume declaration is either a bind mount or a named volume, never both — see .fghj.yaml reference: Volumes for how to write each. Both shapes end up in the exact same place at the Docker layer: a bind mount is "host/path:container/path", a named volume is "volume-name:container/path" — Docker itself tells them apart by whether the left side contains a /, so both are just formatted strings in the same list handed to the container create call.

A bind mount’s host path, if relative, resolves against the live workspace checkout fghjd actually built the image from. It is not sandboxed to that repo: an absolute path, or one that walks up with .., passes straight through to Docker unchanged. That’s a deliberate choice, not an oversight — it’s what lets a service bind-mount a sibling repo’s checkout directly, the same way Docker Compose would.

A named volume’s real Docker name is derived, never the literal string you write — the same principle as a node’s *.fghj.internal domain (see Node identity & domains). It’s built from the volume’s own name, folding in the run id unless scope is "stable". Because the derivation is keyed by that author-chosen name rather than by any node id, two unrelated nodes — two services, or a service and a backing dependency — that declare the same name and scope land on the same derived value and transparently share one Docker volume. This is also why a kind: shared-backing reference automatically gets the backing dependency’s persisted data for free: there’s only ever one container that owns it, no matter how many services reference it.

Volumes are never deleted by fghj — stopping a run tears down its containers and network only, which is what lets a volume survive a restart in the first place. A "run"-scoped preview run that’s stopped and never restarted leaves its volume behind, with no cleanup command yet.

Fetching the last N lines of a container’s logs backs the log drawer’s “load logs” button. A continuous, never-terminating log stream backs the live-follow view — opened automatically over Server-Sent Events whenever the logs panel is showing a running container. See UI architecture.

Background download jobs: don’t block the request

Section titled “Background download jobs: don’t block the request”

Cloning a repo can take anywhere from under a second to tens of seconds, and a “pull all” might clone several repos in sequence — far too slow for a synchronous HTTP handler. Each clone job runs on its own OS thread and streams progress into a growing log buffer the UI polls instead:

  • Idempotent starts: kicking off a download that’s already running under the same key just returns its current progress instead of starting a second concurrent clone of the same thing.
  • Independent tracking: a single-node download, a whole-graph “pull all”, and a flow-scoped pull are all tracked independently, so more than one can be running — or polled — at once without one clobbering another’s status.
  • Log streaming: both stdout and stderr are captured (Git’s own progress meter writes to stderr, not stdout) into the same log, with carriage returns translated into newlines so a terminal progress meter reads sensibly inside the UI instead of as a wall of overwritten lines.
  • Ordering: the job list shown in the UI is ordered “job first started,” most-recent-first — not by any incidental key sort order.

Pull all: a fixpoint, because cloning can reveal more repos

Section titled “Pull all: a fixpoint, because cloning can reveal more repos”

A single graph resolution can only see stub nodes for dependencies declared by repos already on disk — a not-yet-cloned repo’s own dependencies are, by definition, unknown until it’s cloned. “Pull all” therefore loops: resolve the graph, clone every currently missing (and, if a flow is selected, flow-reachable) service node, then resolve again — repeating until a pass finds nothing left to clone. Each pass either makes progress or terminates, so the loop can’t spin forever short of a clone that never actually lands the repo at its expected path.

The download path’s own git clone runs as the real workspace owner rather than as root, using the identity-borrowing mechanism described in Persistence & workspace store, as defense in depth against a clone subprocess hanging on an unanswerable interactive prompt. (The now-removed branch-override build path — see Run lifecycle & registry) — used to share this same privilege drop for its own mirror clone; that code no longer exists.)