# Phase 3: Docker Client **Status:** :white_check_mark: Complete **Parent plan:** [PLAN.md](./PLAN.md) **Domain:** backend ## Objective Implement the Docker Engine API wrapper for container lifecycle management — pull images, inspect, create/start/stop/remove containers, and manage networks. ## Tasks - [x] Task 1: Create Docker client wrapper with socket connection (`/var/run/docker.sock`) - [x] Task 2: Implement `PullImage(ctx, image, tag, authConfig)` — pull with optional registry auth - [x] Task 3: Implement `InspectImage(ctx, image)` — extract EXPOSE ports, HEALTHCHECK, labels - [x] Task 4: Implement `CreateContainer(ctx, config)` — create with name, image, env, ports, network, labels - [x] Task 5: Implement `StartContainer(ctx, containerID)`, `StopContainer(ctx, containerID, timeout)`, `RemoveContainer(ctx, containerID, force)` - [x] Task 6: Implement `RestartContainer(ctx, containerID, timeout)` - [x] Task 7: Implement `ListContainers(ctx, filters)` — filter by labels to find managed containers - [x] Task 8: Implement `EnsureNetwork(ctx, networkName)` — create network if not exists - [x] Task 9: Implement `ConnectNetwork(ctx, networkID, containerID)` — attach container to network - [x] Task 10: Add docker-watcher labels to all managed containers (`docker-watcher.project`, `docker-watcher.stage`, `docker-watcher.instance-id`) ## Files to Modify/Create - `internal/docker/client.go` — Docker client wrapper, connection setup - `internal/docker/container.go` — container lifecycle operations - `internal/docker/image.go` — pull and inspect operations - `internal/docker/network.go` — network management ## Acceptance Criteria - Client connects to Docker socket - Pull handles both public and authenticated registries - Image inspection extracts port, healthcheck, and label metadata - Container creation applies all config (env, ports, network, labels) - All operations return meaningful errors - Managed containers are identifiable via labels ## Notes - Use `github.com/docker/docker/client` SDK - Container names should be deterministic: `dw-{project}-{stage}-{tag-sanitized}` - All containers should be on the shared network (e.g., `staging-net`) - Port mapping: container's EXPOSE port → random host port (Docker auto-assigns) - Auth config for private registries will come from the store (encrypted tokens) ## Review Checklist - [x] All tasks completed - [x] Proper context propagation for cancellation - [x] Resource cleanup (close client, remove failed containers) - [x] No hardcoded values - [x] Error messages include container/image identifiers ## Handoff to Next Phase ### Exported API surface (`internal/docker` package) **Client lifecycle:** - `docker.New() (*Client, error)` — creates client with env-based config and API version negotiation - `(*Client).Close() error` — releases resources - `(*Client).Ping(ctx) error` — checks daemon connectivity **Image operations (`image.go`):** - `(*Client).PullImage(ctx, imageRef, tag, authConfig) error` — pulls image; authConfig is base64-encoded JSON (use `EncodeRegistryAuth` helper) - `(*Client).InspectImage(ctx, imageRef) (ImageInfo, error)` — returns `ImageInfo{ExposedPorts, Healthcheck, Labels}` - `docker.EncodeRegistryAuth(username, password, serverAddress) (string, error)` — builds auth payload for `PullImage` **Container operations (`container.go`):** - `(*Client).CreateContainer(ctx, ContainerConfig) (containerID string, error)` — creates container with labels, env, ports, network - `(*Client).StartContainer(ctx, containerID) error` - `(*Client).StopContainer(ctx, containerID, timeoutSeconds) error` - `(*Client).RemoveContainer(ctx, containerID, force) error` - `(*Client).RestartContainer(ctx, containerID, timeoutSeconds) error` - `(*Client).ListContainers(ctx, labelFilters) ([]ManagedContainer, error)` — always scoped to docker-watcher labels - `(*Client).InspectContainerPort(ctx, containerID, containerPort) (uint16, error)` — gets auto-assigned host port - `docker.ContainerName(project, stage, tag) string` — deterministic name: `dw-{project}-{stage}-{tag-sanitized}` **Network operations (`network.go`):** - `(*Client).EnsureNetwork(ctx, networkName) (networkID string, error)` — idempotent create-if-not-exists - `(*Client).ConnectNetwork(ctx, networkID, containerID) error` **Label constants:** - `docker.LabelProject` = `"docker-watcher.project"` - `docker.LabelStage` = `"docker-watcher.stage"` - `docker.LabelInstanceID` = `"docker-watcher.instance-id"` **Key types:** - `docker.ContainerConfig` — input for `CreateContainer` (Name, Image, Env, ExposedPorts, NetworkName, NetworkID, Labels, Project, Stage, InstanceID) - `docker.ImageInfo` — output of `InspectImage` (ExposedPorts, Healthcheck, Labels) - `docker.ManagedContainer` — output of `ListContainers` (ID, Name, Image, Status, State, Project, Stage, InstanceID, Ports) ### Dependencies added - `github.com/docker/docker v27.5.1+incompatible` - `github.com/docker/go-connections v0.5.0` - Run `go mod tidy` to resolve transitive dependencies before building ### Conventions maintained - `context.Context` as first parameter on all methods - Errors wrapped with `fmt.Errorf("context: %w", err)` - Package-level constants for labels - Immutable patterns (new maps created rather than mutating input)