{"record":{"id":"c0d60535239c0395","repo":"juanfont/headscale","slug":"setting-up-extrarecord-manager-w","errorCode":null,"errorMessage":"setting up extrarecord manager: %w","messagePattern":"setting up extrarecord manager: %w","errorType":"console","errorClass":null,"httpStatus":null,"severity":"error","filePath":"hscontrol/app.go","lineNumber":588,"sourceCode":"\t}\n\n\th.state.SetDERPMap(derpMap)\n\n\t// Start ephemeral node garbage collector and schedule all nodes\n\t// that are already in the database and ephemeral. If they are still\n\t// around between restarts, they will reconnect and the GC will\n\t// be cancelled.\n\tgo h.ephemeralGC.Start()\n\n\tephmNodes := h.state.ListEphemeralNodes()\n\tfor _, node := range ephmNodes.All() {\n\t\th.ephemeralGC.Schedule(node.ID(), h.cfg.Node.Ephemeral.InactivityTimeout)\n\t}\n\n\tif h.cfg.DNSConfig.ExtraRecordsPath != \"\" {\n\t\th.extraRecordMan, err = dns.NewExtraRecordsManager(h.cfg.DNSConfig.ExtraRecordsPath)\n\t\tif err != nil {\n\t\t\treturn fmt.Errorf(\"setting up extrarecord manager: %w\", err)\n\t\t}\n\n\t\th.cfg.SetExtraRecords(h.extraRecordMan.Records())\n\n\t\tgo h.extraRecordMan.Run()\n\t\tdefer h.extraRecordMan.Close()\n\t}\n\n\t// Start all scheduled tasks, e.g. expiring nodes, derp updates and\n\t// records updates\n\tscheduleCtx, scheduleCancel := context.WithCancel(context.Background())\n\tdefer scheduleCancel()\n\n\tgo h.scheduledTasks(scheduleCtx)\n\n\t// Prepare group for running listeners\n\terrorGroup := new(errgroup.Group)\n","sourceCodeStart":570,"sourceCodeEnd":606,"githubUrl":"https://github.com/juanfont/headscale/blob/565fd254d06c4c7f9a8cad1714a43445c79ba420/hscontrol/app.go#L570-L606","documentation":"dns.NewExtraRecordsManager failed at startup when dns_config.extra_records_path is set (hscontrol/dns/extrarecords.go:34). The manager creates an fsnotify watcher, stats the file (rejecting directories with ErrPathIsDirectory), parses it as a JSON array of DNS records, and adds a watch. Failure means the path does not exist, is a directory, holds invalid JSON or wrong record fields, or the inotify watch could not be registered.","triggerScenarios":"extra_records_path pointing at a missing file or a directory (explicit ErrPathIsDirectory), a JSON file whose entries do not match the tailcfg.DNSRecord shape (missing/misspelled name/type/value), malformed JSON, or fsnotify/inotify exhaustion (too many watches, ulimit on inotify instances).","commonSituations":"Ops putting a whole directory of records instead of one file; hand-edited JSON with trailing commas or comments; schema drift after upgrading (record fields renamed); systems with a low fs.inotify.max_user_instances limit where watcher creation fails.","solutions":["Check the wrapped stage: 'getting file info' (missing path), 'path is directory', 'reading extra records from path' (JSON/parse), or 'creating/adding watcher' (inotify limits)","Ensure the path is a single regular file containing a JSON array like [{\"name\":\"foo.example.com\",\"type\":\"A\",\"value\":\"1.2.3.4\"}]","Validate with jq before starting headscale","If inotify-limited, raise fs.inotify.max_user_instances/max_user_watches or unset extra_records_path"],"exampleFix":"# before\ndns_config:\n  extra_records_path: /etc/headscale/extra_records/  # directory -> ErrPathIsDirectory\n\n# after\ndns_config:\n  extra_records_path: /etc/headscale/extra_records.json","handlingStrategy":"validation","validationCode":"if p := cfg.DNSConfig.ExtraRecordsPath; p != \"\" {\n    fi, err := os.Stat(p)\n    if err != nil { return fmt.Errorf(\"extra_records_path missing: %s\", p) }\n    if fi.IsDir() { return fmt.Errorf(\"extra_records_path must be a file: %s\", p) }\n    b, _ := os.ReadFile(p)\n    var rs []tailcfg.DNSRecord\n    if err := json.Unmarshal(b, &rs); err != nil {\n        return fmt.Errorf(\"extra_records_path is not a JSON array of records: %w\", err)\n    }\n}","typeGuard":null,"tryCatchPattern":"if _, err := dns.NewExtraRecordsManager(path); err != nil {\n    if errors.Is(err, dns.ErrPathIsDirectory) {\n        // point config at the records file itself, not its folder\n    }\n    // watcher/inotify failures: check fs.inotify limits or drop extra_records_path\n}","preventionTips":["Point extra_records_path at one JSON file, not a directory","jq-validate the records file before deploying config changes","On watch-heavy hosts, raise fs.inotify.max_user_instances before enabling the feature"],"tags":["dns","configuration","json","fsnotify","startup"],"backgroundTag":null,"analyzedSha":"565fd254d06c4c7f9a8cad1714a43445c79ba420","analyzedAt":"2026-08-15T13:12:30.133Z","schemaVersion":2},"datasetVersion":"2026-08-15T22:17:37.221Z"}