{"record":{"id":"9d4d2f590ad45404","repo":"docker/compose","slug":"invalid-service-q-must-specify-either-image-or-b-9d4d2f","errorCode":null,"errorMessage":"invalid service %q. Must specify either image or build","messagePattern":"invalid service %q\\. Must specify either image or build","errorType":"validation","errorClass":null,"httpStatus":null,"severity":"error","filePath":"pkg/compose/build.go","lineNumber":112,"sourceCode":"\n\tif len(serviceToBuild) == 0 {\n\t\treturn imageIDs, nil\n\t}\n\n\tbake, err := buildWithBake(s.dockerCli)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\tif bake {\n\t\treturn s.doBuildBake(ctx, project, serviceToBuild, options)\n\t}\n\treturn s.doBuildClassic(ctx, project, serviceToBuild, options)\n}\n\nfunc (s *composeService) ensureImagesExists(ctx context.Context, project *types.Project, buildOpts *api.BuildOptions, quietPull bool) error {\n\tfor name, service := range project.Services {\n\t\tif service.Provider == nil && service.Image == \"\" && service.Build == nil {\n\t\t\treturn fmt.Errorf(\"invalid service %q. Must specify either image or build\", name)\n\t\t}\n\t}\n\n\timages, pinnedDigests, err := s.getLocalImagesDigests(ctx, project)\n\tif err != nil {\n\t\treturn err\n\t}\n\n\terr = tracing.SpanWrapFunc(\"project/pull\", tracing.ProjectOptions(ctx, project),\n\t\tfunc(ctx context.Context) error {\n\t\t\treturn s.pullRequiredImages(ctx, project, images, quietPull)\n\t\t},\n\t)(ctx)\n\tif err != nil {\n\t\treturn err\n\t}\n\n\tif buildOpts != nil {","sourceCodeStart":94,"sourceCodeEnd":130,"githubUrl":"https://github.com/docker/compose/blob/ddc4b044b62e9f715212ea4143fa830fac76382f/pkg/compose/build.go#L94-L130","documentation":"Same contract as error 120 but enforced later, in composeService.ensureImagesExists (build/up pipeline): before pulling or building, compose verifies each service declares image, build, or provider. This is the runtime backstop for projects that reach the build phase without passing the earlier api.BuildOptions.Apply validation (e.g. paths that skip Apply).","triggerScenarios":"Calling the build or up API on a project whose service has no image/build/provider — commonly when the project was loaded with loaders that skip validation (WithConsistency off) or constructed programmatically and passed straight to compose.Service.Build/Up.","commonSituations":"Programmatic users of pkg/compose building a *types.Project by hand; YAML typos (images:) that remove the image key; services intended only as metadata that accidentally stay enabled because their profile is active.","solutions":["Add image: or build: to the named service in compose.yaml","If the service is external, give it a provider: block","Load projects with validation enabled (compose-go loader with consistency checks) so the mistake is caught earlier with a clearer message","Run docker compose config to inspect the effective service definition"],"exampleFix":"# before\nservices:\n  worker:\n    command: [\"python\", \"worker.py\"]\n# after\nservices:\n  worker:\n    build: ./worker\n    command: [\"python\", \"worker.py\"]\n","handlingStrategy":"validation","validationCode":"// Before compose build/up on a programmatically loaded project:\nfor name, svc := range project.Services {\n\tif svc.Provider == nil && svc.Image == \"\" && svc.Build == nil {\n\t\treturn fmt.Errorf(\"service %q cannot be built or pulled: add image/build/provider\", name)\n\t}\n}","typeGuard":null,"tryCatchPattern":null,"preventionTips":["Load compose files with the standard loader and validation (docker compose config equivalent) in CI","When generating projects in code, assert every service has Image or Build before handing it to compose","Watch for schema typos — unknown keys are silently accepted as extensions"],"tags":["compose","validation","service-config","build"],"backgroundTag":null,"analyzedSha":"ddc4b044b62e9f715212ea4143fa830fac76382f","analyzedAt":"2026-08-15T13:31:42.319Z","schemaVersion":2},"datasetVersion":"2026-08-15T22:17:37.221Z"}