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!).
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.