Docker容器执行编译文件后输出读取失败问题排查与解决
解决Docker容器执行C++代码后读取输出文件出现ENOENT错误的问题
问题背景
我开发了一个支持文件上传的网页,用户提交C++文件后,服务端会启动Docker容器编译并运行代码,最终在网页显示程序输出。最初遇到输出包含不可打印字符的问题,改为将输出写入容器内的文本文件后,又出现Node.js读取文件时的ENOENT错误:
Error reading the file: [Error: ENOENT: no such file or directory, open '/home/slava/Desktop/TESTING/output/output.txt'] { errno: -2, code: 'ENOENT', syscall: 'open', path: '/home/slava/Desktop/TESTING/output/output.txt' }
注释掉容器停止和删除的代码后输出正常,但业务要求每次使用后必须清理容器。观察发现文件已在主机创建,但Node.js要到第二次提交才能识别,问题与容器清理时机直接相关。
问题根源
- 异步操作顺序错误:
fs.readFile是异步非阻塞调用,代码触发文件读取后立刻执行容器停止/删除,此时文件可能还未完全从容器同步到主机挂载卷,Node.js无法感知文件存在。 - Docker卷缓存机制:容器内的文件写入可能被操作系统缓存,未立刻同步到主机文件系统,容器停止后缓存未刷新导致主机无法读取文件。
- 文件覆盖风险:所有用户输出都写入同一个
output.txt,并发场景下会出现读写冲突或文件覆盖。
解决方案
1. 修正异步操作顺序,确保读取完成后再清理容器
将异步的fs.readFile替换为Promise版的fs.promises.readFile,用await等待读取完成后再执行容器停止和删除,保证文件读取完成后再销毁容器。
2. 强制同步容器内文件到主机
在entrypoint.sh的文件写入完成后添加sync命令,强制操作系统将缓存数据写入磁盘,确保主机能立刻获取到文件内容。
3. 避免文件覆盖,生成用户独立输出文件
用用户名作为输出文件的后缀(如output_${username}.txt),防止多用户提交时互相覆盖输出内容。
4. 移除Dockerfile中不必要的COPY操作
Dockerfile中的COPY submissions/ /submissions/会覆盖挂载的卷内容,应删除该指令,因为我们已经通过HostConfig绑定了主机的submissions目录到容器。
修改后的关键代码
修改后的server.js核心逻辑
app.post('/execute', upload.single('file'), async (req, res) => { const { code, username } = req.body; if (req.file) { const userDirectory = `/submissions/${username}`; const uploadedFilePath = path.join(userDirectory, req.file.originalname); const codeFileName = path.basename(uploadedFilePath, path.extname(uploadedFilePath)); const container = await docker.createContainer({ Image: 'my-cpp-app', Tty: true, Detach: true, HostConfig: { Binds: [ `${__dirname}/submissions:/submissions`, `${__dirname}/output:/output`, ], }, }); await container.start(); const sourceCodeFile = `/submissions/${username}/${req.file.originalname}`; // 传递用户名给entrypoint.sh,生成独立输出文件 const command = `/entrypoint.sh "${sourceCodeFile}" "${username}"`; const exec = await container.exec({ Cmd: ['bash', '-c', command] }); const execResult = await exec.start(); if (execResult.output) { await new Promise((resolve, reject) => { container.modem.demuxStream(execResult.output, process.stdout, process.stderr); execResult.output.on('end', resolve); execResult.output.on('error', reject); }); } // 使用Promise版readFile,等待读取完成后再清理容器 try { const data = await fs.promises.readFile(`${__dirname}/output/output_${username}.txt`, 'utf8'); res.send({output: data}); } catch (err) { console.error('Error reading the file:', err); res.status(500).send({output: '读取输出文件失败'}); } // 读取完成后再执行容器清理 await container.stop(); await container.remove(); } });
修改后的entrypoint.sh
#!/bin/bash if [ $# -ne 2 ]; then echo "Usage: $0 <source_code_file.cpp> <username>" exit 1 fi g++ -o user_program "$1" if [ $? -eq 0 ]; then ./user_program > /output/output_$2.txt # 强制同步文件到磁盘,确保主机能立刻读取 sync else echo "Compilation failed." >&2 echo "Compilation failed." > /output/output_$2.txt sync fi
修改后的Dockerfile
FROM ubuntu:20.04 # Set environment variables to avoid interactive prompts during installation ENV DEBIAN_FRONTEND=noninteractive # Update and install essential packages RUN apt-get update -y && apt-get install -y \ build-essential \ cmake \ gdb \ g++ RUN mkdir /output && chmod 777 /output # 移除不必要的COPY操作,避免覆盖挂载卷 # COPY submissions/ /submissions/ # Define an entry point script that will be executed by the Node.js server COPY entrypoint.sh /entrypoint.sh RUN chmod +x /entrypoint.sh
额外注意事项
- 确保主机的
output目录已提前创建,并设置正确的读写权限(如chmod 777 output)。 - 可添加输出文件的清理逻辑,避免磁盘空间被大量输出文件占用(如定时删除7天前的文件)。
内容的提问来源于stack exchange,提问作者Slava
相关产品推荐
相关产品推荐

