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
- 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>.
- Fix ownership of the socket's parent directory so the runtime user can write and remove: chown <user> $(dirname <unix_socket_path>).
- Ensure only one headscale instance runs with this socket: pgrep -a headscale, and stop the duplicate (e.g. systemctl stop headscale) before restarting.
- 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
- Give each headscale instance a unique unix_socket path.
- Pre-create the socket directory with correct ownership before start (RuntimeDirectory= in systemd).
- Never point unix_socket at a directory or network mount.
- In containers, mount a tmpfs at the socket directory.
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
- setting up unix socket: %w
- changing socket permission: %w
- creating directory failed with permission error
- reading or creating Noise protocol private key: %w
- reading or creating DERP server private key: %w
AI-assisted analysis of juanfont/headscale@565fd254d0 (2026-08-15).
Data as JSON: /api/errors/3a70f6d91eb6c89b.
Report an issue: GitHub.