[GH-ISSUE #1419] docker部署版本在手动备份后的恢复功能会误删其他nginx容器的配置文件并且报错device or resource busy #5219

Closed
opened 2026-03-01 15:40:27 +03:00 by kerem · 1 comment
Owner

Originally created by @sevenand888 on GitHub (Nov 3, 2025).
Original GitHub issue: https://github.com/0xJacky/nginx-ui/issues/1419

Issue报告:Nginx UI备份/恢复功能在Docker环境下存在设计缺陷

Issue标题

[Bug] Backup/Restore功能在Docker环境中失败:设备繁忙错误 (device or resource busy)

问题描述

在使用Docker部署的Nginx UI中,备份功能可以正常工作,但恢复功能总是失败。错误信息显示Nginx UI试图清理Docker挂载的目录,导致"device or resource busy"错误。

环境信息

  • Nginx UI版本: latest (uozi/nginx-ui:latest)
  • 部署方式: Docker Compose
  • 操作系统: Anolis OS 8.10
  • Docker版本: 最新稳定版

复现步骤

  1. 使用Docker Compose部署Nginx UI,挂载多个Nginx配置目录:
volumes:
  - /host/path/nginx_v1/conf:/etc/nginx/ngx_v1
  - /host/path/nginx_v2/conf:/etc/nginx/ngx_v2
  - /host/path/nginx_v3/nginx:/etc/nginx/ngx_v3
  1. 在Nginx UI界面创建备份(成功)
  2. 尝试恢复刚才创建的备份(失败)

错误日志

2025-11-03 10:14:03.072 ERROR backup/restore.go:106 Failed to restore Nginx configs: Failed to copy Nginx config directory: failed to clean directory: unlinkat /etc/nginx/ngx_v1: device or resource busy
2025-11-03 10:14:04.159 ERROR backup/restore.go:106 Failed to restore Nginx configs: Failed to copy Nginx config directory: failed to clean directory: unlinkat /etc/nginx/ngx_v1: device or resource busy

问题分析

根本原因

恢复功能的清理逻辑存在设计缺陷:

  1. 备份阶段:正确读取了配置路径
  2. 恢复阶段:硬编码清理整个/etc/nginx目录,没有考虑Docker卷挂载的特殊性
  3. 错误处理:对"设备繁忙"错误没有适当的跳过机制

具体问题

  • backup/restore.go中的清理函数试图删除/etc/nginx下的所有内容
  • 对于Docker挂载的目录(如ngx_v1, ngx_v2, ngx_v3),无法直接删除
  • 清理失败导致整个恢复过程中止

期望行为
完整恢复流程:清理应该成功完成,然后正常恢复备份内容到所有目录(包括挂载目录)
正确处理挂载点:对于Docker挂载的目录,应该能够正常清理和恢复,而不是跳过或报错
数据一致性:恢复后,所有配置文件(包括挂载目录中的)都应该与备份内容保持一致
建议的修复方案
方案1:改进清理逻辑,正确处理挂载点

// 在backup/restore.go中改进清理逻辑
func cleanNginxConfigDir() error {
    entries, err := os.ReadDir("/etc/nginx")
    if err != nil {
        return err
    }
    
    for _, entry := range entries {
        path := filepath.Join("/etc/nginx", entry.Name())
        
        // 对于挂载目录,清空内容而不是删除目录本身
        if isMountedDirectory(path) {
            // 清空挂载目录内容,保留目录结构
            if err := clearDirectoryContents(path); err != nil {
                return fmt.Errorf("failed to clear mounted directory %s: %v", path, err)
            }
        } else {
            // 对于非挂载目录,正常删除
            if err := os.RemoveAll(path); err != nil {
                return fmt.Errorf("failed to remove %s: %v", path, err)
            }
        }
    }
    return nil
}

// 检查是否为挂载目录
func isMountedDirectory(path string) bool {
    // 方法1:检查inode设备号
    var stat syscall.Stat_t
    if err := syscall.Stat(path, &stat); err != nil {
        return false
    }
    
    // 方法2:检查/proc/mounts
    data, err := os.ReadFile("/proc/mounts")
    if err != nil {
        return false
    }
    
    return strings.Contains(string(data), path)
}

// 清空目录内容,保留目录结构
func clearDirectoryContents(dirPath string) error {
    entries, err := os.ReadDir(dirPath)
    if err != nil {
        return err
    }
    
    for _, entry := range entries {
        path := filepath.Join(dirPath, entry.Name())
        if err := os.RemoveAll(path); err != nil {
            // 如果是设备繁忙错误,可能是嵌套挂载,记录警告但继续
            if strings.Contains(err.Error(), "device or resource busy") {
                log.Warnf("Skipping busy path in mounted directory: %s", path)
                continue
            }
            return err
        }
    }
    return nil
}

方案2:使用rsync式恢复,避免完全清理

func restoreNginxConfigs(backupPath string) error {
    // 使用rsync方式恢复,只更新变化的文件
    cmd := exec.Command("rsync", "-av", "--delete", 
        filepath.Join(backupPath, "nginx/"), "/etc/nginx/")
    
    output, err := cmd.CombinedOutput()
    if err != nil {
        return fmt.Errorf("rsync restore failed: %v, output: %s", err, string(output))
    }
    
    return nil
}

方案3:分阶段恢复,更好的错误处理


func restoreNginxConfigs(backupPath string) error {
    // 阶段1:备份当前配置(安全措施)
    if err := backupCurrentConfig(); err != nil {
        return fmt.Errorf("failed to backup current config: %v", err)
    }
    
    // 阶段2:尝试智能清理
    if err := smartCleanNginxDir(); err != nil {
        log.Warnf("Partial clean failure: %v, attempting selective restore", err)
        // 即使清理部分失败,也尝试恢复
    }
    
    // 阶段3:恢复备份内容
    if err := copyBackupContents(backupPath); err != nil {
        // 阶段4:恢复失败时回滚
        if rollbackErr := rollbackFromBackup(); rollbackErr != nil {
            return fmt.Errorf("restore failed and rollback also failed: restore=%v, rollback=%v", err, rollbackErr)
        }
        return fmt.Errorf("restore failed, rolled back: %v", err)
    }
    
    return nil
}

附加信息
核心问题:恢复功能应该在清理挂载目录时清空内容而不是删除目录本身
数据安全:恢复失败时不应该让系统处于中间状态(部分清理但未恢复)
用户体验:用户期望备份/恢复是原子操作,要么完全成功要么完全失败
建议优先级
高 - 这个bug导致恢复功能在Docker环境中完全不可用,且存在数据丢失风险。

希望开发团队能够修复这个设计缺陷,让恢复功能能够正确处理Docker挂载目录,实现完整的配置恢复。

相关文件
受影响的源码文件:backup/restore.go中的清理和恢复逻辑
需要改进的错误处理机制和挂载点检测
备注:这个问题的核心是恢复流程需要区分"删除目录"和"清空目录内容"的不同场景,特别是在Docker挂载环境下

Originally created by @sevenand888 on GitHub (Nov 3, 2025). Original GitHub issue: https://github.com/0xJacky/nginx-ui/issues/1419 # Issue报告:Nginx UI备份/恢复功能在Docker环境下存在设计缺陷 ## Issue标题 **[Bug] Backup/Restore功能在Docker环境中失败:设备繁忙错误 (device or resource busy)** ## 问题描述 在使用Docker部署的Nginx UI中,备份功能可以正常工作,但恢复功能总是失败。错误信息显示Nginx UI试图清理Docker挂载的目录,导致"device or resource busy"错误。 ## 环境信息 - **Nginx UI版本**: latest (uozi/nginx-ui:latest) - **部署方式**: Docker Compose - **操作系统**: Anolis OS 8.10 - **Docker版本**: 最新稳定版 ## 复现步骤 1. 使用Docker Compose部署Nginx UI,挂载多个Nginx配置目录: ```yaml volumes: - /host/path/nginx_v1/conf:/etc/nginx/ngx_v1 - /host/path/nginx_v2/conf:/etc/nginx/ngx_v2 - /host/path/nginx_v3/nginx:/etc/nginx/ngx_v3 ``` 2. 在Nginx UI界面创建备份(成功) 3. 尝试恢复刚才创建的备份(失败) ## 错误日志 ``` 2025-11-03 10:14:03.072 ERROR backup/restore.go:106 Failed to restore Nginx configs: Failed to copy Nginx config directory: failed to clean directory: unlinkat /etc/nginx/ngx_v1: device or resource busy 2025-11-03 10:14:04.159 ERROR backup/restore.go:106 Failed to restore Nginx configs: Failed to copy Nginx config directory: failed to clean directory: unlinkat /etc/nginx/ngx_v1: device or resource busy ``` ## 问题分析 ### 根本原因 恢复功能的清理逻辑存在设计缺陷: 1. **备份阶段**:正确读取了配置路径 2. **恢复阶段**:硬编码清理整个`/etc/nginx`目录,没有考虑Docker卷挂载的特殊性 3. **错误处理**:对"设备繁忙"错误没有适当的跳过机制 ### 具体问题 - `backup/restore.go`中的清理函数试图删除`/etc/nginx`下的所有内容 - 对于Docker挂载的目录(如`ngx_v1`, `ngx_v2`, `ngx_v3`),无法直接删除 - 清理失败导致整个恢复过程中止 期望行为 完整恢复流程:清理应该成功完成,然后正常恢复备份内容到所有目录(包括挂载目录) 正确处理挂载点:对于Docker挂载的目录,应该能够正常清理和恢复,而不是跳过或报错 数据一致性:恢复后,所有配置文件(包括挂载目录中的)都应该与备份内容保持一致 建议的修复方案 方案1:改进清理逻辑,正确处理挂载点 ```go // 在backup/restore.go中改进清理逻辑 func cleanNginxConfigDir() error { entries, err := os.ReadDir("/etc/nginx") if err != nil { return err } for _, entry := range entries { path := filepath.Join("/etc/nginx", entry.Name()) // 对于挂载目录,清空内容而不是删除目录本身 if isMountedDirectory(path) { // 清空挂载目录内容,保留目录结构 if err := clearDirectoryContents(path); err != nil { return fmt.Errorf("failed to clear mounted directory %s: %v", path, err) } } else { // 对于非挂载目录,正常删除 if err := os.RemoveAll(path); err != nil { return fmt.Errorf("failed to remove %s: %v", path, err) } } } return nil } // 检查是否为挂载目录 func isMountedDirectory(path string) bool { // 方法1:检查inode设备号 var stat syscall.Stat_t if err := syscall.Stat(path, &stat); err != nil { return false } // 方法2:检查/proc/mounts data, err := os.ReadFile("/proc/mounts") if err != nil { return false } return strings.Contains(string(data), path) } // 清空目录内容,保留目录结构 func clearDirectoryContents(dirPath string) error { entries, err := os.ReadDir(dirPath) if err != nil { return err } for _, entry := range entries { path := filepath.Join(dirPath, entry.Name()) if err := os.RemoveAll(path); err != nil { // 如果是设备繁忙错误,可能是嵌套挂载,记录警告但继续 if strings.Contains(err.Error(), "device or resource busy") { log.Warnf("Skipping busy path in mounted directory: %s", path) continue } return err } } return nil } ``` 方案2:使用rsync式恢复,避免完全清理 ```go func restoreNginxConfigs(backupPath string) error { // 使用rsync方式恢复,只更新变化的文件 cmd := exec.Command("rsync", "-av", "--delete", filepath.Join(backupPath, "nginx/"), "/etc/nginx/") output, err := cmd.CombinedOutput() if err != nil { return fmt.Errorf("rsync restore failed: %v, output: %s", err, string(output)) } return nil } ``` 方案3:分阶段恢复,更好的错误处理 ```go func restoreNginxConfigs(backupPath string) error { // 阶段1:备份当前配置(安全措施) if err := backupCurrentConfig(); err != nil { return fmt.Errorf("failed to backup current config: %v", err) } // 阶段2:尝试智能清理 if err := smartCleanNginxDir(); err != nil { log.Warnf("Partial clean failure: %v, attempting selective restore", err) // 即使清理部分失败,也尝试恢复 } // 阶段3:恢复备份内容 if err := copyBackupContents(backupPath); err != nil { // 阶段4:恢复失败时回滚 if rollbackErr := rollbackFromBackup(); rollbackErr != nil { return fmt.Errorf("restore failed and rollback also failed: restore=%v, rollback=%v", err, rollbackErr) } return fmt.Errorf("restore failed, rolled back: %v", err) } return nil } ``` 附加信息 核心问题:恢复功能应该在清理挂载目录时清空内容而不是删除目录本身 数据安全:恢复失败时不应该让系统处于中间状态(部分清理但未恢复) 用户体验:用户期望备份/恢复是原子操作,要么完全成功要么完全失败 建议优先级 高 - 这个bug导致恢复功能在Docker环境中完全不可用,且存在数据丢失风险。 希望开发团队能够修复这个设计缺陷,让恢复功能能够正确处理Docker挂载目录,实现完整的配置恢复。 相关文件 受影响的源码文件:backup/restore.go中的清理和恢复逻辑 需要改进的错误处理机制和挂载点检测 备注:这个问题的核心是恢复流程需要区分"删除目录"和"清空目录内容"的不同场景,特别是在Docker挂载环境下
kerem 2026-03-01 15:40:27 +03:00
  • closed this issue
  • added the
    bug
    label
Author
Owner

@0xJacky commented on GitHub (Nov 10, 2025):

感谢反馈,已修复

<!-- gh-comment-id:3509839874 --> @0xJacky commented on GitHub (Nov 10, 2025): 感谢反馈,已修复
Sign in to join this conversation.
No milestone
No project
No assignees
1 participant
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set.

Reference
starred/nginx-ui#5219
No description provided.