You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

通过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服务,比调用系统命令更优雅。

  1. 安装依赖:
npm install docker-compose
  1. 修改后的代码:
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

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.07.07 00:55:00