389ed5aff8
Phase 3: Docker Engine API wrapper — pull/inspect images, container lifecycle (create/start/stop/remove/restart), network management, label-based container tracking, deterministic naming. Phase 4: Nginx Proxy Manager API client — JWT auth with auto-refresh, CRUD for proxy hosts, domain-based host lookup.
5.2 KiB
5.2 KiB
Phase 3: Docker Client
Status: ✅ Complete Parent plan: 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
- Task 1: Create Docker client wrapper with socket connection (
/var/run/docker.sock) - Task 2: Implement
PullImage(ctx, image, tag, authConfig)— pull with optional registry auth - Task 3: Implement
InspectImage(ctx, image)— extract EXPOSE ports, HEALTHCHECK, labels - Task 4: Implement
CreateContainer(ctx, config)— create with name, image, env, ports, network, labels - Task 5: Implement
StartContainer(ctx, containerID),StopContainer(ctx, containerID, timeout),RemoveContainer(ctx, containerID, force) - Task 6: Implement
RestartContainer(ctx, containerID, timeout) - Task 7: Implement
ListContainers(ctx, filters)— filter by labels to find managed containers - Task 8: Implement
EnsureNetwork(ctx, networkName)— create network if not exists - Task 9: Implement
ConnectNetwork(ctx, networkID, containerID)— attach container to network - 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 setupinternal/docker/container.go— container lifecycle operationsinternal/docker/image.go— pull and inspect operationsinternal/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/clientSDK - 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
- All tasks completed
- Proper context propagation for cancellation
- Resource cleanup (close client, remove failed containers)
- No hardcoded values
- 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 (useEncodeRegistryAuthhelper)(*Client).InspectImage(ctx, imageRef) (ImageInfo, error)— returnsImageInfo{ExposedPorts, Healthcheck, Labels}docker.EncodeRegistryAuth(username, password, serverAddress) (string, error)— builds auth payload forPullImage
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 portdocker.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 forCreateContainer(Name, Image, Env, ExposedPorts, NetworkName, NetworkID, Labels, Project, Stage, InstanceID)docker.ImageInfo— output ofInspectImage(ExposedPorts, Healthcheck, Labels)docker.ManagedContainer— output ofListContainers(ID, Name, Image, Status, State, Project, Stage, InstanceID, Ports)
Dependencies added
github.com/docker/docker v27.5.1+incompatiblegithub.com/docker/go-connections v0.5.0- Run
go mod tidyto resolve transitive dependencies before building
Conventions maintained
context.Contextas 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)