通过NodeJS管理Docker容器:游戏服务器启停问题求助
问题描述
我正在开发一款带后端的游戏,希望支持玩家启动自定义或官方游戏服务器,计划在虚拟机中用Docker部署这些服务器。目前已配置好docker-compose文件和镜像,直接在控制台执行docker compose up game_server1 -d可正常启动并连接服务器,但通过NodeJS应用启动未运行的服务时遇到问题。
已尝试方案及问题
- 直接在NodeJS应用中执行CMD命令,但未实现可靠的状态判断;
- 使用dockerode库,发现它没有直接启动docker-compose服务的功能;
- 了解过Swarm,但预判会遇到同样的命令执行问题;
- Kubernetes学习曲线过高,时间有限,不确定是否适配当前需求。
需求
- NodeJS应用接收HTTP请求后,能执行
docker compose up game_server1 -d启动指定服务; - 能判断服务器是否启动成功;
- 能实现服务器关停操作;
- 欢迎其他游戏服务器启动方案建议。
当前代码
var Docker = require('dockerode'); const fs = require('fs'); const yaml = require('js-yaml'); const path = require('path'); //! PORT RANGE 27000-27100 const docker = new Docker({ socketPath: '/var/run/docker.sock' }); const composeFilePath = './docker/docker-compose.yml'; const fullcomposeFilePath = '~/Slave_js/docker/docker-compose.yml'; var services = []; try { const yamlData = fs.readFileSync(composeFilePath, 'utf8'); const serviceobj = yaml.load(yamlData).services; services = Object.keys(serviceobj).map((serviceName) => ({ name: serviceName, config: serviceobj[serviceName], })); } catch (error) { console.error('Error parsing YAML:', error); } class Game { static async start_game(id) { try { let tempServices = services; let containers = await docker.listContainers({ all: true }); for(let container of containers){ for(let i = 0; i<tempServices.length;i++){ let service = tempServices[i]; let name = container.Image; name = name.split("-")[1]; if(name == service.name){ tempServices.splice(i,1); break; } } } if(!tempServices.length) return{status: 507, result: {msg:"Server full"}} let service = tempServices[0]; //up the service //return true with the port return{status: 200, result: {msg:"Started Successfully"}} } catch (err) { console.log(err); return { status: 500, result: { msg: "Internal server error" }}; } } } module.exports = Game;
解决方案建议
方案一:用NodeJS child_process直接执行docker-compose命令
这是最直接的方案,复用你已验证有效的控制台命令,通过NodeJS调用系统命令并捕获输出判断状态。
修改后的代码:
var Docker = require('dockerode'); const fs = require('fs'); const yaml = require('js-yaml'); const { exec } = require('child_process'); const util = require('util'); // 转换exec为Promise形式,适配async/await const execPromise = util.promisify(exec); //! PORT RANGE 27000-27100 const docker = new Docker({ socketPath: '/var/run/docker.sock' }); const composeFilePath = './docker/docker-compose.yml'; var services = []; try { const yamlData = fs.readFileSync(composeFilePath, 'utf8'); const serviceobj = yaml.load(yamlData).services; services = Object.keys(serviceobj).map((serviceName) => ({ name: serviceName, config: serviceobj[serviceName], })); } catch (error) { console.error('Error parsing YAML:', error); } class Game { static async start_game(id) { try { // 拷贝原数组,避免修改全局services let tempServices = [...services]; let containers = await docker.listContainers({ all: true }); for(let container of containers){ for(let i = 0; i<tempServices.length;i++){ let service = tempServices[i]; let name = container.Image; name = name.split("-")[1]; if(name == service.name){ tempServices.splice(i,1); break; } } } if(!tempServices.length) return{status: 507, result: {msg:"Server full"}} let service = tempServices[0]; // 执行启动命令 try { const { stdout, stderr } = await execPromise(`docker compose -f ${composeFilePath} up ${service.name} -d`); if (stderr) { console.error('启动命令错误输出:', stderr); return { status: 500, result: { msg: `启动失败: ${stderr}` }}; } // 重试验证服务是否运行 let isRunning = false; let retryCount = 0; const maxRetries = 5; while (retryCount < maxRetries) { await new Promise(resolve => setTimeout(resolve, 1000)); const containers = await docker.listContainers({ filters: { name: [service.name] } }); if (containers.length > 0 && containers[0].State === 'running') { isRunning = true; break; } retryCount++; } if (!isRunning) { return { status: 500, result: { msg: "服务启动超时,未进入运行状态" }}; } // 获取映射端口 const container = await docker.getContainer(containers[0].Id); const inspectData = await container.inspect(); const port = Object.keys(inspectData.NetworkSettings.Ports)[0].split('/')[0]; return{status: 200, result: {msg:"Started Successfully", port: port}} } catch (execErr) { console.error('执行命令出错:', execErr); return { status: 500, result: { msg: `执行启动命令失败: ${execErr.message}` }}; } } catch (err) { console.log(err); return { status: 500, result: { msg: "Internal server error" }}; } } // 新增关停服务方法 static async stop_game(serviceName) { try { const { stdout, stderr } = await execPromise(`docker compose -f ${composeFilePath} stop ${serviceName}`); if (stderr) { console.error('关停命令错误输出:', stderr); return { status: 500, result: { msg: `关停失败: ${stderr}` }}; } // 验证服务是否停止 let isStopped = false; let retryCount = 0; const maxRetries = 3; while (retryCount < maxRetries) { await new Promise(resolve => setTimeout(resolve, 1000)); const containers = await docker.listContainers({ filters: { name: [serviceName] }, all: true }); if (containers.length > 0 && containers[0].State === 'exited') { isStopped = true; break; } retryCount++; } if (!isStopped) { return { status: 500, result: { msg: "服务关停超时" }}; } return { status: 200, result: { msg: "服务已成功关停" }}; } catch (execErr) { console.error('执行关停命令出错:', execErr); return { status: 500, result: { msg: `执行关停命令失败: ${execErr.message}` }}; } } } module.exports = Game;
方案二:使用专门的docker-compose NodeJS库
使用docker-compose npm包,直接操作docker-compose服务,比调用系统命令更优雅。
- 安装依赖:
npm install docker-compose
- 修改后的代码:
var Docker = require('dockerode'); const fs = require('fs'); const yaml = require('js-yaml'); const compose = require('docker-compose'); const path = require('path'); //! PORT RANGE 27000-27100 const docker = new Docker({ socketPath: '/var/run/docker.sock' }); const composeFilePath = './docker/docker-compose.yml'; const composeDir = path.dirname(composeFilePath); var services = []; try { const yamlData = fs.readFileSync(composeFilePath, 'utf8'); const serviceobj = yaml.load(yamlData).services; services = Object.keys(serviceobj).map((serviceName) => ({ name: serviceName, config: serviceobj[serviceName], })); } catch (error) { console.error('Error parsing YAML:', error); } class Game { static async start_game(id) { try { let tempServices = [...services]; let containers = await docker.listContainers({ all: true }); for(let container of containers){ for(let i = 0; i<tempServices.length;i++){ let service = tempServices[i]; let name = container.Image; name = name.split("-")[1]; if(name == service.name){ tempServices.splice(i,1); break; } } } if(!tempServices.length) return{status: 507, result: {msg:"Server full"}} let service = tempServices[0]; // 使用docker-compose库启动服务 const startResult = await compose.upOne(service.name, { cwd: composeDir, log: true }); if (startResult.exitCode !== 0) { return { status: 500, result: { msg: `启动失败: ${startResult.err}` }}; } // 验证运行状态 let isRunning = false; let retryCount = 0; const maxRetries = 5; while (retryCount < maxRetries) { await new Promise(resolve => setTimeout(resolve, 1000)); const containers = await docker.listContainers({ filters: { name: [service.name] } }); if (containers.length > 0 && containers[0].State === 'running') { isRunning = true; break; } retryCount++; } if (!isRunning) { return { status: 500, result: { msg: "服务启动超时,未进入运行状态" }}; } // 获取映射端口 const container = await docker.getContainer(containers[0].Id); const inspectData = await container.inspect(); const port = Object.keys(inspectData.NetworkSettings.Ports)[0].split('/')[0]; return{status: 200, result: {msg:"Started Successfully", port: port}} } catch (err) { console.log(err); return { status: 500, result: { msg: "Internal server error" }}; } } static async stop_game(serviceName) { try { const stopResult = await compose.stopOne(serviceName, { cwd: composeDir, log: true }); if (stopResult.exitCode !== 0) { return { status: 500, result: { msg: `关停失败: ${stopResult.err}` }}; } // 验证停止状态 let isStopped = false; let retryCount = 0; const maxRetries = 3; while (retryCount < maxRetries) { await new Promise(resolve => setTimeout(resolve, 1000)); const containers = await docker.listContainers({ filters: { name: [serviceName] }, all: true }); if (containers.length > 0 && containers[0].State === 'exited') { isStopped = true; break; } retryCount++; } if (!isStopped) { return { status: 500, result: { msg: "服务关停超时" }}; } return { status: 200, result: { msg: "服务已成功关停" }}; } catch (err) { console.log(err); return { status: 500, result: { msg: "Internal server error" }}; } } } module.exports = Game;
其他游戏服务器部署建议
- 端口管理:在docker-compose.yml中为每个服务配置固定端口映射,或启动时动态分配端口,避免冲突;
- 资源限制:为每个游戏服务器容器设置CPU、内存配额,防止单个服务占用过多资源;
- 自动化清理:定期清理已停止的容器和无用镜像,释放磁盘空间;
- 日志收集:配置Docker日志驱动,统一收集游戏服务器日志,方便问题排查。
内容的提问来源于stack exchange,提问作者XChicko
相关产品推荐
相关产品推荐

