juanfont/headscale · critical

setting up unix socket: %w

Error message

setting up unix socket: %w

What it means

Thrown during startup when util.EnsureDir fails to create the parent directory of the configured unix_socket path (socketDir := filepath.Dir(h.cfg.UnixSocket) in hscontrol/app.go:623). EnsureDir does a MkdirAll on that directory; failure means the directory could not be created or already exists with wrong permissions/ownership. Startup aborts before listening.

Source

Thrown at hscontrol/app.go:626

	ctx, cancel := context.WithCancel(ctx)
	defer cancel()

	//
	//
	// Set up LOCAL listeners
	//

	err = h.ensureUnixSocketIsAbsent()
	if err != nil {
		return fmt.Errorf("removing old socket file: %w", err)
	}

	socketDir := filepath.Dir(h.cfg.UnixSocket)

	err = util.EnsureDir(socketDir)
	if err != nil {
		return fmt.Errorf("setting up unix socket: %w", err)
	}

	socketListener, err := new(net.ListenConfig).Listen(context.Background(), "unix", h.cfg.UnixSocket)
	if err != nil {
		return fmt.Errorf("setting up socket: %w", err)
	}

	// Change socket permissions
	if err := os.Chmod(h.cfg.UnixSocket, h.cfg.UnixSocketPermission); err != nil { //nolint:noinlineerr
		return fmt.Errorf("changing socket permission: %w", err)
	}

	// The Huma v1 API mux matches full /api/v1/... paths and is shared by
	// the local unix socket (served without authentication, local trust)
	// and the remote TCP router (served behind the API-key middleware).
	humaMux, _ := apiv1.Handler(apiv1.Backend{
		State:  h.state,
		Change: h.Change,

View on GitHub (pinned to 565fd254d0)

Solutions

  1. Pre-create the socket directory with correct ownership: install -d -o headscale -g headscale /run/headscale (or add RuntimeDirectory=headscale to the systemd unit).
  2. If in a container, mount a writable tmpfs/volume at the socket directory: docker run ... --tmpfs /var/run/headscale.
  3. Check for path-component conflicts: a regular file sitting where a directory is needed (ls -ld each component) and remove or relocate it.
  4. If SELinux is enforcing, set the correct context (restorecon -v on the directory) or adjust the policy for the headscale process.

Example fix

# before: systemd unit without runtime dir, /run/headscale missing -> EACCES
[Service]
User=headscale

# after
[Service]
User=headscale
RuntimeDirectory=headscale
RuntimeDirectoryMode=0755
Defensive patterns

Strategy: validation

Validate before calling

// Pre-flight: ensure the socket dir is creatable/writable.
func ensureSocketDir(socketPath string) error {
    dir := filepath.Dir(socketPath)
    if err := os.MkdirAll(dir, 0o755); err != nil {
        return fmt.Errorf("socket dir not writable: %w", err)
    }
    return nil
}

Try / catch

if err := h.Serve(); err != nil && strings.Contains(err.Error(), "setting up unix socket") {
    log.Fatalf("socket directory problem, check ownership of %s: %v", filepath.Dir(cfg.UnixSocket), err)
}

Prevention

When it happens

Trigger: unix_socket is set to a path like /run/headscale/headscale.sock but /run/headscale cannot be created because /run is root-owned and headscale runs unprivileged (EACCES); the target path component exists as a regular file (ENOTDIR); a read-only filesystem (EROFS), commonly containers with a read-only rootfs and no writable volume for the socket.

Common situations: Running headscale in Docker/Kubernetes without a writable volume or tmpfs at the socket path; running as the headscale system user without RuntimeDirectory= set in the systemd unit (so /run/headscale never exists); SELinux denying writes to /run; moving config to /etc but leaving unix_socket pointing somewhere unwritable.

Related errors


AI-assisted analysis of juanfont/headscale@565fd254d0 (2026-08-15). Data as JSON: /api/errors/f9ea93529d8f7fcf. Report an issue: GitHub.