docker/cli · error
service
Error message
service %s: %w
What it means
Wrap error produced in the Services() conversion loop when convertServiceSecrets fails for a service (service.go:42-44). It prefixes the inner error with the service name so you know which service's secret references were invalid. The inner error is typically 'undefined secret %q' (from service.go:255) or a failure from servicecli.ParseSecrets contacting the Swarm API.
Solutions
- Add the missing secret definition under the top-level `secrets:` key in the compose file.
- If the secret lives outside compose, declare it with `external: true` so compose does not try to create it.
- Check the inner error (unwrapped via errors.Is/As) to distinguish 'undefined secret' from a Swarm API failure and fix accordingly.
Example fix
// before
services:
web:
image: nginx
secrets:
- tls-cert
// after
services:
web:
image: nginx
secrets:
- tls-cert
secrets:
tls-cert:
file: ./certs/tls.pem Defensive patterns
Strategy: try-catch
Validate before calling
// Pre-check that every secret a service uses is declared before calling convert.Services.
func validateSecrets(cfg *composetypes.Config) error {
for _, svc := range cfg.Services {
for _, s := range svc.Secrets {
if _, ok := cfg.Secrets[s.Source]; !ok {
return fmt.Errorf("service %s: undefined secret %q", svc.Name, s.Source)
}
}
}
return nil
} Try / catch
result, err := convert.Services(ctx, namespace, cfg, apiClient)
if err != nil {
var inner error
if errors.As(err, &inner) {
// log svcName + inner; surface a user-friendly message
}
return err
} Prevention
- Declare every secret referenced by a service in the top-level secrets block.
- Run a linter (docker compose config) before stack deploy to catch undefined secrets early.
- Use external: true for Swarm-managed secrets so compose does not try to create them.
When it happens
Trigger: Calling convert.Services(...) on a compose Config whose service references a secret not declared in the top-level secrets map, or when the Swarm API client cannot resolve a referenced secret by ID/name.
Common situations: A service lists `secrets: [mysecret]` but the top-level `secrets:` block omits it; renamed a secret in one place but not both; the secret object does not exist in Swarm and is not marked external; API connectivity issues during ParseSecrets.
Related errors
AI-assisted analysis of docker/cli@4f84911bfe (2026-08-07).
Data as JSON: /api/errors/520fe84a03399f06.
Report an issue: GitHub.
Appendix: source
Thrown at cli/compose/convert/service.go:44
const (
defaultNetwork = "default"
// LabelImage is the label used to store image name provided in the compose file
LabelImage = "com.docker.stack.image"
)
// Services from compose-file types to engine API types
func Services(
ctx context.Context,
namespace Namespace,
config *composetypes.Config,
apiClient client.APIClient,
) (map[string]swarm.ServiceSpec, error) {
result := make(map[string]swarm.ServiceSpec)
for _, service := range config.Services {
secrets, err := convertServiceSecrets(ctx, apiClient, namespace, service.Secrets, config.Secrets)
if err != nil {
return nil, fmt.Errorf("service %s: %w", service.Name, err)
}
configs, err := convertServiceConfigObjs(ctx, apiClient, namespace, service, config.Configs)
if err != nil {
return nil, fmt.Errorf("service %s: %w", service.Name, err)
}
serviceSpec, err := Service(namespace, service, config.Networks, config.Volumes, secrets, configs)
if err != nil {
return nil, fmt.Errorf("service %s: %w", service.Name, err)
}
result[service.Name] = serviceSpec
}
return result, nil
}
// Service converts a ServiceConfig into a swarm ServiceSpec
func Service(View on GitHub (pinned to 4f84911bfe)