juanfont/headscale · critical

removing old socket file: %w

Error message

removing old socket file: %w

What it means

Thrown during server startup when ensureUnixSocketIsAbsent() (hscontrol/app.go:410) cannot clear the path configured as unix_socket. The function stats the path and, if it exists (leftover socket, regular file, or directory), removes it with os.Remove; any stat or remove failure is wrapped in this error. It aborts startup before the gRPC/admin socket listener is created.

Source

Thrown at hscontrol/app.go:619

	go h.scheduledTasks(scheduleCtx)

	// Prepare group for running listeners
	errorGroup := new(errgroup.Group)

	ctx := context.Background()

	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)
	}

View on GitHub (pinned to 565fd254d0)

Solutions

  1. Check what occupies the path: ls -l <unix_socket_path>; if it is a stale socket from a dead process, remove it manually: rm <unix_socket_path>.
  2. Fix ownership of the socket's parent directory so the runtime user can write and remove: chown <user> $(dirname <unix_socket_path>).
  3. Ensure only one headscale instance runs with this socket: pgrep -a headscale, and stop the duplicate (e.g. systemctl stop headscale) before restarting.
  4. If the path is a directory or mount point, set unix_socket in config to a file path inside a writable directory (e.g. /var/run/headscale/headscale.sock).

Example fix

# before (config.yaml)
unix_socket: /var/run/headscale   # a directory -> os.Remove fails with EISDIR

# after
unix_socket: /var/run/headscale/headscale.sock
# and ensure ownership:
# chown -R headscale:headscale /var/run/headscale
Defensive patterns

Strategy: validation

Validate before calling

// Before starting Headscale, verify the socket path is clear and removable.
func socketPathReady(p string) bool {
    fi, err := os.Stat(p)
    if errors.Is(err, os.ErrNotExist) {
        return true
    }
    if err != nil {
        return false
    }
    return fi.Mode()&os.ModeSocket != 0 // stale socket: safe to remove
}

Type guard

func isRemovableSocket(p string) bool {
    fi, err := os.Lstat(p)
    return err == nil && fi.Mode()&os.ModeSocket != 0 && fi.Mode().IsRegular() == false
}

Try / catch

if err := h.Serve(); err != nil {
    var se *os.PathError
    if errors.As(err, &se) && strings.Contains(se.Error(), "removing old socket file") {
        // stale/socket-dir issue: report and exit, do not retry in a loop
    }
}

Prevention

When it happens

Trigger: unix_socket path exists and os.Remove fails: the path is a directory (EISDIR), the parent directory is not writable by the headscale process (EACCES), or an unreachable NFS/FUSE mount returns EIO on stat/remove. Also triggered if another running headscale instance holds a socket at the same path and the filesystem denies removal.

Common situations: Running headscale as root first (socket owned by root under /var/run/headscale) and then restarting as an unprivileged user; a leftover socket after an unclean shutdown combined with wrong directory ownership; unix_socket changed to a path whose parent is root-owned; accidentally pointing unix_socket at a directory.

Related errors


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