docker/cli · error

service is already attached to network

Error message

service is already attached to network %s

What it means

Raised by docker service update when --network-add targets a network the service is already attached to. The updateNetworks function resolves the network name to an ID and checks it against existingNetworks; a duplicate would create a redundant attachment, so the CLI rejects it rather than silently no-op.

Solutions

  1. Verify the service's current networks with `docker service inspect <service>` (look at Spec.TaskTemplate.Networks) and skip networks already attached.
  2. Use a different network name/id, or omit the --network-add for the duplicate.
  3. Make your automation idempotent: diff desired vs. existing networks before invoking update.
  4. If the intent is to change attachment options (aliases), remove and re-add with --network-rm then --network-add.

Example fix

// before
docker service update --network-add mynet myservice   # myservice already on mynet
// after
docker service inspect myservice --format '{{json .Spec.TaskTemplate.Networks}}'
# then only add networks not already present
Defensive patterns

Strategy: validation

Validate before calling

// Before calling docker service update --network-add, check current attachments
// Pseudocode using the daemon API:
func desiredNetworksToAdd(existing []string, desired []string) []string {
    have := map[string]struct{}{}
    for _, n := range existing {
        have[n] = struct{}{}
    }
    var add []string
    for _, d := range desired {
        if _, ok := have[d]; !ok {
            add = append(add, d)
        }
    }
    return add // pass these (and only these) to --network-add
}

Try / catch

// Treat the duplicate as a no-op success in automation
err := svcUpdate(ctx, addNetworks)
if err != nil && strings.Contains(err.Error(), "already attached to network") {
    // idempotent: the desired state is already satisfied
    return nil
}

Prevention

When it happens

Trigger: Running `docker service update --network-add <name-or-id> <service>` where <name-or-id> resolves (via resolveNetworkID) to the same ID as a network already present in spec.TaskTemplate.Networks. The check is at update.go:1342 inside the flagNetworkAdd branch.

Common situations: Running the same --network-add command twice (e.g. from an idempotent-but-not-reconciled deploy script); adding a network by an alias/driver-specific name that resolves to an already-attached ID; copy-pasting a network-add line during troubleshooting.

Related errors


AI-assisted analysis of docker/cli@4f84911bfe (2026-08-07). Data as JSON: /api/errors/8757afe109cab046. Report an issue: GitHub.

Appendix: source

Thrown at cli/command/service/update.go:1343

	for _, nw := range specNetworks {
		if _, exists := idsToRemove[nw.Target]; exists {
			continue
		}

		newNetworks = append(newNetworks, nw)
		existingNetworks[nw.Target] = struct{}{}
	}

	if flags.Changed(flagNetworkAdd) {
		values := flags.Lookup(flagNetworkAdd).Value.(*opts.NetworkOpt)
		networks := convertNetworks(*values)
		for _, nw := range networks {
			nwID, err := resolveNetworkID(ctx, apiClient, nw.Target)
			if err != nil {
				return err
			}
			if _, exists := existingNetworks[nwID]; exists {
				return fmt.Errorf("service is already attached to network %s", nw.Target)
			}
			nw.Target = nwID
			newNetworks = append(newNetworks, nw)
			existingNetworks[nw.Target] = struct{}{}
		}
	}

	sort.Slice(newNetworks, func(i, j int) bool {
		return newNetworks[i].Target < newNetworks[j].Target
	})

	spec.TaskTemplate.Networks = newNetworks
	return nil
}

// updateCredSpecConfig updates the value of the credential spec Config field
// to the config ID if the credential spec has changed. it mutates the passed
// spec. it does not handle the case where the credential spec specifies a

View on GitHub (pinned to 4f84911bfe)