Merge branch 'canary' into feat/libsql

This commit is contained in:
Mauricio Siu
2026-03-19 11:20:22 -06:00
703 changed files with 398640 additions and 18827 deletions

View File

@@ -1,11 +1,11 @@
import { findComposeById } from "@dokploy/server/services/compose";
import { stringify } from "yaml";
import { execAsync, execAsyncRemote } from "../process/execAsync";
import { addAppNameToAllServiceNames } from "./collision/root-network";
import { generateRandomHash } from "./compose";
import { addSuffixToAllVolumes } from "./compose/volume";
import {
cloneCompose,
cloneComposeRemote,
loadDockerCompose,
loadDockerComposeRemote,
} from "./domain";
@@ -31,10 +31,11 @@ export const randomizeIsolatedDeploymentComposeFile = async (
) => {
const compose = await findComposeById(composeId);
const command = await cloneCompose(compose);
if (compose.serverId) {
await cloneComposeRemote(compose);
await execAsyncRemote(compose.serverId, command);
} else {
await cloneCompose(compose);
await execAsync(command);
}
let composeData: ComposeSpecification | null;

View File

@@ -18,7 +18,9 @@ export const randomizeComposeFile = async (
) => {
const compose = await findComposeById(composeId);
const composeFile = compose.composeFile;
const composeData = parse(composeFile) as ComposeSpecification;
const composeData = parse(composeFile, {
maxAliasCount: 10000,
}) as ComposeSpecification;
const randomSuffix = suffix || generateRandomHash();

View File

@@ -1,35 +1,16 @@
import fs, { existsSync, readFileSync } from "node:fs";
import { writeFile } from "node:fs/promises";
import { join } from "node:path";
import { paths } from "@dokploy/server/constants";
import type { Compose } from "@dokploy/server/services/compose";
import type { Domain } from "@dokploy/server/services/domain";
import { parse, stringify } from "yaml";
import { execAsyncRemote } from "../process/execAsync";
import {
cloneRawBitbucketRepository,
cloneRawBitbucketRepositoryRemote,
} from "../providers/bitbucket";
import {
cloneGitRawRepository,
cloneRawGitRepositoryRemote,
} from "../providers/git";
import {
cloneRawGiteaRepository,
cloneRawGiteaRepositoryRemote,
} from "../providers/gitea";
import {
cloneRawGithubRepository,
cloneRawGithubRepositoryRemote,
} from "../providers/github";
import {
cloneRawGitlabRepository,
cloneRawGitlabRepositoryRemote,
} from "../providers/gitlab";
import {
createComposeFileRaw,
createComposeFileRawRemote,
} from "../providers/raw";
import { cloneBitbucketRepository } from "../providers/bitbucket";
import { cloneGitRepository } from "../providers/git";
import { cloneGiteaRepository } from "../providers/gitea";
import { cloneGithubRepository } from "../providers/github";
import { cloneGitlabRepository } from "../providers/gitlab";
import { getCreateComposeFileCommand } from "../providers/raw";
import { randomizeDeployableSpecificationFile } from "./collision";
import { randomizeSpecificationFile } from "./compose";
import type {
@@ -40,35 +21,25 @@ import type {
import { encodeBase64 } from "./utils";
export const cloneCompose = async (compose: Compose) => {
let command = "set -e;";
const entity = {
...compose,
type: "compose" as const,
};
if (compose.sourceType === "github") {
await cloneRawGithubRepository(compose);
command += await cloneGithubRepository(entity);
} else if (compose.sourceType === "gitlab") {
await cloneRawGitlabRepository(compose);
command += await cloneGitlabRepository(entity);
} else if (compose.sourceType === "bitbucket") {
await cloneRawBitbucketRepository(compose);
command += await cloneBitbucketRepository(entity);
} else if (compose.sourceType === "git") {
await cloneGitRawRepository(compose);
command += await cloneGitRepository(entity);
} else if (compose.sourceType === "gitea") {
await cloneRawGiteaRepository(compose);
command += await cloneGiteaRepository(entity);
} else if (compose.sourceType === "raw") {
await createComposeFileRaw(compose);
}
};
export const cloneComposeRemote = async (compose: Compose) => {
if (compose.sourceType === "github") {
await cloneRawGithubRepositoryRemote(compose);
} else if (compose.sourceType === "gitlab") {
await cloneRawGitlabRepositoryRemote(compose);
} else if (compose.sourceType === "bitbucket") {
await cloneRawBitbucketRepositoryRemote(compose);
} else if (compose.sourceType === "git") {
await cloneRawGitRepositoryRemote(compose);
} else if (compose.sourceType === "gitea") {
await cloneRawGiteaRepositoryRemote(compose);
} else if (compose.sourceType === "raw") {
await createComposeFileRawRemote(compose);
command += getCreateComposeFileCommand(compose);
}
return command;
};
export const getComposePath = (compose: Compose) => {
@@ -92,7 +63,9 @@ export const loadDockerCompose = async (
if (existsSync(path)) {
const yamlStr = readFileSync(path, "utf8");
const parsedConfig = parse(yamlStr) as ComposeSpecification;
const parsedConfig = parse(yamlStr, {
maxAliasCount: 10000,
}) as ComposeSpecification;
return parsedConfig;
}
return null;
@@ -115,7 +88,9 @@ export const loadDockerComposeRemote = async (
return null;
}
if (!stdout) return null;
const parsedConfig = parse(stdout) as ComposeSpecification;
const parsedConfig = parse(stdout, {
maxAliasCount: 10000,
}) as ComposeSpecification;
return parsedConfig;
} catch {
return null;
@@ -135,52 +110,27 @@ export const writeDomainsToCompose = async (
compose: Compose,
domains: Domain[],
) => {
if (!domains.length) {
return;
}
const composeConverted = await addDomainToCompose(compose, domains);
const path = getComposePath(compose);
const composeString = stringify(composeConverted, { lineWidth: 1000 });
try {
await writeFile(path, composeString, "utf8");
} catch (error) {
throw error;
}
};
export const writeDomainsToComposeRemote = async (
compose: Compose,
domains: Domain[],
logPath: string,
) => {
if (!domains.length) {
return "";
}
try {
const composeConverted = await addDomainToCompose(compose, domains);
const path = getComposePath(compose);
if (!composeConverted) {
return `
echo "❌ Error: Compose file not found" >> ${logPath};
echo "❌ Error: Compose file not found";
exit 1;
`;
}
if (compose.serverId) {
const composeString = stringify(composeConverted, { lineWidth: 1000 });
const encodedContent = encodeBase64(composeString);
return `echo "${encodedContent}" | base64 -d > "${path}";`;
}
const composeString = stringify(composeConverted, { lineWidth: 1000 });
const encodedContent = encodeBase64(composeString);
return `echo "${encodedContent}" | base64 -d > "${path}";`;
} catch (error) {
// @ts-ignore
return `echo "❌ Has occured an error: ${error?.message || error}" >> ${logPath};
return `echo "❌ Has occurred an error: ${error?.message || error}";
exit 1;
`;
}
};
// (node:59875) MaxListenersExceededWarning: Possible EventEmitter memory leak detected. 11 SIGTERM listeners added to [process]. Use emitter.setMaxListeners() to increase limit
export const addDomainToCompose = async (
compose: Compose,
domains: Domain[],
@@ -190,12 +140,12 @@ export const addDomainToCompose = async (
let result: ComposeSpecification | null;
if (compose.serverId) {
result = await loadDockerComposeRemote(compose); // aca hay que ir al servidor e ir a traer el compose file al servidor
result = await loadDockerComposeRemote(compose);
} else {
result = await loadDockerCompose(compose);
}
if (!result || domains.length === 0) {
if (!result) {
return null;
}
@@ -214,10 +164,12 @@ export const addDomainToCompose = async (
for (const domain of domains) {
const { serviceName, https } = domain;
if (!serviceName) {
throw new Error("Service name not found");
throw new Error(`Domain "${domain.host}" is missing a service name`);
}
if (!result?.services?.[serviceName]) {
throw new Error(`The service ${serviceName} not found in the compose`);
throw new Error(
`Domain "${domain.host}" is attached to service "${serviceName}" which does not exist in the compose`,
);
}
const httpLabels = createDomainLabels(appName, domain, "web");
@@ -380,6 +332,7 @@ export const addDokployNetworkToService = (
) => {
let networks = networkService;
const network = "dokploy-network";
const defaultNetwork = "default";
if (!networks) {
networks = [];
}
@@ -388,10 +341,16 @@ export const addDokployNetworkToService = (
if (!networks.includes(network)) {
networks.push(network);
}
if (!networks.includes(defaultNetwork)) {
networks.push(defaultNetwork);
}
} else if (networks && typeof networks === "object") {
if (!(network in networks)) {
networks[network] = {};
}
if (!(defaultNetwork in networks)) {
networks[defaultNetwork] = {};
}
}
return networks;

View File

@@ -5,6 +5,7 @@ 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";
@@ -144,81 +145,177 @@ export const getContainerByName = (name: string): Promise<ContainerInfo> => {
});
});
};
export const cleanUpUnusedImages = async (serverId?: string) => {
/**
* 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
fi
done
${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 = "docker image prune --force";
const command = cleanupCommands.containers;
if (serverId) {
await execAsyncRemote(serverId, command);
await execAsyncRemote(serverId, dockerSafeExec(command));
} else {
await execAsync(command);
await execAsync(dockerSafeExec(command));
}
} catch (error) {
console.error(error);
throw error;
}
};
export const cleanStoppedContainers = async (serverId?: string) => {
export const cleanupImages = async (serverId?: string) => {
try {
const command = "docker container prune --force";
const command = cleanupCommands.images;
if (serverId) {
await execAsyncRemote(serverId, command);
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(command);
await execAsync(dockerSafeExec(command));
}
} catch (error) {
console.error(error);
throw error;
}
};
export const cleanUpUnusedVolumes = async (serverId?: string) => {
export const cleanupBuilders = async (serverId?: string) => {
try {
const command = "docker volume prune --force";
const command = cleanupCommands.builders;
if (serverId) {
await execAsyncRemote(serverId, command);
await execAsyncRemote(serverId, dockerSafeExec(command));
} else {
await execAsync(command);
await execAsync(dockerSafeExec(command));
}
} catch (error) {
console.error(error);
throw error;
}
};
export const cleanUpInactiveContainers = async () => {
export const cleanupSystem = async (serverId?: string) => {
try {
const containers = await docker.listContainers({ all: true });
const inactiveContainers = containers.filter(
(container) => container.State !== "running",
);
const command = cleanupCommands.system;
for (const container of inactiveContainers) {
await docker.getContainer(container.Id).remove({ force: true });
console.log(`Cleaning up inactive container: ${container.Id}`);
if (serverId) {
await execAsyncRemote(serverId, dockerSafeExec(command));
} else {
await execAsync(dockerSafeExec(command));
}
} catch (error) {
console.error("Error cleaning up inactive containers:", error);
console.error(error);
throw error;
}
};
export const cleanUpDockerBuilder = async (serverId?: string) => {
const command = "docker builder prune --all --force";
if (serverId) {
await execAsyncRemote(serverId, command);
} else {
await execAsync(command);
/**
* 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 cleanUpSystemPrune = async (serverId?: string) => {
const command = "docker system prune --force --volumes";
if (serverId) {
await execAsyncRemote(serverId, command);
} else {
await execAsync(command);
}
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) => {
@@ -311,6 +408,21 @@ export const prepareEnvironmentVariables = (
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] => {
@@ -395,19 +507,25 @@ export const generateConfigContainer = (
replicas,
mounts,
networkSwarm,
stopGracePeriodSwarm,
endpointSpecSwarm,
ulimitsSwarm,
} = application;
const sanitizedStopGracePeriodSwarm =
typeof stopGracePeriodSwarm === "bigint"
? Number(stopGracePeriodSwarm)
: stopGracePeriodSwarm;
const haveMounts = mounts && mounts.length > 0;
return {
...(healthCheckSwarm && {
HealthCheck: healthCheckSwarm,
}),
...(restartPolicySwarm
? {
RestartPolicy: restartPolicySwarm,
}
: {}),
...(restartPolicySwarm && {
RestartPolicy: restartPolicySwarm,
}),
...(placementSwarm
? {
Placement: placementSwarm,
@@ -445,6 +563,10 @@ export const generateConfigContainer = (
Order: "start-first",
},
}),
...(sanitizedStopGracePeriodSwarm !== null &&
sanitizedStopGracePeriodSwarm !== undefined && {
StopGracePeriod: sanitizedStopGracePeriodSwarm,
}),
...(networkSwarm
? {
Networks: networkSwarm,
@@ -452,6 +574,22 @@ export const generateConfigContainer = (
: {
Networks: [{ Target: "dokploy-network" }],
}),
...(endpointSpecSwarm && {
EndpointSpec: {
...(endpointSpecSwarm.Mode && { Mode: endpointSpecSwarm.Mode }),
Ports:
endpointSpecSwarm.Ports?.map((port) => ({
Protocol: (port.Protocol || "tcp") as "tcp" | "udp" | "sctp",
TargetPort: port.TargetPort || 0,
PublishedPort: port.PublishedPort || 0,
PublishMode: (port.PublishMode || "host") as "ingress" | "host",
})) || [],
},
}),
...(ulimitsSwarm &&
ulimitsSwarm.length > 0 && {
Ulimits: ulimitsSwarm,
}),
};
};
@@ -605,3 +743,177 @@ export const getComposeContainer = async (
throw error;
}
};
type ServiceHealthStatus = {
status: "healthy" | "unhealthy";
message?: string;
};
const checkSwarmServiceRunning = async (
serviceName: string,
): Promise<ServiceHealthStatus> => {
try {
const service = docker.getService(serviceName);
const info = await service.inspect();
const replicas = info.Spec?.Mode?.Replicated?.Replicas ?? 0;
if (replicas === 0) {
return {
status: "unhealthy",
message: "Service has 0 replicas configured",
};
}
// Check that at least one task is actually running
const tasks = await docker.listTasks({
filters: JSON.stringify({
service: [serviceName],
"desired-state": ["running"],
}),
});
const runningTask = tasks.find((t) => t.Status?.State === "running");
if (!runningTask) {
const latestTask = tasks[0];
const taskState = latestTask?.Status?.State ?? "unknown";
return {
status: "unhealthy",
message: `No running tasks (current state: ${taskState})`,
};
}
return { status: "healthy" };
} catch (error) {
return {
status: "unhealthy",
message: error instanceof Error ? error.message : "Service not found",
};
}
};
const getSwarmServiceContainerId = async (
serviceName: string,
): Promise<string | null> => {
try {
const tasks = await docker.listTasks({
filters: JSON.stringify({
service: [serviceName],
"desired-state": ["running"],
}),
});
const runningTask = tasks.find((t) => t.Status?.State === "running");
return runningTask?.Status?.ContainerStatus?.ContainerID ?? null;
} catch {
return null;
}
};
export const checkPostgresHealth = async (): Promise<ServiceHealthStatus> => {
const serviceCheck = await checkSwarmServiceRunning("dokploy-postgres");
if (serviceCheck.status === "unhealthy") {
return serviceCheck;
}
// Verify PostgreSQL actually accepts connections
const containerId = await getSwarmServiceContainerId("dokploy-postgres");
if (!containerId) {
return { status: "unhealthy", message: "Could not find running container" };
}
try {
const exec = await docker.getContainer(containerId).exec({
Cmd: ["pg_isready", "-U", "dokploy"],
AttachStdout: true,
AttachStderr: true,
});
const stream = await exec.start({});
const output = await new Promise<string>((resolve) => {
let data = "";
stream.on("data", (chunk: Buffer) => {
data += chunk.toString();
});
stream.on("end", () => resolve(data));
});
const inspectResult = await exec.inspect();
if (inspectResult.ExitCode !== 0) {
return {
status: "unhealthy",
message: `PostgreSQL not ready: ${output.trim()}`,
};
}
return { status: "healthy" };
} catch (error) {
return {
status: "unhealthy",
message:
error instanceof Error ? error.message : "Failed to check PostgreSQL",
};
}
};
export const checkRedisHealth = async (): Promise<ServiceHealthStatus> => {
const serviceCheck = await checkSwarmServiceRunning("dokploy-redis");
if (serviceCheck.status === "unhealthy") {
return serviceCheck;
}
// Verify Redis actually responds to PING
const containerId = await getSwarmServiceContainerId("dokploy-redis");
if (!containerId) {
return { status: "unhealthy", message: "Could not find running container" };
}
try {
const exec = await docker.getContainer(containerId).exec({
Cmd: ["redis-cli", "ping"],
AttachStdout: true,
AttachStderr: true,
});
const stream = await exec.start({});
const output = await new Promise<string>((resolve) => {
let data = "";
stream.on("data", (chunk: Buffer) => {
data += chunk.toString();
});
stream.on("end", () => resolve(data));
});
if (!output.includes("PONG")) {
return {
status: "unhealthy",
message: `Redis did not respond with PONG: ${output.trim()}`,
};
}
return { status: "healthy" };
} catch (error) {
return {
status: "unhealthy",
message: error instanceof Error ? error.message : "Failed to check Redis",
};
}
};
export const checkTraefikHealth = async (): Promise<ServiceHealthStatus> => {
// Traefik can run as a standalone container or a swarm service
try {
const container = docker.getContainer("dokploy-traefik");
const info = await container.inspect();
if (!info.State.Running) {
return {
status: "unhealthy",
message: "Container is not running",
};
}
return { status: "healthy" };
} catch {
// Not a standalone container, check as swarm service
return checkSwarmServiceRunning("dokploy-traefik");
}
};