OpenNHP/opennhp · error

failed to create etc directory

Error message

failed to create etc directory: %v

What it means

SaveZdtoConfig could not create the ZTDO config directory `<ExeDirPath>/etc/ztdo`. os.MkdirAll failed (permissions, read-only filesystem, or a non-directory file at the path), so the data-<doId>.json config cannot be written and the DRG message handling fails.

Solutions

  1. Ensure the directory containing the nhp-serverd binary is writable by the process user (chown/chmod or run under the service account that owns it).
  2. If ExeDirPath/e tc exists as a file, remove or rename it so MkdirAll can create the directories.
  3. In containers, mount a writable volume at the server's working/exe directory or disable the read-only rootfs.
  4. Check SELinux/AppArmor policies that may block directory creation and add appropriate rules.

Example fix

// before: binary installed root-owned, server runs as nhp
sudo chown -R nhp:nhp /opt/opennhp
// after: or pre-create the writable ztdo dir
sudo mkdir -p /opt/opennhp/etc/ztdo && sudo chown nhp:nhp /opt/opennhp/etc/ztdo
Defensive patterns

Strategy: validation

Validate before calling

dir := filepath.Join(exeDir, "etc", "ztdo")
if fi, err := os.Stat(dir); err == nil && !fi.IsDir() {
    return fmt.Errorf("%s exists and is not a directory", dir)
}
if err := os.MkdirAll(dir, 0o755); err != nil {
    return fmt.Errorf("cannot create %s: %v", dir, err)
}

Type guard

func ztdoDirWritable() bool {
    d := filepath.Join(ExeDirPath, "etc", "ztdo")
    os.MkdirAll(d, 0o755)
    p := filepath.Join(d, ".writetest")
    if err := os.WriteFile(p, nil, 0o644); err != nil { return false }
    os.Remove(p)
    return true
}

Try / catch

if err := publishZtdo(drgMsg); err != nil && strings.Contains(err.Error(), "failed to create etc directory") {
    // fix perms or storage, then retry
    fixStoragePermissions(); err = publishZtdo(drgMsg)
}

Prevention

When it happens

Trigger: HandleDHPDRGMessage invokes SaveZdtoConfig while the server process lacks write permission on ExeDirPath, ExeDirPath is read-only (container rootfs), or a regular file named `etc` or `etc/ztdo` already exists blocking directory creation.

Common situations: Running nhp-serverd as a non-root user against a root-owned install directory; Kubernetes/Docker container with a read-only filesystem and no writable volume mounted at the exe path; accidental file created at the etc path.

Understand the failure class

Background: mkdir permission denied (EACCES): failed to create directory errors explained — this error's family across 32 libraries.

Related errors


AI-assisted analysis of OpenNHP/opennhp@6e04ca5ff0 (2026-09-07). Data as JSON: /api/errors/874ac39bf31225c6. Report an issue: GitHub.

Appendix: source

Thrown at endpoints/server/msghandler.go:731

	configFileName := "data-" + objectId + ".json"

	etcDir := filepath.Join(ExeDirPath, "etc", "ztdo")
	configPath := filepath.Join(etcDir, configFileName)

	if existingDrgMsg, err := ReadZdtoConfig(objectId); err == nil {
		// alway keep original date source type
		drgMsg.DataSourceType = existingDrgMsg.DataSourceType

		if drgMsg.AccessUrl == "" { // provider update access url
			drgMsg.AccessUrl = existingDrgMsg.AccessUrl
		}

		os.Remove(configPath)
	}

	// Make sure the etc directory exists
	if err := os.MkdirAll(etcDir, 0755); err != nil {
		return fmt.Errorf("failed to create etc directory: %v", err)
	}

	if _, err := os.Stat(configPath); err == nil {
		return fmt.Errorf("%v already exists, please delete it first", configFileName)
	}

	file, err := os.Create(configPath)
	if err != nil {
		return fmt.Errorf("failed to create config.json: %v", err)
	}
	defer file.Close()

	encoder := json.NewEncoder(file)
	encoder.SetIndent("", "  ")
	return encoder.Encode(drgMsg)
}

// read data-<doId>.json to DRGMsg Object

View on GitHub (pinned to 6e04ca5ff0)