Files
tiny-forge/plans/docker-watcher-core/phase-3-docker-client.md
T
alexei.dolgolyov 389ed5aff8 feat(docker-watcher): phases 3+4 - Docker client & NPM client
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.
2026-03-27 21:08:57 +03:00

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 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

  • 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 (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)