caddyserver/caddy · error

unable to open destination file: %v

Error message

unable to open destination file: %v

What it means

writeCaddyBinary() cannot open the destination path (the original executable's path, just vacated by the rename to .tmp) with O_RDWR|O_CREATE|O_TRUNC. At this point the old binary has already been moved aside, but the deferred restore will put it back because err is set, so the system ends up in its original state.

Source

Thrown at cmd/packagesfuncs.go:323

}

func getPluginPackages(modules []moduleInfo) (map[string]pluginPackage, error) {
	pluginPkgs := make(map[string]pluginPackage)
	for _, mod := range modules {
		if mod.goModule.Replace != nil {
			return nil, fmt.Errorf("cannot auto-upgrade when Go module has been replaced: %s => %s",
				mod.goModule.Path, mod.goModule.Replace.Path)
		}
		pluginPkgs[mod.goModule.Path] = pluginPackage{Version: mod.goModule.Version, Path: mod.goModule.Path}
	}
	return pluginPkgs, nil
}

func writeCaddyBinary(path string, body *io.ReadCloser, fileInfo os.FileInfo) error {
	l := caddy.Log()
	destFile, err := os.OpenFile(path, os.O_RDWR|os.O_CREATE|os.O_TRUNC, fileInfo.Mode())
	if err != nil {
		return fmt.Errorf("unable to open destination file: %v", err)
	}
	defer destFile.Close()

	l.Info("downloading binary", zap.String("destination", path))

	_, err = io.Copy(destFile, *body)
	if err != nil {
		return fmt.Errorf("unable to download file: %v", err)
	}

	err = destFile.Sync()
	if err != nil {
		return fmt.Errorf("syncing downloaded file to device: %v", err)
	}

	return nil
}

View on GitHub (pinned to 50e54ee279)

Solutions

  1. Check the restore happened: run 'caddy version' — the original binary should be back
  2. Inspect permissions and MAC labels on the binary directory: ls -laZ $(dirname $(which caddy))
  3. Free inodes/space if exhausted: df -i, df -h
  4. Re-run the upgrade with sudo / from a writable location
Defensive patterns

Strategy: validation

Validate before calling

DIR="$(dirname "$(command -v caddy)")"
[ -w "$DIR" ] || echo 'directory not writable — run with appropriate privileges'

Prevention

When it happens

Trigger: Write permission on the binary's directory lost between the rename and create (unlikely) or, more typically, the file mode from os.Stat being unusable, SELinux/AppArmor denying creation at the path, a full inode table, or the path being a dangling symlink target on a read-only mount.

Common situations: SELinux denying unlabeled_t file creation in /usr/local/bin; container with a read-only upper layer after the rename somehow succeeded on a different mount; disk out of inodes.

Related errors


AI-assisted analysis of caddyserver/caddy@50e54ee279 (2026-08-15). Data as JSON: /api/errors/3c7b3e1011df9b52. Report an issue: GitHub.