963 lines · 22.8 KB · TypeScript
RawDownload
import fs from "node:fs";import path from "node:path";import type { Readable } from "node:stream";import { docker, paths } from "@dokploy/server/constants";import type { Compose } from "@dokploy/server/services/compose";import type { ContainerInfo, ResourceRequirements } from "dockerode";import { parse } from "dotenv";import { quote } from "shell-quote";import type { ApplicationNested } from "../builders";import type { LibsqlNested } from "../databases/libsql";import type { MariadbNested } from "../databases/mariadb";import type { MongoNested } from "../databases/mongo";import type { MysqlNested } from "../databases/mysql";import type { PostgresNested } from "../databases/postgres";import type { RedisNested } from "../databases/redis";import { execAsync, execAsyncRemote } from "../process/execAsync";import { spawnAsync } from "../process/spawnAsync";import { getRemoteDocker } from "../servers/remote-docker"; interface RegistryAuth {	username: string;	password: string;	registryUrl: string;} export const pullImage = async (	dockerImage: string,	onData?: (data: any) => void,	authConfig?: Partial<RegistryAuth>,): Promise<void> => {	try {		if (!dockerImage) {			throw new Error("Docker image not found");		} 		if (authConfig?.username && authConfig?.password) {			await spawnAsync(				"docker",				[					"login",					authConfig.registryUrl || "",					"-u",					authConfig.username,					"-p",					authConfig.password,				],				onData,			);		}		await spawnAsync("docker", ["pull", dockerImage], onData);	} catch (error) {		throw error;	}}; export const pullRemoteImage = async (	dockerImage: string,	serverId: string,	onData?: (data: any) => void,	authConfig?: Partial<RegistryAuth>,): Promise<void> => {	try {		if (!dockerImage) {			throw new Error("Docker image not found");		} 		const remoteDocker = await getRemoteDocker(serverId); 		await new Promise((resolve, reject) => {			remoteDocker.pull(				dockerImage,				{ authconfig: authConfig },				(err, stream) => {					if (err) {						reject(err);						return;					} 					remoteDocker.modem.followProgress(						stream as Readable,						(err: Error | null, res) => {							if (!err) {								resolve(res);							}							if (err) {								reject(err);							}						},						(event) => {							onData?.(event);						},					);				},			);		});	} catch (error) {		throw error;	}}; export const containerExists = async (containerName: string) => {	const container = docker.getContainer(containerName);	try {		await container.inspect();		return true;	} catch {		return false;	}}; export const stopService = async (appName: string) => {	try {		await execAsync(`docker service scale ${appName}=0 `);	} catch (error) {		console.error(error);		return error;	}}; export const stopServiceRemote = async (serverId: string, appName: string) => {	try {		await execAsyncRemote(serverId, `docker service scale ${appName}=0 `);	} catch (error) {		console.error(error);		return error;	}}; export const getContainerByName = (name: string): Promise<ContainerInfo> => {	const opts = {		limit: 1,		filters: {			name: [name],		},	};	return new Promise((resolve, reject) => {		docker.listContainers(opts, (err, containers) => {			if (err) {				reject(err);			} else if (containers?.length === 0) {				reject(new Error(`No container found with name: ${name}`));			} else if (containers && containers?.length > 0 && containers[0]) {				resolve(containers[0]);			}		});	});}; /** * Docker commands sent using this method are held in a hold when Docker is busy. * * https://github.com/Dokploy/dokploy/pull/3064 */export const dockerSafeExec = (exec: string) => `CHECK_INTERVAL=10 echo "Preparing for execution..." while true; do    PROCESSES=$(ps aux | grep -E "^.*docker [A-Za-z]" | grep -v grep)     if [ -z "$PROCESSES" ]; then        echo "Docker is idle. Starting execution..."        break    else        echo "Docker is busy. Will check again in $CHECK_INTERVAL seconds..."        sleep $CHECK_INTERVAL    fidone ${exec} echo "Execution completed."`; const cleanupCommands = {	containers: "docker container prune --force",	images: "docker image prune --all --force",	volumes: "docker volume prune --all --force",	builders: "docker builder prune --all --force",	system: "docker system prune --all --force",}; export const cleanupContainers = async (serverId?: string) => {	try {		const command = cleanupCommands.containers; 		if (serverId) {			await execAsyncRemote(serverId, dockerSafeExec(command));		} else {			await execAsync(dockerSafeExec(command));		}	} catch (error) {		console.error(error); 		throw error;	}}; export const cleanupImages = async (serverId?: string) => {	try {		const command = cleanupCommands.images; 		if (serverId) {			await execAsyncRemote(serverId, dockerSafeExec(command));		} else await execAsync(dockerSafeExec(command));	} catch (error) {		console.error(error); 		throw error;	}}; export const cleanupVolumes = async (serverId?: string) => {	try {		const command = cleanupCommands.volumes; 		if (serverId) {			await execAsyncRemote(serverId, dockerSafeExec(command));		} else {			await execAsync(dockerSafeExec(command));		}	} catch (error) {		console.error(error); 		throw error;	}}; export const cleanupBuilders = async (serverId?: string) => {	try {		const command = cleanupCommands.builders; 		if (serverId) {			await execAsyncRemote(serverId, dockerSafeExec(command));		} else {			await execAsync(dockerSafeExec(command));		}	} catch (error) {		console.error(error); 		throw error;	}}; export const cleanupSystem = async (serverId?: string) => {	try {		const command = cleanupCommands.system; 		if (serverId) {			await execAsyncRemote(serverId, dockerSafeExec(command));		} else {			await execAsync(dockerSafeExec(command));		}	} catch (error) {		console.error(error); 		throw error;	}}; export interface DockerDiskUsageItem {	type: string;	totalCount: number;	active: number;	size: string;	reclaimable: string;	sizeBytes: number;} const parseSizeToBytes = (size: string): number => {	const match = size.match(/^([\d.]+)\s*([KMGT]?B)$/i);	if (!match) return 0;	const value = Number.parseFloat(match[1] as string);	const unit = (match[2] as string).toUpperCase();	const multipliers: Record<string, number> = {		B: 1,		KB: 1024,		MB: 1024 ** 2,		GB: 1024 ** 3,		TB: 1024 ** 4,	};	return value * (multipliers[unit] || 0);}; export const getDockerDiskUsage = async (): Promise<DockerDiskUsageItem[]> => {	const command = "docker system df --format '{{json .}}'";	const { stdout } = await execAsync(command); 	const lines = stdout.trim().split("\n").filter(Boolean);	return lines.map((line) => {		const data = JSON.parse(line);		return {			type: data.Type,			totalCount: Number.parseInt(data.TotalCount, 10) || 0,			active: Number.parseInt(data.Active, 10) || 0,			size: data.Size,			reclaimable: data.Reclaimable,			sizeBytes: parseSizeToBytes(data.Size),		};	});}; /** * Volume cleanup should always be performed manually by the user. The reason is that during automatic cleanup, a volume may be deleted due to a stopped container, which is a dangerous situation. * * https://github.com/Dokploy/dokploy/pull/3267 */const excludedCleanupAllCommands: (keyof typeof cleanupCommands)[] = [	"volumes",]; export const cleanupAll = async (serverId?: string) => {	for (const [key, command] of Object.entries(cleanupCommands) as [		keyof typeof cleanupCommands,		string,	][]) {		if (excludedCleanupAllCommands.includes(key)) continue; 		try {			if (serverId) {				await execAsyncRemote(serverId, dockerSafeExec(command));			} else {				await execAsync(dockerSafeExec(command));			}		} catch {}	}}; export const cleanupAllBackground = async (serverId?: string) => {	Promise.allSettled(		(			Object.entries(cleanupCommands) as [				keyof typeof cleanupCommands,				string,			][]		)			.filter(([key]) => !excludedCleanupAllCommands.includes(key))			.map(async ([, command]) => {				if (serverId) {					await execAsyncRemote(serverId, dockerSafeExec(command));				} else {					await execAsync(dockerSafeExec(command));				}			}),	)		.then((results) => {			const failed = results.filter((r) => r.status === "rejected");			if (failed.length > 0) {				console.error(`Docker cleanup: ${failed.length} operations failed`);			} else {				console.log("Docker cleanup completed successfully");			}		})		.catch((error) => console.error("Error in cleanup:", error)); 	return {		status: "scheduled",		message: "Docker cleanup has been initiated in the background",	};}; export const startService = async (appName: string) => {	try {		await execAsync(`docker service scale ${appName}=1 `);	} catch (error) {		console.error(error);		throw error;	}}; export const startServiceRemote = async (serverId: string, appName: string) => {	try {		await execAsyncRemote(serverId, `docker service scale ${appName}=1 `);	} catch (error) {		console.error(error);		throw error;	}}; export const removeService = async (	appName: string,	serverId?: string | null,	_deleteVolumes = false,) => {	try {		const command = `docker service rm ${appName}`; 		if (serverId) {			await execAsyncRemote(serverId, command);		} else {			await execAsync(command);		}	} catch (error) {		return error;	}}; export const prepareEnvironmentVariables = (	serviceEnv: string | null,	projectEnv?: string | null,	environmentEnv?: string | null,) => {	const projectVars = parse(projectEnv ?? "");	const environmentVars = parse(environmentEnv ?? "");	const serviceVars = parse(serviceEnv ?? ""); 	const resolvedVars = Object.entries(serviceVars).map(([key, value]) => {		let resolvedValue = value; 		// Replace project variables		if (projectVars) {			resolvedValue = resolvedValue.replace(				/\$\{\{project\.(.*?)\}\}/g,				(_, ref) => {					if (projectVars[ref] !== undefined) {						return projectVars[ref];					}					throw new Error(						`Invalid project environment variable: project.${ref}`,					);				},			);		} 		// Replace environment variables		if (environmentVars) {			resolvedValue = resolvedValue.replace(				/\$\{\{environment\.(.*?)\}\}/g,				(_, ref) => {					if (environmentVars[ref] !== undefined) {						return environmentVars[ref];					}					throw new Error(`Invalid environment variable: environment.${ref}`);				},			);		} 		// Replace self-references (service variables)		resolvedValue = resolvedValue.replace(/\$\{\{(.*?)\}\}/g, (_, ref) => {			if (serviceVars[ref] !== undefined) {				return serviceVars[ref];			}			throw new Error(`Invalid service environment variable: ${ref}`);		}); 		return `${key}=${resolvedValue}`;	}); 	return resolvedVars;}; export const prepareEnvironmentVariablesForShell = (	serviceEnv: string | null,	projectEnv?: string | null,	environmentEnv?: string | null,): string[] => {	const envVars = prepareEnvironmentVariables(		serviceEnv,		projectEnv,		environmentEnv,	);	// Using shell-quote library to properly escape shell arguments	// This is the standard way to handle special characters in shell commands	return envVars.map((env) => quote([env]));}; export const parseEnvironmentKeyValuePair = (	pair: string,): [string, string] => {	const [key, ...valueParts] = pair.split("=");	if (!key || !valueParts.length) {		throw new Error(`Invalid environment variable pair: ${pair}`);	} 	return [key, valueParts.join("=")];}; export const getEnvironmentVariablesObject = (	input: string | null,	projectEnv?: string | null,	environmentEnv?: string | null,) => {	const envs = prepareEnvironmentVariables(input, projectEnv, environmentEnv); 	const jsonObject: Record<string, string> = {}; 	for (const pair of envs) {		const [key, value] = parseEnvironmentKeyValuePair(pair);		if (key && value) {			jsonObject[key] = value;		}	} 	return jsonObject;}; export const generateVolumeMounts = (mounts: ApplicationNested["mounts"]) => {	if (!mounts || mounts.length === 0) {		return [];

Lines 1–500 of 963.