在容器化架构中,我们经常需要在一个业务容器内部执行宿主机的 Docker 命令,用来完成构建任务、运行临时容器、生成构建产物等。比如:
- 后端服务需要调用一个打包镜像完成
Android/iOS 构建
CI 系统需要在隔离环境中执行容器命令
- 某些业务逻辑需要动态运行容器执行任务
然而,在实现这个需求时,遇到一系列问题,如找不到 docker 命令、权限异常、构建环境污染、镜像体积过大、安装速度极慢……
Docker 采用 C/S 架构:
Docker 架构组件
| 组件 | 解释 |
|---|
| dockerd | Docker 守护进程(服务端),实际执行 run/pull/build 等操作 |
| docker CLI | 客户端命令行,仅负责向 dockerd 发送指令 |
| docker.sock | 进程间通信的 Unix socket(相当于 API 入口) |
要从容器操作宿主机 Docker,本质就是:
让容器内的 docker CLI 与宿主机的 dockerd 通信
最简单的方式:
volumes:
- /var/run/docker.sock:/var/run/docker.sock
完成这一步后,容器中的 docker 命令实际上控制的是 宿主机 Docker。
docker.sock 只是接口。
要执行:
容器必须安装 docker 客户端 CLI,否则根本找不到 docker 可执行文件。
所以我们还需要第二步——安装 docker CLI。
如果直接在构建层编写 apk add docker,镜像体积会增加(80–100MB) 这会安装:
docker daemon(dockerd)
containerd
runc
- 所有底层组件
但作为客户端,业务可能根本不需要这些。所以我们只需要安装 docker-cli 的客户端即可,镜像增加的大小可控制在 10MB 左右。
RUN apk add --no-cache docker-cli
我们通过 --no-cache 避免了索引缓存,但也带来了下载缓慢的问题,每次都要重新下载索引。
可以将默认源改成阿里云或中科大:
RUN sed -i 's/dl-cdn.alpinelinux.org/mirrors.aliyun.com/g' /etc/apk/repositories
效果极其显著,构建从 数分钟 → 数秒
const { spawn } = require('child_process');
const child = spawn('docker', [
'run',
'--rm',
'-v', '/host/project:/app',
'builder-image'
]);
child.stdout.on('data', d => console.log(d.toString()));
child.stderr.on('data', d => console.error(d.toString()));
当我们需要执行一些临时任务,比如代码构建、文件转换、数据处理等,任务完成后希望容器自动销毁:
const { spawn } = require('child_process');
function runOneTimeTask(imageName, command, volumes = []) {
return new Promise((resolve, reject) => {
const dockerArgs = [
'run',
'--rm', // 容器退出后自动删除
...volumes.flatMap(v => ['-v', v]),
imageName,
...command
];
const child = spawn('docker', dockerArgs);
child.on('close', (code) => {
if (code === 0) {
resolve(`Task completed successfully`);
} else {
reject(new Error(`Task failed with code ${code}`));
}
});
child.on('error', reject);
});
}
// 使用示例:PDF 生成任务
await runOneTimeTask(
'pdf-generator:latest',
['generate-pdf', '/app/input.html', '/app/output.pdf'],
['/host/documents:/app']
);
在多个任务并发时,我们希望能够取消之前正在运行的任务:
concurrent-task-manager.ts class DockerTaskManager {
private runningContainers = new Map<string, string>(); // taskId -> containerId
async runTask(taskId: string, imageName: string, command: string[]) {
// 取消之前的同类任务
await this.cancelTask(taskId);
return new Promise((resolve, reject) => {
// 启动新容器
const dockerArgs = ['run', '--rm', '-d', imageName, ...command];
const child = spawn('docker', dockerArgs);
let containerId = '';
child.stdout.on('data', (data) => {
containerId = data.toString().trim();
this.runningContainers.set(taskId, containerId);
});
child.on('close', (code) => {
this.runningContainers.delete(taskId);
if (code === 0) resolve(containerId);
else reject(new Error(`Failed to start container: ${code}`));
});
});
}
async cancelTask(taskId: string) {
const containerId = this.runningContainers.get(taskId);
if (containerId) {
try {
// 停止并删除容器
await this.execDocker(['stop', containerId]);
await this.execDocker(['rm', containerId]);
this.runningContainers.delete(taskId);
console.log(`Cancelled task: ${taskId}`);
} catch (error) {
console.error(`Failed to cancel task ${taskId}:`, error);
}
}
}
private execDocker(args: string[]) {
return new Promise((resolve, reject) => {
const child = spawn('docker', args);
child.on('close', (code) => {
if (code === 0) resolve(null);
else reject(new Error(`Docker command failed: ${args.join(' ')}`));
});
});
}
}
// 使用示例
const taskManager = new DockerTaskManager();
// 启动构建任务
await taskManager.runTask('build-android', 'android-builder', ['./gradlew', 'build']);
// 如果用户触发了新的构建,会自动取消上一个
await taskManager.runTask('build-android', 'android-builder', ['./gradlew', 'build']);
根据不同参数执行多个构建任务,比如同时构建多个平台的应用:
interface BuildConfig {
platform: string;
image: string;
command: string[];
volumes: string[];
env?: { [key: string]: string };
}
class MultiBuildManager {
async buildMultiplePlatforms(configs: BuildConfig[]) {
const buildPromises = configs.map(config => this.buildPlatform(config));
try {
const results = await Promise.all(buildPromises);
return results;
} catch (error) {
console.error('One or more builds failed:', error);
throw error;
}
}
private buildPlatform(config: BuildConfig): Promise<string> {
return new Promise((resolve, reject) => {
const dockerArgs = [
'run',
'--rm',
'--name', `build-${config.platform}-${Date.now()}`,
...config.volumes.flatMap(v => ['-v', v]),
];
// 添加环境变量
if (config.env) {
Object.entries(config.env).forEach(([key, value]) => {
dockerArgs.push('-e', `${key}=${value}`);
});
}
dockerArgs.push(config.image, ...config.command);
console.log(`Starting build for ${config.platform}...`);
const child = spawn('docker', dockerArgs);
let output = '';
child.stdout.on('data', (data) => {
output += data.toString();
console.log(`[${config.platform}] ${data.toString().trim()}`);
});
child.stderr.on('data', (data) => {
console.error(`[${config.platform}] ${data.toString().trim()}`);
});
child.on('close', (code) => {
if (code === 0) {
console.log(`✅ Build completed for ${config.platform}`);
resolve(output);
} else {
reject(new Error(`❌ Build failed for ${config.platform} with code ${code}`));
}
});
});
}
}
// 使用示例:同时构建多个平台
const buildManager = new MultiBuildManager();
const buildConfigs: BuildConfig[] = [
{
platform: 'android',
image: 'android-builder:latest',
command: ['./gradlew', 'assembleRelease'],
volumes: ['/host/project:/app', '/host/output:/output'],
env: { PLATFORM: 'android', BUILD_TYPE: 'release' }
},
{
platform: 'ios',
image: 'ios-builder:latest',
command: ['xcodebuild', '-workspace', 'App.xcworkspace', '-scheme', 'App', 'archive'],
volumes: ['/host/project:/app', '/host/output:/output'],
env: { PLATFORM: 'ios', BUILD_TYPE: 'release' }
},
{
platform: 'web',
image: 'node:18-alpine',
command: ['npm', 'run', 'build'],
volumes: ['/host/project:/app', '/host/output:/output'],
env: { NODE_ENV: 'production' }
}
];
try {
await buildManager.buildMultiplePlatforms(buildConfigs);
console.log('🎉 All platforms built successfully!');
} catch (error) {
console.error('Build process failed:', error);
}
对于复杂任务,我们希望有一个服务来管理这些 Docker 任务,支持启动、停止、查询状态和日志等功能:
import { spawn, ChildProcess } from 'node:child_process';
import { v4 as uuidv4 } from 'uuid';
export interface DockerTask {
id: string;
command: string;
args: string[];
status: 'running' | 'completed' | 'failed';
startTime: number;
endTime?: number;
logs: string[];
process?: ChildProcess;
exitCode?: number | null;
}
export class DockerService {
private tasks: Map<string, DockerTask> = new Map();
/**
* 运行一个新的 Docker 任务
* @param command Docker 命令 (e.g., 'run')
* @param args 参数列表
* @param options spawn 选项
* @returns 任务 ID
*/
public runTask(command: string, args: string[], options: any = {}): string {
const taskId = uuidv4();
const task: DockerTask = {
id: taskId,
command,
args,
status: 'running',
startTime: Date.now(),
logs: []
};
const runProcess = spawn('docker', [command, ...args], {
...options,
detached: true
});
task.process = runProcess;
// 收集日志
if (runProcess.stdout) {
runProcess.stdout.on('data', (data) => {
const log = data.toString();
task.logs.push(log);
});
}
if (runProcess.stderr) {
runProcess.stderr.on('data', (data) => {
const log = data.toString();
task.logs.push(log);
});
}
// 监听结束
runProcess.on('close', (code) => {
task.status = code === 0 ? 'completed' : 'failed';
task.endTime = Date.now();
task.exitCode = code;
task.process = undefined; // 清理 process 引用
});
runProcess.on('error', (err) => {
task.status = 'failed';
task.logs.push(`Process error: ${err.message}`);
task.endTime = Date.now();
task.process = undefined;
});
// 让进程在后台运行
runProcess.unref();
this.tasks.set(taskId, task);
return taskId;
}
/**
* 停止任务
* @param taskId 任务 ID
*/
public stopTask(taskId: string): boolean {
const task = this.tasks.get(taskId);
if (task && task.process && task.status === 'running') {
try {
if (task.process.pid) {
process.kill(-task.process.pid); // 尝试 kill 进程组
} else {
task.process.kill();
}
task.status = 'failed'; // 被强行停止视为 failed 或者可以加个 stopped 状态
task.logs.push('Task stopped by user.');
return true;
} catch (e) {
console.error(`Failed to stop task ${taskId}:`, e);
return false;
}
}
return false;
}
/**
* 获取任务日志
* @param taskId 任务 ID
*/
public getTaskLogs(taskId: string): string {
const task = this.tasks.get(taskId);
return task ? task.logs.join('') : '';
}
/**
* 获取任务状态
* @param taskId 任务 ID
*/
public getTaskStatus(taskId: string): DockerTask | null {
const task = this.tasks.get(taskId);
if (!task) return null;
// 返回不带 process 对象的副本,避免循环引用等问题
const { process, ...taskInfo } = task;
return taskInfo as DockerTask;
}
/**
* 删除任务
*/
public deleteTask(taskId: string): boolean {
this.tasks.delete(taskId);
return true;
}
}
export const dockerService = new DockerService();
// 使用示例
const taskId = dockerService.runTask('run', ['--rm', 'alpine', 'echo', 'Hello, Docker in Docker!']);
console.log(`Started Docker task with ID: ${taskId}`);
setTimeout(() => {
const status = dockerService.getTaskStatus(taskId);
const logs = dockerService.getTaskLogs(taskId);
console.log(`Task Status: ${JSON.stringify(status, null, 2)}`);
console.log(`Task Logs:\n${logs}`);
}, 3000);
典型使用场景
Android Cordova 构建、ffmpeg 转码、PDF 生成、OCR 识别、AI 模型推理、大型依赖任务
带来的优势
构建环境独立、稳定可复现、维护简单、不影响主服务镜像体积
这些步骤往往:
- 依赖复杂
- 容易污染主业务容器
- 版本变化频繁
- 需要在隔离环境执行
一个专用的 build 镜像是现代 DevOps 的标准方式。
安全警告
挂载 docker.sock 等于:
给容器 root 权限控制宿主机所有 Docker 功能
这本质上等同于宿主机 root 权限
安全上:
- 不允许不可信容器挂载
docker.sock
- 不允许公网暴露
- 尽量只给可信后端使用
- 可通过
docker-socket-proxy 做访问权限隔离