diff --git a/NON-ROOT-GUIDE.md b/NON-ROOT-GUIDE.md new file mode 100644 index 00000000..76fd76ab --- /dev/null +++ b/NON-ROOT-GUIDE.md @@ -0,0 +1,281 @@ +# 非root用户运行 Docker 指南 / Non-Root Docker User Guide + +[English](#english) | [简体中文](#简体中文) + +--- + +## 简体中文 + +### 问题背景 + +青龙面板依赖系统的 `cron` 服务来执行定时任务。在 Docker 容器中运行时,不同的基础镜像对 `cron` 的权限要求不同: + +- **Alpine Linux**: 使用 BusyBox 的 `crond` 实现,**要求 root 权限**才能运行 +- **Debian/Ubuntu**: 使用标准的 `cron` 实现,**支持非 root 用户**运行自己的 crontab + +### 推荐方案:使用 Debian 镜像 + +如果您的环境**不能使用 root 用户运行 Docker**,我们强烈推荐使用 Debian 版本的镜像: + +```bash +docker pull whyour/qinglong:debian +``` + +### 为什么 Debian 镜像适合非 root 用户? + +1. **用户级 crontab 支持**: Debian 的 `cron` 服务支持每个用户维护自己的 crontab,无需 root 权限 +2. **兼容性更好**: 支持更多需要标准 GNU 工具链的依赖包 +3. **权限管理更灵活**: 可以轻松配置文件和进程的权限 + +### 使用 Debian 镜像运行(非 root 用户) + +#### 方式一:使用 docker run + +```bash +# 创建数据目录并设置权限 +mkdir -p /your/data/path +chown -R 1000:1000 /your/data/path # 1000 是容器内默认用户 ID + +# 以非 root 用户运行 +docker run -d \ + --name qinglong \ + --user 1000:1000 \ + -v /your/data/path:/ql/data \ + -p 5700:5700 \ + whyour/qinglong:debian +``` + +#### 方式二:使用 docker-compose + +```yaml +version: '3' +services: + qinglong: + image: whyour/qinglong:debian + container_name: qinglong + user: "1000:1000" # 指定用户 ID 和组 ID + volumes: + - ./data:/ql/data + ports: + - "5700:5700" + restart: unless-stopped +``` + +### Alpine 镜像的限制 + +如果您必须使用 Alpine 镜像(`whyour/qinglong:latest`),需要注意: + +1. **必须以 root 用户运行**: Alpine 的 `crond` 需要 root 权限 +2. **不支持非 root 模式**: 尝试以非 root 用户运行会导致定时任务无法执行 +3. **错误表现**: + - 可以添加定时任务(数据库操作成功) + - 任务不会被定时执行(`crontab` 命令失败) + - 可能看到 "Operation not permitted" 相关错误 + +### 故障排查 + +#### 如何确认当前使用的镜像版本? + +```bash +docker inspect qinglong | grep Image +``` + +#### 如何测试 crontab 权限? + +在容器内执行: + +```bash +# 进入容器 +docker exec -it qinglong bash + +# 测试 crontab 命令 +crontab -l + +# 如果看到 "must be suid to work properly" 或权限错误,说明需要 root 权限 +``` + +#### 如何迁移到 Debian 镜像? + +```bash +# 1. 停止并备份当前容器的数据 +docker stop qinglong +docker cp qinglong:/ql/data ./data_backup + +# 2. 删除旧容器 +docker rm qinglong + +# 3. 使用 Debian 镜像创建新容器 +docker run -d \ + --name qinglong \ + --user 1000:1000 \ + -v ./data_backup:/ql/data \ + -p 5700:5700 \ + whyour/qinglong:debian +``` + +### 技术细节:定时任务添加流程 + +当您在前端添加新的定时任务时,后端经历以下步骤: + +1. **数据库操作** (`back/services/cron.ts:create()`) + - 创建 `Crontab` 记录并保存到数据库 + - 这一步**不需要特殊权限**,通常能成功 + +2. **Node Cron 注册** (如果是 6 段或更多段的 cron 表达式) + - 通过 `cronClient.addCron()` 注册到 Node.js 的 cron 调度器 + - 这一步也**不需要系统权限** + +3. **系统 Crontab 更新** (`back/services/cron.ts:setCrontab()`, 第 672 行) + - 执行 `crontab ` 命令 + - **这一步在 Alpine 上需要 root 权限** + - 在 Debian 上非 root 用户可以成功执行 + +4. **Crond 守护进程** (`docker/docker-entrypoint.sh`, 第 36 行) + - 运行 `crond -f` 守护进程来执行定时任务 + - **Alpine 的 crond 必须以 root 运行** + - Debian 的 cron 支持用户级运行 + +### 相关资源 + +- [Alpine Linux crontab 权限问题](https://gitlab.alpinelinux.org/alpine/aports/-/issues/5380) +- [StackOverflow: Alpine 上 crontab 编辑失败](https://stackoverflow.com/questions/36453787/failed-to-edit-crontab-linux-alpine) +- [Debian cron 手册](https://manpages.debian.org/bullseye/cron/cron.8.en.html) + +--- + +## English + +### Background + +Qinglong panel relies on the system's `cron` service to execute scheduled tasks. When running in Docker containers, different base images have different permission requirements for `cron`: + +- **Alpine Linux**: Uses BusyBox's `crond` implementation, **requires root privileges** +- **Debian/Ubuntu**: Uses standard `cron` implementation, **supports non-root users** + +### Recommended Solution: Use Debian Image + +If your environment **cannot run Docker as root**, we strongly recommend using the Debian version: + +```bash +docker pull whyour/qinglong:debian +``` + +### Why Debian Image is Better for Non-Root Users? + +1. **User-level crontab support**: Debian's `cron` allows each user to maintain their own crontab without root privileges +2. **Better compatibility**: Supports more dependency packages that require standard GNU toolchain +3. **Flexible permission management**: Easy to configure file and process permissions + +### Running with Debian Image (Non-Root User) + +#### Method 1: Using docker run + +```bash +# Create data directory and set permissions +mkdir -p /your/data/path +chown -R 1000:1000 /your/data/path # 1000 is the default user ID in container + +# Run as non-root user +docker run -d \ + --name qinglong \ + --user 1000:1000 \ + -v /your/data/path:/ql/data \ + -p 5700:5700 \ + whyour/qinglong:debian +``` + +#### Method 2: Using docker-compose + +```yaml +version: '3' +services: + qinglong: + image: whyour/qinglong:debian + container_name: qinglong + user: "1000:1000" # Specify user ID and group ID + volumes: + - ./data:/ql/data + ports: + - "5700:5700" + restart: unless-stopped +``` + +### Alpine Image Limitations + +If you must use the Alpine image (`whyour/qinglong:latest`), please note: + +1. **Must run as root**: Alpine's `crond` requires root privileges +2. **No non-root support**: Attempting to run as non-root will cause scheduled tasks to fail +3. **Error symptoms**: + - Can add scheduled tasks (database operation succeeds) + - Tasks won't execute on schedule (`crontab` command fails) + - May see "Operation not permitted" related errors + +### Troubleshooting + +#### How to check current image version? + +```bash +docker inspect qinglong | grep Image +``` + +#### How to test crontab permissions? + +Execute inside the container: + +```bash +# Enter container +docker exec -it qinglong bash + +# Test crontab command +crontab -l + +# If you see "must be suid to work properly" or permission errors, root is required +``` + +#### How to migrate to Debian image? + +```bash +# 1. Stop and backup current container data +docker stop qinglong +docker cp qinglong:/ql/data ./data_backup + +# 2. Remove old container +docker rm qinglong + +# 3. Create new container with Debian image +docker run -d \ + --name qinglong \ + --user 1000:1000 \ + -v ./data_backup:/ql/data \ + -p 5700:5700 \ + whyour/qinglong:debian +``` + +### Technical Details: Scheduled Task Addition Flow + +When you add a new scheduled task from the frontend, the backend goes through these steps: + +1. **Database Operation** (`back/services/cron.ts:create()`) + - Creates `Crontab` record and saves to database + - **Doesn't require special permissions**, usually succeeds + +2. **Node Cron Registration** (if cron expression has 6+ segments) + - Registers with Node.js cron scheduler via `cronClient.addCron()` + - **Doesn't require system permissions** + +3. **System Crontab Update** (`back/services/cron.ts:setCrontab()`, line 672) + - Executes `crontab ` command + - **Requires root privileges on Alpine** + - Non-root users can execute successfully on Debian + +4. **Crond Daemon** (`docker/docker-entrypoint.sh`, line 36) + - Runs `crond -f` daemon to execute scheduled tasks + - **Alpine's crond must run as root** + - Debian's cron supports user-level execution + +### Related Resources + +- [Alpine Linux crontab permission issue](https://gitlab.alpinelinux.org/alpine/aports/-/issues/5380) +- [StackOverflow: Failed to edit crontab on Alpine](https://stackoverflow.com/questions/36453787/failed-to-edit-crontab-linux-alpine) +- [Debian cron manual](https://manpages.debian.org/bullseye/cron/cron.8.en.html) diff --git a/README-en.md b/README-en.md index ce4e9542..0639a78e 100644 --- a/README-en.md +++ b/README-en.md @@ -41,6 +41,8 @@ Timed task management platform supporting Python3, JavaScript, Shell, Typescript The `latest` image is built on `alpine` and the `debian` image is built on `debian-slim`. If you need to use a dependency that is not supported by `alpine`, it is recommended that you use the `debian` image. +**⚠️ Important**: If you need to run Docker as a **non-root user**, please use the `debian` image. Alpine's `crond` requires root privileges, while Debian supports user-level crontab. See [Non-Root User Guide](./NON-ROOT-GUIDE.md) for details. + ```bash docker pull whyour/qinglong:latest docker pull whyour/qinglong:debian diff --git a/README.md b/README.md index e46d9d79..6c17f59f 100644 --- a/README.md +++ b/README.md @@ -43,6 +43,8 @@ Timed task management platform supporting Python3, JavaScript, Shell, Typescript `latest` 镜像是基于 `alpine` 构建,`debian` 镜像是基于 `debian-slim` 构建。如果需要使用 `alpine` 不支持的依赖,建议使用 `debian` 镜像 +**⚠️ 重要提示**: 如果您需要以**非 root 用户**运行 Docker,请使用 `debian` 镜像。Alpine 的 `crond` 需要 root 权限,而 Debian 支持用户级 crontab。详见 [非root用户运行指南](./NON-ROOT-GUIDE.md)。 + ```bash docker pull whyour/qinglong:latest docker pull whyour/qinglong:debian diff --git a/back/services/cron.ts b/back/services/cron.ts index 67bfefd2..edb79fcb 100644 --- a/back/services/cron.ts +++ b/back/services/cron.ts @@ -669,12 +669,47 @@ export default class CronService { await writeFileWithLock(config.crontabFile, crontab_string); - execSync(`crontab ${config.crontabFile}`); - await CrontabModel.update({ saved: true }, { where: {} }); + try { + execSync(`crontab ${config.crontabFile}`); + await CrontabModel.update({ saved: true }, { where: {} }); + } catch (error: any) { + const errorMsg = error.message || String(error); + this.logger.error('[crontab] Failed to update system crontab:', errorMsg); + + // Provide helpful error message for permission issues + if (errorMsg.includes('must be suid') || + errorMsg.includes('Operation not permitted') || + errorMsg.includes('Permission denied')) { + this.logger.error( + '[crontab] ⚠️ Crontab permission error detected. ' + + 'If running as non-root user on Alpine Linux, please use the Debian image instead. ' + + 'See NON-ROOT-GUIDE.md for details.' + ); + } + + throw error; + } } public importCrontab() { exec('crontab -l', (error, stdout, stderr) => { + if (error) { + const errorMsg = error.message || String(error); + this.logger.error('[crontab] Failed to read system crontab:', errorMsg); + + // Provide helpful error message for permission issues + if (errorMsg.includes('must be suid') || + errorMsg.includes('Operation not permitted') || + errorMsg.includes('Permission denied')) { + this.logger.error( + '[crontab] ⚠️ Crontab permission error detected. ' + + 'If running as non-root user on Alpine Linux, please use the Debian image instead. ' + + 'See NON-ROOT-GUIDE.md for details.' + ); + } + return; + } + const lines = stdout.split('\n'); const namePrefix = new Date().getTime();