Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 6 additions & 1 deletion ctl
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ set -euo pipefail

ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
COMPOSE_FILE="$ROOT/docker-compose.yml"
COMPOSE_ISOLATED_FILE="$ROOT/docker-compose.isolated.yml"
CONTAINER_NAME="docker-git-api"
API_PORT="${DOCKER_GIT_API_PORT:-3334}"
API_HOST="${DOCKER_GIT_API_BIND_HOST:-127.0.0.1}"
Expand Down Expand Up @@ -54,7 +55,11 @@ USAGE
}

compose() {
"${DOCKER_CMD[@]}" compose -f "$COMPOSE_FILE" "$@"
local compose_args=(-f "$COMPOSE_FILE")
if [[ "${DOCKER_GIT_DOCKER_RUNTIME:-host}" == "isolated" ]]; then
compose_args+=(-f "$COMPOSE_ISOLATED_FILE")
fi
"${DOCKER_CMD[@]}" compose "${compose_args[@]}" "$@"
}

compute_controller_revision() {
Expand Down
8 changes: 8 additions & 0 deletions docker-compose.api.isolated.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
services:
api:
privileged: ${DOCKER_GIT_CONTROLLER_PRIVILEGED:-true}
environment:
DOCKER_GIT_PROJECT_DOCKER_HOST: ${DOCKER_GIT_PROJECT_DOCKER_HOST:-tcp://host.docker.internal:2375}
volumes: !override
- docker_git_projects:${DOCKER_GIT_PROJECTS_ROOT:-/home/dev/.docker-git}
- docker_git_docker_data:/var/lib/docker
7 changes: 4 additions & 3 deletions docker-compose.api.yml
Original file line number Diff line number Diff line change
Expand Up @@ -11,11 +11,11 @@ services:
environment:
DOCKER_GIT_API_PORT: ${DOCKER_GIT_API_PORT:-3334}
DOCKER_GIT_CONTROLLER_REV: ${DOCKER_GIT_CONTROLLER_REV:-unknown}
DOCKER_GIT_DOCKER_RUNTIME: ${DOCKER_GIT_DOCKER_RUNTIME:-isolated}
DOCKER_GIT_DOCKER_RUNTIME: ${DOCKER_GIT_DOCKER_RUNTIME:-host}
DOCKER_HOST: ${DOCKER_GIT_CONTROLLER_DOCKER_HOST:-unix:///var/run/docker.sock}
Comment thread
coderabbitai[bot] marked this conversation as resolved.
DOCKER_GIT_DOCKERD_TCP_HOST: ${DOCKER_GIT_DOCKERD_TCP_HOST:-tcp://0.0.0.0:2375}
DOCKER_GIT_DOCKERD_DEFAULT_CGROUPNS_MODE: ${DOCKER_GIT_DOCKERD_DEFAULT_CGROUPNS_MODE:-host}
DOCKER_GIT_PROJECT_DOCKER_HOST: ${DOCKER_GIT_PROJECT_DOCKER_HOST:-tcp://host.docker.internal:2375}
DOCKER_GIT_PROJECT_DOCKER_HOST: ${DOCKER_GIT_PROJECT_DOCKER_HOST:-}
DOCKER_GIT_PROJECT_SSH_BIND_HOST: ${DOCKER_GIT_PROJECT_SSH_BIND_HOST:-0.0.0.0}
DOCKER_GIT_PROJECTS_ROOT: ${DOCKER_GIT_PROJECTS_ROOT:-/home/dev/.docker-git}
DOCKER_GIT_PROJECTS_ROOT_VOLUME: ${DOCKER_GIT_PROJECTS_ROOT_VOLUME:-docker-git-projects}
Expand All @@ -36,7 +36,8 @@ services:
volumes:
- docker_git_projects:${DOCKER_GIT_PROJECTS_ROOT:-/home/dev/.docker-git}
- docker_git_docker_data:/var/lib/docker
privileged: true
- /var/run/docker.sock:/var/run/docker.sock
privileged: ${DOCKER_GIT_CONTROLLER_PRIVILEGED:-false}
cgroup: host
init: true
restart: unless-stopped
Expand Down
8 changes: 8 additions & 0 deletions docker-compose.isolated.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
services:
api:
privileged: ${DOCKER_GIT_CONTROLLER_PRIVILEGED:-true}
environment:
DOCKER_GIT_PROJECT_DOCKER_HOST: ${DOCKER_GIT_PROJECT_DOCKER_HOST:-tcp://host.docker.internal:2375}
volumes: !override
- docker_git_projects:${DOCKER_GIT_PROJECTS_ROOT:-/home/dev/.docker-git}
- docker_git_docker_data:/var/lib/docker
7 changes: 4 additions & 3 deletions docker-compose.yml
Original file line number Diff line number Diff line change
Expand Up @@ -11,11 +11,11 @@ services:
environment:
DOCKER_GIT_API_PORT: ${DOCKER_GIT_API_PORT:-3334}
DOCKER_GIT_CONTROLLER_REV: ${DOCKER_GIT_CONTROLLER_REV:-unknown}
DOCKER_GIT_DOCKER_RUNTIME: ${DOCKER_GIT_DOCKER_RUNTIME:-isolated}
DOCKER_GIT_DOCKER_RUNTIME: ${DOCKER_GIT_DOCKER_RUNTIME:-host}
DOCKER_HOST: ${DOCKER_GIT_CONTROLLER_DOCKER_HOST:-unix:///var/run/docker.sock}
DOCKER_GIT_DOCKERD_TCP_HOST: ${DOCKER_GIT_DOCKERD_TCP_HOST:-tcp://0.0.0.0:2375}
DOCKER_GIT_DOCKERD_DEFAULT_CGROUPNS_MODE: ${DOCKER_GIT_DOCKERD_DEFAULT_CGROUPNS_MODE:-host}
DOCKER_GIT_PROJECT_DOCKER_HOST: ${DOCKER_GIT_PROJECT_DOCKER_HOST:-tcp://host.docker.internal:2375}
DOCKER_GIT_PROJECT_DOCKER_HOST: ${DOCKER_GIT_PROJECT_DOCKER_HOST:-}
DOCKER_GIT_PROJECT_SSH_BIND_HOST: ${DOCKER_GIT_PROJECT_SSH_BIND_HOST:-0.0.0.0}
DOCKER_GIT_PROJECTS_ROOT: ${DOCKER_GIT_PROJECTS_ROOT:-/home/dev/.docker-git}
DOCKER_GIT_PROJECTS_ROOT_VOLUME: ${DOCKER_GIT_PROJECTS_ROOT_VOLUME:-docker-git-projects}
Expand All @@ -36,7 +36,8 @@ services:
volumes:
- docker_git_projects:${DOCKER_GIT_PROJECTS_ROOT:-/home/dev/.docker-git}
- docker_git_docker_data:/var/lib/docker
privileged: true
- /var/run/docker.sock:/var/run/docker.sock
privileged: ${DOCKER_GIT_CONTROLLER_PRIVILEGED:-false}
cgroup: host
init: true
restart: unless-stopped
Expand Down
2 changes: 1 addition & 1 deletion packages/api/Dockerfile
Original file line number Diff line number Diff line change
Expand Up @@ -150,7 +150,7 @@ LABEL io.prover-coder-ai.docker-git.controller-build-skiller=$DOCKER_GIT_CONTROL
ENV DOCKER_GIT_CONTROLLER_REV=$DOCKER_GIT_CONTROLLER_REV
ENV DOCKER_GIT_CONTROLLER_BUILD_SKILLER=$DOCKER_GIT_CONTROLLER_BUILD_SKILLER
ENV DOCKER_GIT_API_PORT=3334
ENV DOCKER_GIT_DOCKER_RUNTIME=isolated
ENV DOCKER_GIT_DOCKER_RUNTIME=host
ENV DOCKER_HOST=unix:///var/run/docker.sock
EXPOSE 3334

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

## Runtime contract: host-Docker-backed

`docker-git` is host-Docker-backed, not isolated. The controller container
created from this package binds the host socket
`docker-git` is host-Docker-backed by default. The primary controller
container created from this package binds the host socket
(`/var/run/docker.sock:/var/run/docker.sock`, see `docker-compose.yml`) and
uses it to spawn per-project containers. There is no Docker-in-Docker
runtime; the daemon is always the host's daemon.
uses it to spawn per-project containers. `DOCKER_GIT_DOCKER_RUNTIME=isolated`
is an opt-in fallback for environments that explicitly require an embedded
controller daemon. In isolated mode, start the controller through the host CLI
or include `docker-compose.isolated.yml`; that overlay removes the host socket
bind and defaults project containers to the embedded daemon endpoint
`tcp://host.docker.internal:2375`.

Security note: binding `/var/run/docker.sock` gives the controller container
root-equivalent control over the host Docker daemon, including the ability to
create containers and mount host paths. This is an intended trade-off for the
host-backed architecture; run the controller only in trusted environments and
review the threat model before exposing the API.

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

Isolated fallback:

```bash
DOCKER_GIT_DOCKER_RUNTIME=isolated \
docker compose -f docker-compose.yml -f docker-compose.isolated.yml up -d --build
./ctl health
```

Default port mapping:

- host: `127.0.0.1:3334`
Expand All @@ -61,12 +79,13 @@ Optional env:

- `DOCKER_GIT_API_BIND_HOST` (default: `127.0.0.1`)
- `DOCKER_GIT_API_PORT` (default: `3334`)
- `DOCKER_GIT_DOCKER_RUNTIME` (default: `isolated`; starts a managed Docker daemon in `docker-git-api`)
- `DOCKER_GIT_DOCKER_RUNTIME` (default: `host`; set to `isolated` as an optional fallback to use an embedded controller daemon)
- `DOCKER_GIT_CONTROLLER_DOCKER_HOST` (default: `unix:///var/run/docker.sock`; socket path inside the controller)
- `DOCKER_GIT_CONTROLLER_PRIVILEGED` (default: `false` in host mode; isolated overlays default it to `true` for the embedded Docker daemon)
- `DOCKER_GIT_DOCKERD_TCP_HOST` (default: `tcp://0.0.0.0:2375`; reachable only inside Docker networks unless explicitly published)
- `DOCKER_GIT_DOCKERD_DEFAULT_CGROUPNS_MODE` (default: `host`; keeps nested project containers compatible with cgroup v2 DinD)
- `DOCKER_GIT_PROJECT_DOCKER_HOST` (default: `tcp://host.docker.internal:2375`; lets project containers use the isolated daemon)
- `DOCKER_GIT_PROJECT_SSH_BIND_HOST` (default: `0.0.0.0` in controller mode; project SSH binds inside the isolated controller runtime)
- `DOCKER_GIT_PROJECT_DOCKER_HOST` (default: empty in host mode; isolated mode defaults to `tcp://host.docker.internal:2375`)
- `DOCKER_GIT_PROJECT_SSH_BIND_HOST` (default: `0.0.0.0`)
- `DOCKER_GIT_PROJECTS_ROOT` (container path, default: `/home/dev/.docker-git`)
- `DOCKER_GIT_PROJECTS_ROOT_VOLUME` (Docker volume name for controller state, default: `docker-git-projects`)
- `DOCKER_GIT_FEDERATION_PUBLIC_ORIGIN` (optional public ActivityPub origin)
Expand Down
6 changes: 5 additions & 1 deletion packages/api/scripts/start-controller.sh
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
#!/usr/bin/env bash
set -euo pipefail

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

Expand All @@ -15,6 +15,10 @@ cleanup() {
trap cleanup EXIT INT TERM

if [[ "$runtime" == "isolated" ]]; then
if [[ -z "${DOCKER_GIT_PROJECT_DOCKER_HOST:-}" ]]; then
export DOCKER_GIT_PROJECT_DOCKER_HOST="tcp://host.docker.internal:2375"
fi

if [[ "$docker_host" != unix://* ]]; then
echo "DOCKER_GIT_DOCKER_RUNTIME=isolated requires a unix:// DOCKER_HOST for the managed controller daemon." >&2
exit 1
Expand Down
33 changes: 31 additions & 2 deletions packages/app/scripts/print-controller-revision.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,13 @@
import { NodeContext, NodeRuntime } from "@effect/platform-node"
import { Effect, pipe } from "effect"

import {
controllerRevisionForMode,
parseControllerBuildSkillerMode,
parseControllerGpuMode
} from "../src/docker-git/controller-compose.ts"
import { computeLocalControllerRevision } from "../src/docker-git/controller-revision.ts"
import { parseControllerDockerRuntime } from "../src/docker-git/controller-runtime.ts"

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

const readControllerRevisionModes = (): Effect.Effect<{
readonly buildSkillerMode: "0" | "1"
readonly dockerRuntime: "host" | "isolated"
readonly gpuMode: "none" | "all"
}, Error> => {
const gpuMode = parseControllerGpuMode(process.env["DOCKER_GIT_CONTROLLER_GPU"])
const buildSkillerMode = parseControllerBuildSkillerMode(process.env["DOCKER_GIT_CONTROLLER_BUILD_SKILLER"])
const dockerRuntime = parseControllerDockerRuntime(process.env["DOCKER_GIT_DOCKER_RUNTIME"])
if (gpuMode === null || buildSkillerMode === null || dockerRuntime === null) {
return Effect.fail(new Error("Invalid controller revision mode environment."))
}
return Effect.succeed({ buildSkillerMode, dockerRuntime, gpuMode })
}

const main = pipe(
readComposePath(),
Effect.flatMap((composePath) => computeLocalControllerRevision(composePath)),
Effect.all({
composePath: readComposePath(),
modes: readControllerRevisionModes()
}),
Effect.flatMap(({ composePath, modes }) =>
computeLocalControllerRevision(composePath).pipe(
Effect.map((revision) =>
controllerRevisionForMode(revision, modes.gpuMode, modes.buildSkillerMode, modes.dockerRuntime)
)
)
),
Effect.tap((revision) => Effect.sync(() => process.stdout.write(`${revision}\n`))),
Effect.asVoid,
Effect.provide(NodeContext.layer)
Expand Down
67 changes: 67 additions & 0 deletions packages/app/src/docker-git/controller-compose-runtime.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,67 @@
import type { PlatformError } from "@effect/platform/Error"
import * as FileSystem from "@effect/platform/FileSystem"
import * as Path from "@effect/platform/Path"
import { Effect } from "effect"

import {
type ControllerDockerRuntime,
controllerDockerRuntimeEnvKey,
parseControllerDockerRuntime
} from "./controller-runtime.js"
import { type ControllerBootstrapError, controllerBootstrapError } from "./host-errors.js"

const mapComposePathError = (error: PlatformError): ControllerBootstrapError =>
controllerBootstrapError(`Failed to resolve docker-compose.yml path.\nDetails: ${String(error)}`)

export const loadControllerDockerRuntime = (): Effect.Effect<ControllerDockerRuntime, ControllerBootstrapError> => {
const raw = process.env[controllerDockerRuntimeEnvKey]
const parsed = parseControllerDockerRuntime(raw)
if (parsed !== null) {
return Effect.succeed(parsed)
}
return Effect.fail(
controllerBootstrapError(
`${controllerDockerRuntimeEnvKey} must be unset or one of: host, isolated. Received: ${raw ?? ""}`
)
)
}

const isolatedOverlayFileName = (composeFileName: string): Effect.Effect<string, ControllerBootstrapError> => {
if (composeFileName.endsWith(".yaml")) {
return Effect.succeed(`${composeFileName.slice(0, -".yaml".length)}.isolated.yaml`)
}
if (composeFileName.endsWith(".yml")) {
return Effect.succeed(`${composeFileName.slice(0, -".yml".length)}.isolated.yml`)
}
return Effect.fail(
controllerBootstrapError(
`${controllerDockerRuntimeEnvKey}=isolated requires a .yml or .yaml compose file. Received: ${composeFileName}`
)
)
}

export const resolveControllerRuntimeOverlayPath = (
composePath: string,
dockerRuntime: ControllerDockerRuntime
): Effect.Effect<string | null, ControllerBootstrapError, FileSystem.FileSystem | Path.Path> =>
dockerRuntime === "host"
? Effect.succeed(null)
: Effect.gen(function*(_) {
const fs = yield* _(FileSystem.FileSystem)
const path = yield* _(Path.Path)
const overlayFileName = yield* _(isolatedOverlayFileName(path.basename(composePath)))
const runtimeOverlayPath = path.join(
path.dirname(composePath),
overlayFileName
)
const exists = yield* _(fs.exists(runtimeOverlayPath).pipe(Effect.mapError(mapComposePathError)))
return exists
? runtimeOverlayPath
: yield* _(
Effect.fail(
controllerBootstrapError(
`${controllerDockerRuntimeEnvKey}=isolated requires ${runtimeOverlayPath}, but it was not found.`
)
)
)
})
Loading
Loading