Docker & downloads
Building without Compose
Section titled “Building without Compose”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
Createdstate —fghjdalways 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.1specifically, never0.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.
Volumes: two shapes, one Docker primitive
Section titled “Volumes: two shapes, one Docker primitive”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.
Two log-reading modes
Section titled “Two log-reading modes”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.
Privilege drop for clones
Section titled “Privilege drop for clones”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.)