Node.js spawn `docker run` command with extra streams

Viewed 235

I'm running a command line app from a Node.js process using spawn(). The process is launched with extra pipes in the stdio option. Here's a simplified code sample:

const stdio = ['ignore', 'pipe', 'pipe', 'pipe', 'pipe'];
const process = spawn('/path/to/command', [], { stdio });

// Later...
const { 3: pipeWrite, 4: pipeRead } = process.stdio;
pipeRead.on('data', (data) => {
  if (String(data) === "PING?") {
    pipeWrite.write("PONG!");
  }
}); 

Now, this works fine but I want to run the command inside a docker container, using docker run as the spawned executable:

const stdio = ['ignore', 'pipe', 'pipe', 'pipe', 'pipe'];
const process = spawn(
  '/usr/bin/env', [
    'docker',
    'run',
    '--rm',
    'my-image',
    '/path/to/command'
  ], { stdio }
);

This fails, the command line app inside the docker container says it cannot write to pipe. Is it possible to achieve this with docker run?

I've set up a Github repo that demonstrates the problem, though I should make it clear that this is merely a demonstration and I do not have the means to change behaviour of the real child process (which is Chromium, in case anyone is interested!).

2 Answers

The method you are trying isn't achievable. A child process can share the file descriptors with parent however when running a process inside docker it is containerized first. You can achieve this using named pipes instead of pipes. Named pipes are like pipes but have the interface of a file.

According to this it is doable using named pipes:

This is the modified code of the Github repo you posted:

test.js

import { spawn } from "child_process";
import path from 'path';
import { fileURLToPath } from 'url';
import {describe, jest} from '@jest/globals'
import fs from "fs";

const __dirname = path.dirname(fileURLToPath(import.meta.url));

describe("on docker container", () => {
  let child;
  jest.setTimeout(60000);
  afterEach(() => child?.kill('SIGINT'));
  it("responds with PONG! when PING? is sent", async () => {
    child = spawn(
      "/usr/bin/env",
      [
        "docker",
        "run",
        "--rm",
        "--init",
        `-v=${ __dirname }:/usr/app`,
        "node:latest",
        "node",
        "/usr/app/ponger.js"
      ],
      {
        stdio: ['inherit', 'inherit', 'inherit']
      }
    );

    await new Promise((resolve, reject) => {
      const fifoWritePath = path.resolve(`${ __dirname }/named_pipe_in`);
      const fifoReadPath = path.resolve(`${ __dirname }/named_pipe_out`);
      const pipeWrite = fs.createWriteStream(fifoWritePath);
      const pipeRead = fs.createReadStream(fifoReadPath);

      pipeRead.on('data', (message) => {
        console.log(message)
        if (String(message) === "PONG!\n") {
          resolve();
        }
        else {
          reject();
        }
      });
      pipeWrite.write("PING?\n");
    });
  });
});

ponger.js

import fs from "fs";
import path from 'path';

const fifoReadPath = path.resolve('/usr/app/named_pipe_in');
const fifoWritePath = path.resolve('/usr/app/named_pipe_out');

const pipeRead = fs.createReadStream(fifoReadPath);
const pipeWrite = fs.createWriteStream(fifoWritePath);

pipeRead.on("data", (message) => {
  if (String(message) === "PING?\n") {
    pipeWrite.write("PONG!\n");
  }
});

Before you run npm test while in the directory of the project execute the following :

mkfifo --mode=777 named_pipe_out
mkfifo --mode=777 named_pipe_out

The previledges are only for test purposes, keep in mind that such a setting can ruin the security of your application.

PS. I can elaborate further on why your approach will not going to work in this setting.

EDITED: It seems docker ignores extraneous pipes, and the approach above does not work. What you could do instead, is create a shared volume, in which a stream/pipe is generated, and use it like you would want to.

Be aware that it is generally advised to avoid piping large amounts of data in/out of docker, as this results in quite heavy log files. (Among other issues)

Consider using a TCP socket from the host into the container instead

Related