docker/compose · error

cannot create %s %q in read-only service %s: `file` is the s

Error message

cannot create %s %q in read-only service %s: `file` is the sole supported option

What it means

When starting a service, compose materializes secrets/configs that are inlined (content:), from environment, or CDK-provided by creating them inside the container's filesystem at the mount target. If the service is declared read_only: true, compose cannot write those files and requires the source to come from a host file (file:), which is bind-mounted read-only instead.

Source

Thrown at pkg/compose/secrets.go:59

func (s *composeService) injectConfigs(ctx context.Context, project *types.Project, service types.ServiceConfig, id string) error {
	return s.injectFileReferences(ctx, project, service, id, configMount)
}

func (s *composeService) injectFileReferences(ctx context.Context, project *types.Project, service types.ServiceConfig, id string, mountType mountType) error {
	mounts, sources := s.getFilesAndMap(project, service, mountType)

	for _, mount := range mounts {
		content, err := s.resolveFileContent(project, sources[mount.Source], mountType)
		if err != nil {
			return err
		}
		if content == "" {
			continue
		}

		if service.ReadOnly {
			return fmt.Errorf("cannot create %s %q in read-only service %s: `file` is the sole supported option", mountType, sources[mount.Source].Name, service.Name)
		}

		if mount.Target == "" {
			if mountType == secretMount {
				mount.Target = "/run/secrets/" + mount.Source
			} else {
				mount.Target = "/" + mount.Source
			}
		} else if mountType == secretMount && !isAbsTarget(mount.Target) {
			mount.Target = "/run/secrets/" + mount.Target
		}

		if err := s.copyFileToContainer(ctx, id, content, mount); err != nil {
			return err
		}
	}
	return nil
}

View on GitHub (pinned to ddc4b044b6)

Solutions

  1. Define the secret/config with file: pointing at a host path so it is mounted rather than written.
  2. Remove read_only: true (or scope it) if the service must use inline content secrets.
  3. Use an external secret driver that surfaces the secret as a file compatible with read-only roots.
  4. Ensure the file path exists and is readable by the daemon host user.

Example fix

# before
services:
  app:
    read_only: true
    secrets: [api_token]
secrets:
  api_token:
    environment: API_TOKEN   # must be written into container → error

# after
services:
  app:
    read_only: true
    secrets: [api_token]
secrets:
  api_token:
    file: ./secrets/api_token.txt
Defensive patterns

Strategy: validation

Validate before calling

func readOnlySafeMounts(project *types.Project) error {
	for name, svc := range project.Services {
		if !svc.ReadOnly {
			continue
		}
		for _, sec := range svc.Secrets { checkFileSource(project.Secrets, sec, "secret", name) }
		for _, cfg := range svc.Configs { checkFileSource(project.Configs, cfg, "config", name) }
	}
	return nil // checkFileSource errors when source uses content:/environment: without file:
}

Try / catch

if err := compose.Up(ctx, project, opts); err != nil {
    if strings.Contains(err.Error(), "read-only service") && strings.Contains(err.Error(), "sole supported option") {
        // convert the named secret/config to file: or drop read_only
    }
    return err
}

Prevention

When it happens

Trigger: A service with read_only: true that mounts a secret or config defined via content:, via environment:, or otherwise resolved to inline content. The file-creation path requires a writable container filesystem, so compose errors naming the service and mount.

Common situations: Hardening a service by adding read_only: true without migrating its inline configs to files; using secrets: top-level entries with file: pointing at a real path works, but content: / env: variants do not.

Related errors


AI-assisted analysis of docker/compose@ddc4b044b6 (2026-08-15). Data as JSON: /api/errors/ea19a0fb9322f79d. Report an issue: GitHub.