Skip to content

Commit 580c99f

Browse files
fix(shell): prepare host-backed controller builds (#351)
* fix(shell): prepare host-backed controller builds - Default controller runtime to host Docker with socket mount - Initialize the pinned Skiller submodule before controller revision/build - Include Skiller inputs in controller revision hashing * test(shell): cover controller compose preparation * fix(shell): support containerized remote Docker controller startup * fix(shell): preserve remote and isolated controller bootstrap * fix(shell): make isolated controller fallback self-contained * fix(shell): address controller runtime review comments --------- Co-authored-by: skulidropek <66840575+skulidropek@users.noreply.github.com>
1 parent 142352c commit 580c99f

28 files changed

Lines changed: 1221 additions & 179 deletions

ctl

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@ set -euo pipefail
1313

1414
ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
1515
COMPOSE_FILE="$ROOT/docker-compose.yml"
16+
COMPOSE_ISOLATED_FILE="$ROOT/docker-compose.isolated.yml"
1617
CONTAINER_NAME="docker-git-api"
1718
API_PORT="${DOCKER_GIT_API_PORT:-3334}"
1819
API_HOST="${DOCKER_GIT_API_BIND_HOST:-127.0.0.1}"
@@ -54,7 +55,11 @@ USAGE
5455
}
5556

5657
compose() {
57-
"${DOCKER_CMD[@]}" compose -f "$COMPOSE_FILE" "$@"
58+
local compose_args=(-f "$COMPOSE_FILE")
59+
if [[ "${DOCKER_GIT_DOCKER_RUNTIME:-host}" == "isolated" ]]; then
60+
compose_args+=(-f "$COMPOSE_ISOLATED_FILE")
61+
fi
62+
"${DOCKER_CMD[@]}" compose "${compose_args[@]}" "$@"
5863
}
5964

6065
compute_controller_revision() {

docker-compose.api.isolated.yml

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
services:
2+
api:
3+
privileged: ${DOCKER_GIT_CONTROLLER_PRIVILEGED:-true}
4+
environment:
5+
DOCKER_GIT_PROJECT_DOCKER_HOST: ${DOCKER_GIT_PROJECT_DOCKER_HOST:-tcp://host.docker.internal:2375}
6+
volumes: !override
7+
- docker_git_projects:${DOCKER_GIT_PROJECTS_ROOT:-/home/dev/.docker-git}
8+
- docker_git_docker_data:/var/lib/docker

docker-compose.api.yml

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -11,11 +11,11 @@ services:
1111
environment:
1212
DOCKER_GIT_API_PORT: ${DOCKER_GIT_API_PORT:-3334}
1313
DOCKER_GIT_CONTROLLER_REV: ${DOCKER_GIT_CONTROLLER_REV:-unknown}
14-
DOCKER_GIT_DOCKER_RUNTIME: ${DOCKER_GIT_DOCKER_RUNTIME:-isolated}
14+
DOCKER_GIT_DOCKER_RUNTIME: ${DOCKER_GIT_DOCKER_RUNTIME:-host}
1515
DOCKER_HOST: ${DOCKER_GIT_CONTROLLER_DOCKER_HOST:-unix:///var/run/docker.sock}
1616
DOCKER_GIT_DOCKERD_TCP_HOST: ${DOCKER_GIT_DOCKERD_TCP_HOST:-tcp://0.0.0.0:2375}
1717
DOCKER_GIT_DOCKERD_DEFAULT_CGROUPNS_MODE: ${DOCKER_GIT_DOCKERD_DEFAULT_CGROUPNS_MODE:-host}
18-
DOCKER_GIT_PROJECT_DOCKER_HOST: ${DOCKER_GIT_PROJECT_DOCKER_HOST:-tcp://host.docker.internal:2375}
18+
DOCKER_GIT_PROJECT_DOCKER_HOST: ${DOCKER_GIT_PROJECT_DOCKER_HOST:-}
1919
DOCKER_GIT_PROJECT_SSH_BIND_HOST: ${DOCKER_GIT_PROJECT_SSH_BIND_HOST:-0.0.0.0}
2020
DOCKER_GIT_PROJECTS_ROOT: ${DOCKER_GIT_PROJECTS_ROOT:-/home/dev/.docker-git}
2121
DOCKER_GIT_PROJECTS_ROOT_VOLUME: ${DOCKER_GIT_PROJECTS_ROOT_VOLUME:-docker-git-projects}
@@ -36,7 +36,8 @@ services:
3636
volumes:
3737
- docker_git_projects:${DOCKER_GIT_PROJECTS_ROOT:-/home/dev/.docker-git}
3838
- docker_git_docker_data:/var/lib/docker
39-
privileged: true
39+
- /var/run/docker.sock:/var/run/docker.sock
40+
privileged: ${DOCKER_GIT_CONTROLLER_PRIVILEGED:-false}
4041
cgroup: host
4142
init: true
4243
restart: unless-stopped

docker-compose.isolated.yml

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
services:
2+
api:
3+
privileged: ${DOCKER_GIT_CONTROLLER_PRIVILEGED:-true}
4+
environment:
5+
DOCKER_GIT_PROJECT_DOCKER_HOST: ${DOCKER_GIT_PROJECT_DOCKER_HOST:-tcp://host.docker.internal:2375}
6+
volumes: !override
7+
- docker_git_projects:${DOCKER_GIT_PROJECTS_ROOT:-/home/dev/.docker-git}
8+
- docker_git_docker_data:/var/lib/docker

docker-compose.yml

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -11,11 +11,11 @@ services:
1111
environment:
1212
DOCKER_GIT_API_PORT: ${DOCKER_GIT_API_PORT:-3334}
1313
DOCKER_GIT_CONTROLLER_REV: ${DOCKER_GIT_CONTROLLER_REV:-unknown}
14-
DOCKER_GIT_DOCKER_RUNTIME: ${DOCKER_GIT_DOCKER_RUNTIME:-isolated}
14+
DOCKER_GIT_DOCKER_RUNTIME: ${DOCKER_GIT_DOCKER_RUNTIME:-host}
1515
DOCKER_HOST: ${DOCKER_GIT_CONTROLLER_DOCKER_HOST:-unix:///var/run/docker.sock}
1616
DOCKER_GIT_DOCKERD_TCP_HOST: ${DOCKER_GIT_DOCKERD_TCP_HOST:-tcp://0.0.0.0:2375}
1717
DOCKER_GIT_DOCKERD_DEFAULT_CGROUPNS_MODE: ${DOCKER_GIT_DOCKERD_DEFAULT_CGROUPNS_MODE:-host}
18-
DOCKER_GIT_PROJECT_DOCKER_HOST: ${DOCKER_GIT_PROJECT_DOCKER_HOST:-tcp://host.docker.internal:2375}
18+
DOCKER_GIT_PROJECT_DOCKER_HOST: ${DOCKER_GIT_PROJECT_DOCKER_HOST:-}
1919
DOCKER_GIT_PROJECT_SSH_BIND_HOST: ${DOCKER_GIT_PROJECT_SSH_BIND_HOST:-0.0.0.0}
2020
DOCKER_GIT_PROJECTS_ROOT: ${DOCKER_GIT_PROJECTS_ROOT:-/home/dev/.docker-git}
2121
DOCKER_GIT_PROJECTS_ROOT_VOLUME: ${DOCKER_GIT_PROJECTS_ROOT_VOLUME:-docker-git-projects}
@@ -36,7 +36,8 @@ services:
3636
volumes:
3737
- docker_git_projects:${DOCKER_GIT_PROJECTS_ROOT:-/home/dev/.docker-git}
3838
- docker_git_docker_data:/var/lib/docker
39-
privileged: true
39+
- /var/run/docker.sock:/var/run/docker.sock
40+
privileged: ${DOCKER_GIT_CONTROLLER_PRIVILEGED:-false}
4041
cgroup: host
4142
init: true
4243
restart: unless-stopped

packages/api/Dockerfile

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -150,7 +150,7 @@ LABEL io.prover-coder-ai.docker-git.controller-build-skiller=$DOCKER_GIT_CONTROL
150150
ENV DOCKER_GIT_CONTROLLER_REV=$DOCKER_GIT_CONTROLLER_REV
151151
ENV DOCKER_GIT_CONTROLLER_BUILD_SKILLER=$DOCKER_GIT_CONTROLLER_BUILD_SKILLER
152152
ENV DOCKER_GIT_API_PORT=3334
153-
ENV DOCKER_GIT_DOCKER_RUNTIME=isolated
153+
ENV DOCKER_GIT_DOCKER_RUNTIME=host
154154
ENV DOCKER_HOST=unix:///var/run/docker.sock
155155
EXPOSE 3334
156156

packages/api/README.md

Lines changed: 29 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -5,17 +5,27 @@ HTTP API for docker-git orchestration (projects, agents, logs/events, federation
55
This is now the intended controller plane:
66
- the API runs inside `docker-git-api`
77
- `.docker-git` state lives in the Docker volume `docker-git-projects`
8-
- the API starts an isolated Docker daemon inside the controller by default
9-
- child project containers no longer depend on host bind mounts for bootstrap auth/env
10-
- the host `/var/run/docker.sock` is not mounted into the controller or project containers
8+
- the API uses the host Docker daemon by default via `/var/run/docker.sock`
9+
- child project containers use host-backed Docker unless an explicit
10+
`DOCKER_GIT_PROJECT_DOCKER_HOST` is provided
1111

1212
## Runtime contract: host-Docker-backed
1313

14-
`docker-git` is host-Docker-backed, not isolated. The controller container
15-
created from this package binds the host socket
14+
`docker-git` is host-Docker-backed by default. The primary controller
15+
container created from this package binds the host socket
1616
(`/var/run/docker.sock:/var/run/docker.sock`, see `docker-compose.yml`) and
17-
uses it to spawn per-project containers. There is no Docker-in-Docker
18-
runtime; the daemon is always the host's daemon.
17+
uses it to spawn per-project containers. `DOCKER_GIT_DOCKER_RUNTIME=isolated`
18+
is an opt-in fallback for environments that explicitly require an embedded
19+
controller daemon. In isolated mode, start the controller through the host CLI
20+
or include `docker-compose.isolated.yml`; that overlay removes the host socket
21+
bind and defaults project containers to the embedded daemon endpoint
22+
`tcp://host.docker.internal:2375`.
23+
24+
Security note: binding `/var/run/docker.sock` gives the controller container
25+
root-equivalent control over the host Docker daemon, including the ability to
26+
create containers and mount host paths. This is an intended trade-off for the
27+
host-backed architecture; run the controller only in trusted environments and
28+
review the threat model before exposing the API.
1929

2030
The host CLI (`packages/app`) also talks to that same daemon directly when
2131
it bootstraps the controller. Three failure modes look identical at first
@@ -52,6 +62,14 @@ docker compose up -d --build
5262
./ctl health
5363
```
5464

65+
Isolated fallback:
66+
67+
```bash
68+
DOCKER_GIT_DOCKER_RUNTIME=isolated \
69+
docker compose -f docker-compose.yml -f docker-compose.isolated.yml up -d --build
70+
./ctl health
71+
```
72+
5573
Default port mapping:
5674

5775
- host: `127.0.0.1:3334`
@@ -61,12 +79,13 @@ Optional env:
6179

6280
- `DOCKER_GIT_API_BIND_HOST` (default: `127.0.0.1`)
6381
- `DOCKER_GIT_API_PORT` (default: `3334`)
64-
- `DOCKER_GIT_DOCKER_RUNTIME` (default: `isolated`; starts a managed Docker daemon in `docker-git-api`)
82+
- `DOCKER_GIT_DOCKER_RUNTIME` (default: `host`; set to `isolated` as an optional fallback to use an embedded controller daemon)
6583
- `DOCKER_GIT_CONTROLLER_DOCKER_HOST` (default: `unix:///var/run/docker.sock`; socket path inside the controller)
84+
- `DOCKER_GIT_CONTROLLER_PRIVILEGED` (default: `false` in host mode; isolated overlays default it to `true` for the embedded Docker daemon)
6685
- `DOCKER_GIT_DOCKERD_TCP_HOST` (default: `tcp://0.0.0.0:2375`; reachable only inside Docker networks unless explicitly published)
6786
- `DOCKER_GIT_DOCKERD_DEFAULT_CGROUPNS_MODE` (default: `host`; keeps nested project containers compatible with cgroup v2 DinD)
68-
- `DOCKER_GIT_PROJECT_DOCKER_HOST` (default: `tcp://host.docker.internal:2375`; lets project containers use the isolated daemon)
69-
- `DOCKER_GIT_PROJECT_SSH_BIND_HOST` (default: `0.0.0.0` in controller mode; project SSH binds inside the isolated controller runtime)
87+
- `DOCKER_GIT_PROJECT_DOCKER_HOST` (default: empty in host mode; isolated mode defaults to `tcp://host.docker.internal:2375`)
88+
- `DOCKER_GIT_PROJECT_SSH_BIND_HOST` (default: `0.0.0.0`)
7089
- `DOCKER_GIT_PROJECTS_ROOT` (container path, default: `/home/dev/.docker-git`)
7190
- `DOCKER_GIT_PROJECTS_ROOT_VOLUME` (Docker volume name for controller state, default: `docker-git-projects`)
7291
- `DOCKER_GIT_FEDERATION_PUBLIC_ORIGIN` (optional public ActivityPub origin)

packages/api/scripts/start-controller.sh

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
#!/usr/bin/env bash
22
set -euo pipefail
33

4-
runtime="${DOCKER_GIT_DOCKER_RUNTIME:-isolated}"
4+
runtime="${DOCKER_GIT_DOCKER_RUNTIME:-host}"
55
docker_host="${DOCKER_HOST:-unix:///var/run/docker.sock}"
66
dockerd_pid=""
77

@@ -15,6 +15,10 @@ cleanup() {
1515
trap cleanup EXIT INT TERM
1616

1717
if [[ "$runtime" == "isolated" ]]; then
18+
if [[ -z "${DOCKER_GIT_PROJECT_DOCKER_HOST:-}" ]]; then
19+
export DOCKER_GIT_PROJECT_DOCKER_HOST="tcp://host.docker.internal:2375"
20+
fi
21+
1822
if [[ "$docker_host" != unix://* ]]; then
1923
echo "DOCKER_GIT_DOCKER_RUNTIME=isolated requires a unix:// DOCKER_HOST for the managed controller daemon." >&2
2024
exit 1

packages/app/scripts/print-controller-revision.ts

Lines changed: 31 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,13 @@
33
import { NodeContext, NodeRuntime } from "@effect/platform-node"
44
import { Effect, pipe } from "effect"
55

6+
import {
7+
controllerRevisionForMode,
8+
parseControllerBuildSkillerMode,
9+
parseControllerGpuMode
10+
} from "../src/docker-git/controller-compose.ts"
611
import { computeLocalControllerRevision } from "../src/docker-git/controller-revision.ts"
12+
import { parseControllerDockerRuntime } from "../src/docker-git/controller-runtime.ts"
713

814
// CHANGE: expose controller revision computation as a reusable Bun script for shell tooling
915
// WHY: ctl must inject the same deterministic controller revision into docker compose as the host CLI bootstrap path
@@ -25,9 +31,32 @@ const readComposePath = (): Effect.Effect<string, Error> => {
2531
: Effect.fail(new Error(usage))
2632
}
2733

34+
const readControllerRevisionModes = (): Effect.Effect<{
35+
readonly buildSkillerMode: "0" | "1"
36+
readonly dockerRuntime: "host" | "isolated"
37+
readonly gpuMode: "none" | "all"
38+
}, Error> => {
39+
const gpuMode = parseControllerGpuMode(process.env["DOCKER_GIT_CONTROLLER_GPU"])
40+
const buildSkillerMode = parseControllerBuildSkillerMode(process.env["DOCKER_GIT_CONTROLLER_BUILD_SKILLER"])
41+
const dockerRuntime = parseControllerDockerRuntime(process.env["DOCKER_GIT_DOCKER_RUNTIME"])
42+
if (gpuMode === null || buildSkillerMode === null || dockerRuntime === null) {
43+
return Effect.fail(new Error("Invalid controller revision mode environment."))
44+
}
45+
return Effect.succeed({ buildSkillerMode, dockerRuntime, gpuMode })
46+
}
47+
2848
const main = pipe(
29-
readComposePath(),
30-
Effect.flatMap((composePath) => computeLocalControllerRevision(composePath)),
49+
Effect.all({
50+
composePath: readComposePath(),
51+
modes: readControllerRevisionModes()
52+
}),
53+
Effect.flatMap(({ composePath, modes }) =>
54+
computeLocalControllerRevision(composePath).pipe(
55+
Effect.map((revision) =>
56+
controllerRevisionForMode(revision, modes.gpuMode, modes.buildSkillerMode, modes.dockerRuntime)
57+
)
58+
)
59+
),
3160
Effect.tap((revision) => Effect.sync(() => process.stdout.write(`${revision}\n`))),
3261
Effect.asVoid,
3362
Effect.provide(NodeContext.layer)
Lines changed: 67 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,67 @@
1+
import type { PlatformError } from "@effect/platform/Error"
2+
import * as FileSystem from "@effect/platform/FileSystem"
3+
import * as Path from "@effect/platform/Path"
4+
import { Effect } from "effect"
5+
6+
import {
7+
type ControllerDockerRuntime,
8+
controllerDockerRuntimeEnvKey,
9+
parseControllerDockerRuntime
10+
} from "./controller-runtime.js"
11+
import { type ControllerBootstrapError, controllerBootstrapError } from "./host-errors.js"
12+
13+
const mapComposePathError = (error: PlatformError): ControllerBootstrapError =>
14+
controllerBootstrapError(`Failed to resolve docker-compose.yml path.\nDetails: ${String(error)}`)
15+
16+
export const loadControllerDockerRuntime = (): Effect.Effect<ControllerDockerRuntime, ControllerBootstrapError> => {
17+
const raw = process.env[controllerDockerRuntimeEnvKey]
18+
const parsed = parseControllerDockerRuntime(raw)
19+
if (parsed !== null) {
20+
return Effect.succeed(parsed)
21+
}
22+
return Effect.fail(
23+
controllerBootstrapError(
24+
`${controllerDockerRuntimeEnvKey} must be unset or one of: host, isolated. Received: ${raw ?? ""}`
25+
)
26+
)
27+
}
28+
29+
const isolatedOverlayFileName = (composeFileName: string): Effect.Effect<string, ControllerBootstrapError> => {
30+
if (composeFileName.endsWith(".yaml")) {
31+
return Effect.succeed(`${composeFileName.slice(0, -".yaml".length)}.isolated.yaml`)
32+
}
33+
if (composeFileName.endsWith(".yml")) {
34+
return Effect.succeed(`${composeFileName.slice(0, -".yml".length)}.isolated.yml`)
35+
}
36+
return Effect.fail(
37+
controllerBootstrapError(
38+
`${controllerDockerRuntimeEnvKey}=isolated requires a .yml or .yaml compose file. Received: ${composeFileName}`
39+
)
40+
)
41+
}
42+
43+
export const resolveControllerRuntimeOverlayPath = (
44+
composePath: string,
45+
dockerRuntime: ControllerDockerRuntime
46+
): Effect.Effect<string | null, ControllerBootstrapError, FileSystem.FileSystem | Path.Path> =>
47+
dockerRuntime === "host"
48+
? Effect.succeed(null)
49+
: Effect.gen(function*(_) {
50+
const fs = yield* _(FileSystem.FileSystem)
51+
const path = yield* _(Path.Path)
52+
const overlayFileName = yield* _(isolatedOverlayFileName(path.basename(composePath)))
53+
const runtimeOverlayPath = path.join(
54+
path.dirname(composePath),
55+
overlayFileName
56+
)
57+
const exists = yield* _(fs.exists(runtimeOverlayPath).pipe(Effect.mapError(mapComposePathError)))
58+
return exists
59+
? runtimeOverlayPath
60+
: yield* _(
61+
Effect.fail(
62+
controllerBootstrapError(
63+
`${controllerDockerRuntimeEnvKey}=isolated requires ${runtimeOverlayPath}, but it was not found.`
64+
)
65+
)
66+
)
67+
})

0 commit comments

Comments
 (0)