| | #!/usr/bin/env node |
| | |
| | |
| | |
| | |
| | |
| | |
| | |
| |
|
| | import { $, echo, fs, chalk, os } from 'zx'; |
| | import { fileURLToPath } from 'url'; |
| | import path from 'path'; |
| |
|
| | |
| | $.verbose = false; |
| | process.env.FORCE_COLOR = '1'; |
| |
|
| | |
| |
|
| | |
| | |
| | |
| | |
| | function getDockerPlatform() { |
| | const arch = os.arch(); |
| | const dockerArch = { |
| | x64: 'amd64', |
| | arm64: 'arm64', |
| | }[arch]; |
| |
|
| | if (!dockerArch) { |
| | throw new Error(`Unsupported architecture: ${arch}. Only x64 and arm64 are supported.`); |
| | } |
| |
|
| | return `linux/${dockerArch}`; |
| | } |
| |
|
| | |
| | |
| | |
| | |
| | |
| | function formatDuration(ms) { |
| | return `${Math.floor(ms / 1000)}s`; |
| | } |
| |
|
| | |
| | |
| | |
| | |
| | |
| | async function getImageSize(imageName) { |
| | try { |
| | const { stdout } = await $`docker images ${imageName} --format "{{.Size}}"`; |
| | return stdout.trim(); |
| | } catch { |
| | return 'Unknown'; |
| | } |
| | } |
| |
|
| | |
| | |
| | |
| | |
| | |
| | async function commandExists(command) { |
| | try { |
| | await $`command -v ${command}`; |
| | return true; |
| | } catch { |
| | return false; |
| | } |
| | } |
| |
|
| | const SupportedContainerEngines = (['docker', 'podman']); |
| |
|
| | |
| | |
| | |
| | |
| | async function isDockerPodmanShim() { |
| | try { |
| | const { stdout } = await $`docker version`; |
| | return stdout.toLowerCase().includes('podman'); |
| | } catch { |
| | return false; |
| | } |
| | } |
| | |
| | |
| | |
| | async function getContainerEngine() { |
| | |
| | const override = process.env.CONTAINER_ENGINE?.toLowerCase(); |
| | if (override && (SupportedContainerEngines).includes(override)) { |
| | return (override); |
| | } |
| |
|
| | const hasDocker = await commandExists('docker'); |
| | const hasPodman = await commandExists('podman'); |
| |
|
| | if (hasDocker) { |
| | |
| | if (hasPodman && (await isDockerPodmanShim())) { |
| | return 'podman'; |
| | } |
| | return 'docker'; |
| | } |
| |
|
| | if (hasPodman) return 'podman'; |
| |
|
| | throw new Error('No supported container engine found. Please install Docker or Podman.'); |
| | } |
| |
|
| | |
| |
|
| | const __filename = fileURLToPath(import.meta.url); |
| | const __dirname = path.dirname(__filename); |
| | const isInScriptsDir = path.basename(__dirname) === 'scripts'; |
| | const rootDir = isInScriptsDir ? path.join(__dirname, '..') : __dirname; |
| |
|
| | const config = { |
| | n8n: { |
| | dockerfilePath: path.join(rootDir, 'docker/images/n8n/Dockerfile'), |
| | imageBaseName: process.env.IMAGE_BASE_NAME || 'n8nio/n8n', |
| | imageTag: process.env.IMAGE_TAG || 'local', |
| | get fullImageName() { |
| | return `${this.imageBaseName}:${this.imageTag}`; |
| | }, |
| | }, |
| | runners: { |
| | dockerfilePath: path.join(rootDir, 'docker/images/runners/Dockerfile'), |
| | imageBaseName: process.env.RUNNERS_IMAGE_BASE_NAME || 'n8nio/runners', |
| | get imageTag() { |
| | |
| | return config.n8n.imageTag; |
| | }, |
| | get fullImageName() { |
| | return `${this.imageBaseName}:${this.imageTag}`; |
| | }, |
| | }, |
| | buildContext: rootDir, |
| | compiledAppDir: path.join(rootDir, 'compiled'), |
| | compiledTaskRunnerDir: path.join(rootDir, 'dist', 'task-runner-javascript'), |
| | }; |
| |
|
| | |
| |
|
| | const platform = getDockerPlatform(); |
| |
|
| | async function main() { |
| | echo(chalk.blue.bold('===== Docker Build for n8n & Runners =====')); |
| | echo(`INFO: n8n Image: ${config.n8n.fullImageName}`); |
| | echo(`INFO: Runners Image: ${config.runners.fullImageName}`); |
| | echo(`INFO: Platform: ${platform}`); |
| | echo(chalk.gray('-'.repeat(47))); |
| |
|
| | await checkPrerequisites(); |
| |
|
| | |
| | const n8nBuildTime = await buildDockerImage({ |
| | name: 'n8n', |
| | dockerfilePath: config.n8n.dockerfilePath, |
| | fullImageName: config.n8n.fullImageName, |
| | }); |
| |
|
| | |
| | const runnersBuildTime = await buildDockerImage({ |
| | name: 'runners', |
| | dockerfilePath: config.runners.dockerfilePath, |
| | fullImageName: config.runners.fullImageName, |
| | }); |
| |
|
| | |
| | const n8nImageSize = await getImageSize(config.n8n.fullImageName); |
| | const runnersImageSize = await getImageSize(config.runners.fullImageName); |
| |
|
| | |
| | displaySummary([ |
| | { |
| | imageName: config.n8n.fullImageName, |
| | platform, |
| | size: n8nImageSize, |
| | buildTime: n8nBuildTime, |
| | }, |
| | { |
| | imageName: config.runners.fullImageName, |
| | platform, |
| | size: runnersImageSize, |
| | buildTime: runnersBuildTime, |
| | }, |
| | ]); |
| | } |
| |
|
| | async function checkPrerequisites() { |
| | if (!(await fs.pathExists(config.compiledAppDir))) { |
| | echo(chalk.red(`Error: Compiled app directory not found at ${config.compiledAppDir}`)); |
| | echo(chalk.yellow('Please run build-n8n.mjs first!')); |
| | process.exit(1); |
| | } |
| |
|
| | if (!(await fs.pathExists(config.compiledTaskRunnerDir))) { |
| | echo(chalk.red(`Error: Task runner directory not found at ${config.compiledTaskRunnerDir}`)); |
| | echo(chalk.yellow('Please run build-n8n.mjs first!')); |
| | process.exit(1); |
| | } |
| |
|
| | |
| | if (!(await commandExists('docker')) && !(await commandExists('podman'))) { |
| | echo(chalk.red('Error: Neither Docker nor Podman is installed or in PATH')); |
| | process.exit(1); |
| | } |
| | } |
| |
|
| | async function buildDockerImage({ name, dockerfilePath, fullImageName }) { |
| | const startTime = Date.now(); |
| | const containerEngine = await getContainerEngine(); |
| | echo(chalk.yellow(`INFO: Building ${name} Docker image using ${containerEngine}...`)); |
| |
|
| | try { |
| | if (containerEngine === 'podman') { |
| | const { stdout } = await $`podman build \ |
| | --platform ${platform} \ |
| | --build-arg TARGETPLATFORM=${platform} \ |
| | -t ${fullImageName} \ |
| | -f ${dockerfilePath} \ |
| | ${config.buildContext}`; |
| | echo(stdout); |
| | } else { |
| | |
| | |
| | const { stdout } = await $`docker buildx build \ |
| | --platform ${platform} \ |
| | --build-arg TARGETPLATFORM=${platform} \ |
| | -t ${fullImageName} \ |
| | -f ${dockerfilePath} \ |
| | --load \ |
| | ${config.buildContext}`; |
| | echo(stdout); |
| | } |
| |
|
| | return formatDuration(Date.now() - startTime); |
| | } catch (error) { |
| | echo(chalk.red(`ERROR: ${name} Docker build failed: ${error.stderr || error.message}`)); |
| | process.exit(1); |
| | } |
| | } |
| |
|
| | function displaySummary(images) { |
| | echo(''); |
| | echo(chalk.green.bold('═'.repeat(54))); |
| | echo(chalk.green.bold(' DOCKER BUILD COMPLETE')); |
| | echo(chalk.green.bold('═'.repeat(54))); |
| | for (const { imageName, platform, size, buildTime } of images) { |
| | echo(chalk.green(`✅ Image built: ${imageName}`)); |
| | echo(` Platform: ${platform}`); |
| | echo(` Size: ${size}`); |
| | echo(` Build time: ${buildTime}`); |
| | echo(''); |
| | } |
| | echo(chalk.green.bold('═'.repeat(54))); |
| | } |
| |
|
| | |
| |
|
| | main().catch((error) => { |
| | echo(chalk.red(`Unexpected error: ${error.message}`)); |
| | process.exit(1); |
| | }); |
| |
|