Skip to content

feat(workspace): remote shared backend — run the shared stack on a remote Docker host (spec 21) - #107

Merged
gustavobertoi merged 1 commit into
mainfrom
feat/remote-backend
Jul 1, 2026
Merged

feat(workspace): remote shared backend — run the shared stack on a remote Docker host (spec 21)#107
gustavobertoi merged 1 commit into
mainfrom
feat/remote-backend

Conversation

@gustavobertoi

Copy link
Copy Markdown
Contributor

What & why

Generalizes the shared stack (spec 21) so it can run on a remote Docker host — an SSH/TCP docker context or a DOCKER_HOST endpoint — instead of only the local daemon. The ref-counting, provisioning, and DNS/alias model are backend-agnostic; this only swaps where the containers run and how host-side consumers reach them. The local backend is the default and byte-for-byte unchanged.

Delivered (tested core)

  1. Config selector — a workspace backend: block (context: or host:), plus a machine-global default on the store config.yaml. Validated: a dockerhost scheme rule (ssh|tcp|unix|npipe|fd://) and a cross-field check that context and host are mutually exclusive. nil = local. (internal/config, internal/store)
  2. Docker seamdocker.Backend (ComposeEnv/Reachability/NewClient/RemoteReachable). The local backend is the zero value and defers to the existing NewClient (so context-name resolution / ledger keying is unchanged). A remote backend binds moby to the resolved endpoint (WithHost + version negotiation; a docker context endpoint is resolved via docker context inspect) and keys the ledger by the context name / host, so remote rows never bleed into the local counts — exactly like the WSL2 Desktop-vs-dockerd split. Compose gains ContextEnv, appended after the resolved-secret Env, pinning every compose verb to the endpoint via DOCKER_HOST/DOCKER_CONTEXT (never written to disk).
  3. Wiringcli/up.go buildUpDeps resolves the backend, builds the read-only client from it, keys state.Open by the backend context, and threads it through UpDeps.Backend into the shared-phase, project, and down composes. doctor targets the selected endpoint too.
  4. Reachability — a remote bridge is not host-routable, so no loopback provision port is published; host-side Postgres provisioning over a remote backend is guarded with a clear, actionable error (re-run with --no-provision, or provision from the remote host). Remote up of the non-provisioned shared stack (redis/minio, or postgres with no host-provisioned consumer) works.
  5. doctor — a backend.remote reachability probe (Ping + ServerVersion) with a remediation; only emitted for a remote backend (the local daemon is already covered by the existing preflight).

Scoped out (flagged follow-ups — this is a frontier spec)

  • Distributed lock — the local flock cannot serialize two developers on two machines against the same remote ledger. The DistLock (recommended: a Postgres advisory lock on the cluster DB) is not implemented; this pass is the single-user, local-flock, all-remote-topology sketch the spec calls ~3–4w.
  • Per-user tenant isolation on a shared cluster (u_<user>_<project> roles, REVOKE … FROM PUBLIC, a tenant ledger column).
  • Host-side provisioning over remote — needs an SSH local-forward/tunnel; guarded with an error rather than silently publishing an unreachable port.
  • Remote-authoritative ledger (local SQLite degrading to a cache).

These are documented in code doc-comments (internal/docker/backend.go) and gate on a daemon decision (Q-DAEMON).

How tested

mock docker.Client + a fake compose runner, no real remote:

  • internal/dockerIsRemote/Reachability/ComposeEnv/String; ContextEnv threads through the runner after secrets; local adds nothing; RemoteReachable ok/fail/nil-client.
  • internal/config — backend default-local, context, host, bad scheme, mutual exclusion, valid schemes.
  • internal/orchestrate — remote up pins every compose verb (shared + project) to DOCKER_CONTEXT; local leaks no backend env; the remote host-provision guard fails the shared phase with an actionable message.
  • internal/cli — the doctor backend.remote probe (reachable / unreachable / local-absent).

CGO_ENABLED=0 go build ./..., CGO_ENABLED=1 go test ./internal/..., gofmt -l (clean), go vet ./..., and a darwin/arm64 cross-build all pass. No new dependencies.

🤖 Generated with Claude Code

…mote Docker host (spec 21)

Generalize the shared stack to run on a remote Docker host (an SSH/TCP
`docker context` or a DOCKER_HOST endpoint) instead of only the local
daemon. Ref-counting, provisioning, and DNS/alias are backend-agnostic;
this swaps WHERE containers run. Default is local, byte-for-byte unchanged.

- internal/docker: a Backend seam (ComposeEnv/Reachability/NewClient/
  RemoteReachable). Local = zero value (unchanged). Remote binds moby to the
  resolved endpoint and keys the ledger by the context name/host so remote
  rows never bleed into local counts, like the WSL2 Desktop-vs-dockerd split.
- internal/docker: Compose gains ContextEnv, appended after the secret Env,
  pinning every compose verb to the endpoint (DOCKER_HOST/DOCKER_CONTEXT).
- internal/config: a workspace `backend:` selector (+ store global default);
  validated (dockerhost scheme; context XOR host).
- orchestrate/up + cli/up: thread the backend into the client, ledger key,
  and every compose driver. Remote host-side Postgres provisioning is guarded
  with a clear error (scoped follow-up — needs an SSH tunnel).
- doctor: a backend.remote reachability probe with remediation.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@gustavobertoi
gustavobertoi force-pushed the feat/remote-backend branch from 82a85f0 to 07f6010 Compare July 1, 2026 16:49
@gustavobertoi
gustavobertoi merged commit 79ed100 into main Jul 1, 2026
4 checks passed
@gustavobertoi
gustavobertoi deleted the feat/remote-backend branch July 1, 2026 17:03
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant