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
- Check the restore happened: run 'caddy version' — the original binary should be back
- Inspect permissions and MAC labels on the binary directory: ls -laZ $(dirname $(which caddy))
- Free inodes/space if exhausted: df -i, df -h
- 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
- Confirm writable directory and sane SELinux/AppArmor context before upgrading
- Watch df -i (inodes) on long-lived servers
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
- backing up current binary: %v
- checking if default Caddyfile exists: %v
- reading default Caddyfile: %v
- retrieving current executable permission bits: %v
- resolving current executable symlink: %v
AI-assisted analysis of caddyserver/caddy@50e54ee279 (2026-08-15).
Data as JSON: /api/errors/3c7b3e1011df9b52.
Report an issue: GitHub.