Merge branch 'canary' into feat/introduce-monitoring-self-hosted-pay

This commit is contained in:
Mauricio Siu
2025-05-01 21:05:46 -06:00
102 changed files with 23839 additions and 984 deletions

View File

@@ -36,11 +36,11 @@
"@ai-sdk/mistral": "^1.0.6",
"@ai-sdk/openai": "^1.0.12",
"@ai-sdk/openai-compatible": "^0.0.13",
"@better-auth/utils": "0.2.3",
"@better-auth/utils": "0.2.4",
"@oslojs/encoding": "1.1.0",
"@oslojs/crypto": "1.0.1",
"drizzle-dbml-generator": "0.10.0",
"better-auth": "1.2.4",
"better-auth": "1.2.6",
"@faker-js/faker": "^8.4.1",
"@octokit/auth-app": "^6.0.4",
"@react-email/components": "^0.0.21",

View File

@@ -24,7 +24,7 @@ import { redirects } from "./redirects";
import { registry } from "./registry";
import { security } from "./security";
import { server } from "./server";
import { applicationStatus, certificateType } from "./shared";
import { applicationStatus, certificateType, triggerType } from "./shared";
import { sshKeys } from "./ssh-key";
import { generateAppName } from "./utils";
@@ -149,6 +149,7 @@ export const applications = pgTable("application", {
owner: text("owner"),
branch: text("branch"),
buildPath: text("buildPath").default("/"),
triggerType: triggerType("triggerType").default("push"),
autoDeploy: boolean("autoDeploy").$defaultFn(() => true),
// Gitlab
gitlabProjectId: integer("gitlabProjectId"),
@@ -182,6 +183,7 @@ export const applications = pgTable("application", {
onDelete: "set null",
},
),
enableSubmodules: boolean("enableSubmodules").notNull().default(false),
dockerfile: text("dockerfile"),
dockerContextPath: text("dockerContextPath"),
dockerBuildStage: text("dockerBuildStage"),
@@ -470,8 +472,12 @@ export const apiSaveGithubProvider = createSchema
buildPath: true,
githubId: true,
watchPaths: true,
enableSubmodules: true,
})
.required();
.required()
.extend({
triggerType: z.enum(["push", "tag"]).default("push"),
});
export const apiSaveGitlabProvider = createSchema
.pick({
@@ -484,6 +490,7 @@ export const apiSaveGitlabProvider = createSchema
gitlabProjectId: true,
gitlabPathNamespace: true,
watchPaths: true,
enableSubmodules: true,
})
.required();
@@ -496,6 +503,7 @@ export const apiSaveBitbucketProvider = createSchema
bitbucketId: true,
applicationId: true,
watchPaths: true,
enableSubmodules: true,
})
.required();
@@ -508,6 +516,7 @@ export const apiSaveGiteaProvider = createSchema
giteaRepository: true,
giteaId: true,
watchPaths: true,
enableSubmodules: true,
})
.required();
@@ -528,6 +537,7 @@ export const apiSaveGitProvider = createSchema
customGitBuildPath: true,
customGitUrl: true,
watchPaths: true,
enableSubmodules: true,
})
.required()
.merge(

View File

@@ -12,7 +12,7 @@ import { gitlab } from "./gitlab";
import { mounts } from "./mount";
import { projects } from "./project";
import { server } from "./server";
import { applicationStatus } from "./shared";
import { applicationStatus, triggerType } from "./shared";
import { sshKeys } from "./ssh-key";
import { generateAppName } from "./utils";
@@ -72,10 +72,12 @@ export const compose = pgTable("compose", {
),
command: text("command").notNull().default(""),
//
enableSubmodules: boolean("enableSubmodules").notNull().default(false),
composePath: text("composePath").notNull().default("./docker-compose.yml"),
suffix: text("suffix").notNull().default(""),
randomize: boolean("randomize").notNull().default(false),
isolatedDeployment: boolean("isolatedDeployment").notNull().default(false),
triggerType: triggerType("triggerType").default("push"),
composeStatus: applicationStatus("composeStatus").notNull().default("idle"),
projectId: text("projectId")
.notNull()

View File

@@ -12,3 +12,5 @@ export const certificateType = pgEnum("certificateType", [
"none",
"custom",
]);
export const triggerType = pgEnum("triggerType", ["push", "tag"]);

View File

@@ -50,6 +50,7 @@ export const users_temp = pgTable("user_temp", {
// Admin
serverIp: text("serverIp"),
certificateType: certificateType("certificateType").notNull().default("none"),
https: boolean("https").notNull().default(false),
host: text("host"),
letsEncryptEmail: text("letsEncryptEmail"),
sshPrivateKey: text("sshPrivateKey"),
@@ -203,10 +204,12 @@ export const apiAssignDomain = createSchema
host: true,
certificateType: true,
letsEncryptEmail: true,
https: true,
})
.required()
.partial({
letsEncryptEmail: true,
https: true,
});
export const apiUpdateDockerCleanup = createSchema

View File

@@ -201,7 +201,7 @@ const { handler, api } = betterAuth({
const host =
process.env.NODE_ENV === "development"
? "http://localhost:3000"
: "https://dokploy.com";
: "https://app.dokploy.com";
const inviteLink = `${host}/invitation?token=${data.id}`;
await sendEmail({

View File

@@ -356,6 +356,7 @@ export const deployRemoteCompose = async ({
deployment.logPath,
true,
);
console.log(command);
} else if (compose.sourceType === "raw") {
command += getCreateComposeFileCommand(compose, deployment.logPath);
} else if (compose.sourceType === "gitea") {

View File

@@ -144,7 +144,8 @@ export const updateMount = async (
await deleteFileMount(mountId);
await createFileMount(mountId);
}
return mount;
return await findMountById(mountId);
});
};

View File

@@ -76,7 +76,7 @@ CURRENT_USER=$USER
echo "Installing requirements for: OS: $OS_TYPE"
if [ $EUID != 0 ]; then
echo "Please run this script as root or with sudo ❌"
echo "Please run this script as root or with sudo ❌"
exit
fi
@@ -263,7 +263,7 @@ const setupMainDirectory = () => `
# Create the /etc/dokploy directory
mkdir -p /etc/dokploy
chmod 777 /etc/dokploy
echo "Directory /etc/dokploy created ✅"
fi
`;
@@ -276,16 +276,16 @@ export const setupSwarm = () => `
# Get IP address
get_ip() {
local ip=""
# Try IPv4 with multiple services
# First attempt: ifconfig.io
ip=\$(curl -4s --connect-timeout 5 https://ifconfig.io 2>/dev/null)
# Second attempt: icanhazip.com
if [ -z "\$ip" ]; then
ip=\$(curl -4s --connect-timeout 5 https://icanhazip.com 2>/dev/null)
fi
# Third attempt: ipecho.net
if [ -z "\$ip" ]; then
ip=\$(curl -4s --connect-timeout 5 https://ipecho.net/plain 2>/dev/null)
@@ -295,12 +295,12 @@ export const setupSwarm = () => `
if [ -z "\$ip" ]; then
# Try IPv6 with ifconfig.io
ip=\$(curl -6s --connect-timeout 5 https://ifconfig.io 2>/dev/null)
# Try IPv6 with icanhazip.com
if [ -z "\$ip" ]; then
ip=\$(curl -6s --connect-timeout 5 https://icanhazip.com 2>/dev/null)
fi
# Try IPv6 with ipecho.net
if [ -z "\$ip" ]; then
ip=\$(curl -6s --connect-timeout 5 https://ipecho.net/plain 2>/dev/null)
@@ -549,7 +549,7 @@ export const createTraefikInstance = () => {
sleep 8
echo "Traefik migrated to Standalone ✅"
fi
if docker inspect dokploy-traefik > /dev/null 2>&1; then
echo "Traefik already exists ✅"
else
@@ -577,7 +577,7 @@ const installNixpacks = () => `
if command_exists nixpacks; then
echo "Nixpacks already installed ✅"
else
export NIXPACKS_VERSION=1.29.1
export NIXPACKS_VERSION=1.35.0
bash -c "$(curl -fsSL https://nixpacks.com/install.sh)"
echo "Nixpacks version $NIXPACKS_VERSION installed ✅"
fi

View File

@@ -1,4 +1,4 @@
import { randomBytes } from "node:crypto";
import { randomBytes, createHmac } from "node:crypto";
import { existsSync } from "node:fs";
import { mkdir, readFile, writeFile } from "node:fs/promises";
import { join } from "node:path";
@@ -24,6 +24,12 @@ export interface Template {
domains: DomainSchema[];
}
export interface GenerateJWTOptions {
length?: number;
secret?: string;
payload?: Record<string, unknown> | undefined;
}
export const generateRandomDomain = ({
serverIp,
projectName,
@@ -61,8 +67,48 @@ export function generateBase64(bytes = 32): string {
return randomBytes(bytes).toString("base64");
}
export function generateJwt(length = 256): string {
return randomBytes(length).toString("hex");
function safeBase64(str: string): string {
return str.replace(/=/g, "").replace(/\+/g, "-").replace(/\//g, "_");
}
function objToJWTBase64(obj: any): string {
return safeBase64(
Buffer.from(JSON.stringify(obj), "utf8").toString("base64"),
);
}
export function generateJwt(options: GenerateJWTOptions = {}): string {
let { length, secret, payload = {} } = options;
if (length) {
return randomBytes(length).toString("hex");
}
const encodedHeader = objToJWTBase64({
alg: "HS256",
typ: "JWT",
});
if (!payload.iss) {
payload.iss = "dokploy";
}
if (!payload.iat) {
payload.iat = Math.floor(Date.now() / 1000);
}
if (!payload.exp) {
payload.exp = Math.floor(new Date("2030-01-01T00:00:00Z").getTime() / 1000);
}
const encodedPayload = objToJWTBase64({
iat: Math.floor(Date.now() / 1000),
exp: Math.floor(new Date("2030-01-01T00:00:00Z").getTime() / 1000),
...payload,
});
if (!secret) {
secret = randomBytes(32).toString("hex");
}
const signature = safeBase64(
createHmac("SHA256", secret)
.update(`${encodedHeader}.${encodedPayload}`)
.digest("base64"),
);
return `${encodedHeader}.${encodedPayload}.${signature}`;
}
/**

View File

@@ -65,7 +65,7 @@ export interface Template {
/**
* Process a string value and replace variables
*/
function processValue(
export function processValue(
value: string,
variables: Record<string, string>,
schema: Schema,
@@ -84,11 +84,11 @@ function processValue(
const length = Number.parseInt(varName.split(":")[1], 10) || 32;
return generateBase64(length);
}
if (varName.startsWith("password:")) {
const length = Number.parseInt(varName.split(":")[1], 10) || 16;
return generatePassword(length);
}
if (varName === "password") {
return generatePassword(16);
}
@@ -97,14 +97,31 @@ function processValue(
const length = Number.parseInt(varName.split(":")[1], 10) || 8;
return generateHash(length);
}
if (varName === "hash") {
return generateHash();
}
if (varName === "uuid") {
return crypto.randomUUID();
}
if (varName === "timestamp") {
if (varName === "timestamp" || varName === "timestampms") {
return Date.now().toString();
}
if (varName === "timestamps") {
return Math.round(Date.now() / 1000).toString();
}
if (varName.startsWith("timestampms:")) {
return new Date(varName.slice(12)).getTime().toString();
}
if (varName.startsWith("timestamps:")) {
return Math.round(
new Date(varName.slice(11)).getTime() / 1000,
).toString();
}
if (varName === "randomPort") {
return Math.floor(Math.random() * 65535).toString();
}
@@ -114,8 +131,34 @@ function processValue(
}
if (varName.startsWith("jwt:")) {
const length = Number.parseInt(varName.split(":")[1], 10) || 256;
return generateJwt(length);
const params: string[] = varName.split(":").slice(1);
if (params.length === 1 && params[0] && params[0].match(/^\d{1,3}$/)) {
return generateJwt({ length: Number.parseInt(params[0], 10) });
}
let [secret, payload] = params;
if (typeof payload === "string" && variables[payload]) {
payload = variables[payload];
}
if (
typeof payload === "string" &&
payload.startsWith("{") &&
payload.endsWith("}")
) {
try {
payload = JSON.parse(payload);
} catch (e) {
// If payload is not a valid JSON, invalid it
payload = undefined;
console.error("Invalid JWT payload", e);
}
}
if (typeof payload !== "object") {
payload = undefined;
}
return generateJwt({
secret: secret ? variables[secret] || secret : undefined,
payload: payload as any,
});
}
if (varName === "username") {
@@ -147,7 +190,7 @@ export function processVariables(
): Record<string, string> {
const variables: Record<string, string> = {};
// First pass: Process variables that don't depend on other variables
// First pass: Process some variables that don't depend on other variables
for (const [key, value] of Object.entries(template.variables)) {
if (typeof value !== "string") continue;
@@ -161,6 +204,8 @@ export function processVariables(
const match = value.match(/\${password:(\d+)}/);
const length = match?.[1] ? Number.parseInt(match[1], 10) : 16;
variables[key] = generatePassword(length);
} else if (value === "${hash}") {
variables[key] = generateHash();
} else if (value.startsWith("${hash:")) {
const match = value.match(/\${hash:(\d+)}/);
const length = match?.[1] ? Number.parseInt(match[1], 10) : 8;

View File

@@ -106,8 +106,8 @@ export const keepLatestNBackups = async (
backup.prefix,
);
// --include "*.sql.gz" ensures nothing else other than the db backup files are touched by rclone
const rcloneList = `rclone lsf ${rcloneFlags.join(" ")} --include "*.sql.gz" ${backupFilesPath}`;
// --include "*.sql.gz" or "*.zip" ensures nothing else other than the dokploy backup files are touched by rclone
const rcloneList = `rclone lsf ${rcloneFlags.join(" ")} --include "*${backup.databaseType === "web-server" ? ".zip" : ".sql.gz"}" ${backupFilesPath}`;
// when we pipe the above command with this one, we only get the list of files we want to delete
const sortAndPickUnwantedBackups = `sort -r | tail -n +$((${backup.keepLatestCount}+1)) | xargs -I{}`;
// this command deletes the files

View File

@@ -1,4 +1,3 @@
import path from "node:path";
import type { BackupSchedule } from "@dokploy/server/services/backup";
import type { Mariadb } from "@dokploy/server/services/mariadb";
import { findProjectById } from "@dokploy/server/services/project";
@@ -8,7 +7,7 @@ import {
} from "../docker/utils";
import { sendDatabaseBackupNotifications } from "../notifications/database-backup";
import { execAsync, execAsyncRemote } from "../process/execAsync";
import { getS3Credentials } from "./utils";
import { getS3Credentials, normalizeS3Path } from "./utils";
export const runMariadbBackup = async (
mariadb: Mariadb,
@@ -19,7 +18,7 @@ export const runMariadbBackup = async (
const { prefix, database } = backup;
const destination = backup.destination;
const backupFileName = `${new Date().toISOString()}.sql.gz`;
const bucketDestination = path.join(prefix, backupFileName);
const bucketDestination = `${normalizeS3Path(prefix)}${backupFileName}`;
try {
const rcloneFlags = getS3Credentials(destination);

View File

@@ -1,4 +1,3 @@
import path from "node:path";
import type { BackupSchedule } from "@dokploy/server/services/backup";
import type { Mongo } from "@dokploy/server/services/mongo";
import { findProjectById } from "@dokploy/server/services/project";
@@ -8,7 +7,7 @@ import {
} from "../docker/utils";
import { sendDatabaseBackupNotifications } from "../notifications/database-backup";
import { execAsync, execAsyncRemote } from "../process/execAsync";
import { getS3Credentials } from "./utils";
import { getS3Credentials, normalizeS3Path } from "./utils";
// mongodb://mongo:Bqh7AQl-PRbnBu@localhost:27017/?tls=false&directConnection=true
export const runMongoBackup = async (mongo: Mongo, backup: BackupSchedule) => {
@@ -17,7 +16,7 @@ export const runMongoBackup = async (mongo: Mongo, backup: BackupSchedule) => {
const { prefix, database } = backup;
const destination = backup.destination;
const backupFileName = `${new Date().toISOString()}.dump.gz`;
const bucketDestination = path.join(prefix, backupFileName);
const bucketDestination = `${normalizeS3Path(prefix)}${backupFileName}`;
try {
const rcloneFlags = getS3Credentials(destination);

View File

@@ -1,4 +1,3 @@
import path from "node:path";
import type { BackupSchedule } from "@dokploy/server/services/backup";
import type { MySql } from "@dokploy/server/services/mysql";
import { findProjectById } from "@dokploy/server/services/project";
@@ -8,7 +7,7 @@ import {
} from "../docker/utils";
import { sendDatabaseBackupNotifications } from "../notifications/database-backup";
import { execAsync, execAsyncRemote } from "../process/execAsync";
import { getS3Credentials } from "./utils";
import { getS3Credentials, normalizeS3Path } from "./utils";
export const runMySqlBackup = async (mysql: MySql, backup: BackupSchedule) => {
const { appName, databaseRootPassword, projectId, name } = mysql;
@@ -16,7 +15,7 @@ export const runMySqlBackup = async (mysql: MySql, backup: BackupSchedule) => {
const { prefix, database } = backup;
const destination = backup.destination;
const backupFileName = `${new Date().toISOString()}.sql.gz`;
const bucketDestination = path.join(prefix, backupFileName);
const bucketDestination = `${normalizeS3Path(prefix)}${backupFileName}`;
try {
const rcloneFlags = getS3Credentials(destination);

View File

@@ -1,4 +1,3 @@
import path from "node:path";
import type { BackupSchedule } from "@dokploy/server/services/backup";
import type { Postgres } from "@dokploy/server/services/postgres";
import { findProjectById } from "@dokploy/server/services/project";
@@ -8,7 +7,7 @@ import {
} from "../docker/utils";
import { sendDatabaseBackupNotifications } from "../notifications/database-backup";
import { execAsync, execAsyncRemote } from "../process/execAsync";
import { getS3Credentials } from "./utils";
import { getS3Credentials, normalizeS3Path } from "./utils";
export const runPostgresBackup = async (
postgres: Postgres,
@@ -20,7 +19,7 @@ export const runPostgresBackup = async (
const { prefix, database } = backup;
const destination = backup.destination;
const backupFileName = `${new Date().toISOString()}.sql.gz`;
const bucketDestination = path.join(prefix, backupFileName);
const bucketDestination = `${normalizeS3Path(prefix)}${backupFileName}`;
try {
const rcloneFlags = getS3Credentials(destination);
const rcloneDestination = `:s3:${destination.bucket}/${bucketDestination}`;

View File

@@ -36,6 +36,13 @@ export const removeScheduleBackup = (backupId: string) => {
currentJob?.cancel();
};
export const normalizeS3Path = (prefix: string) => {
// Trim whitespace and remove leading/trailing slashes
const normalizedPrefix = prefix.trim().replace(/^\/+|\/+$/g, "");
// Return empty string if prefix is empty, otherwise append trailing slash
return normalizedPrefix ? `${normalizedPrefix}/` : "";
};
export const getS3Credentials = (destination: Destination) => {
const { accessKey, secretAccessKey, region, endpoint, provider } =
destination;

View File

@@ -1,6 +1,6 @@
import type { BackupSchedule } from "@dokploy/server/services/backup";
import { execAsync } from "../process/execAsync";
import { getS3Credentials } from "./utils";
import { getS3Credentials, normalizeS3Path } from "./utils";
import { findDestinationById } from "@dokploy/server/services/destination";
import { IS_CLOUD, paths } from "@dokploy/server/constants";
import { mkdtemp } from "node:fs/promises";
@@ -18,18 +18,30 @@ export const runWebServerBackup = async (backup: BackupSchedule) => {
const { BASE_PATH } = paths();
const tempDir = await mkdtemp(join(tmpdir(), "dokploy-backup-"));
const backupFileName = `webserver-backup-${timestamp}.zip`;
const s3Path = `:s3:${destination.bucket}/${backup.prefix}${backupFileName}`;
const s3Path = `:s3:${destination.bucket}/${normalizeS3Path(backup.prefix)}${backupFileName}`;
try {
await execAsync(`mkdir -p ${tempDir}/filesystem`);
const postgresCommand = `docker exec $(docker ps --filter "name=dokploy-postgres" -q) pg_dump -v -Fc -U dokploy -d dokploy > ${tempDir}/database.sql`;
// First get the container ID
const { stdout: containerId } = await execAsync(
`docker ps --filter "name=dokploy-postgres" --filter "status=running" -q | head -n 1`,
);
if (!containerId) {
throw new Error("PostgreSQL container not found");
}
const postgresContainerId = containerId.trim();
const postgresCommand = `docker exec ${postgresContainerId} pg_dump -v -Fc -U dokploy -d dokploy > '${tempDir}/database.sql'`;
await execAsync(postgresCommand);
await execAsync(`cp -r ${BASE_PATH}/* ${tempDir}/filesystem/`);
await execAsync(
`cd ${tempDir} && zip -r ${backupFileName} database.sql filesystem/`,
// Zip all .sql files since we created more than one
`cd ${tempDir} && zip -r ${backupFileName} *.sql filesystem/ > /dev/null 2>&1`,
);
const uploadCommand = `rclone copyto ${rcloneFlags.join(" ")} "${tempDir}/${backupFileName}" "${s3Path}"`;

View File

@@ -84,7 +84,7 @@ export const buildRailpack = async (
for (const envVar of envVariables) {
const [key, value] = envVar.split("=");
if (key && value) {
buildArgs.push("--secret", `id=${key},env=${key}`);
buildArgs.push("--secret", `id=${key},env='${key}'`);
env[key] = value;
}
}
@@ -132,7 +132,7 @@ export const getRailpackCommand = (
];
for (const env of envVariables) {
prepareArgs.push("--env", env);
prepareArgs.push("--env", `'${env}'`);
}
// Calculate secrets hash for layer invalidation
@@ -164,7 +164,7 @@ export const getRailpackCommand = (
for (const envVar of envVariables) {
const [key, value] = envVar.split("=");
if (key && value) {
buildArgs.push("--secret", `id=${key},env=${key}`);
buildArgs.push("--secret", `id=${key},env='${key}'`);
exportEnvs.push(`export ${key}=${value}`);
}
}

View File

@@ -25,6 +25,12 @@ export const buildStatic = async (
].join("\n"),
);
createFile(
buildAppDirectory,
".dockerignore",
[".git", ".env", "Dockerfile", ".dockerignore"].join("\n"),
);
await buildCustomDocker(
{
...application,

View File

@@ -249,6 +249,11 @@ export const addDomainToCompose = async (
labels.unshift("traefik.enable=true");
}
labels.unshift(...httpLabels);
if (!compose.isolatedDeployment) {
if (!labels.includes("traefik.docker.network=dokploy-network")) {
labels.unshift("traefik.docker.network=dokploy-network");
}
}
}
if (!compose.isolatedDeployment) {

View File

@@ -40,68 +40,84 @@ export const sendDokployRestartNotifications = async () => {
const decorate = (decoration: string, text: string) =>
`${discord.decoration ? decoration : ""} ${text}`.trim();
await sendDiscordNotification(discord, {
title: decorate(">", "`✅` Dokploy Server Restarted"),
color: 0x57f287,
fields: [
{
name: decorate("`📅`", "Date"),
value: `<t:${unixDate}:D>`,
inline: true,
try {
await sendDiscordNotification(discord, {
title: decorate(">", "`✅` Dokploy Server Restarted"),
color: 0x57f287,
fields: [
{
name: decorate("`📅`", "Date"),
value: `<t:${unixDate}:D>`,
inline: true,
},
{
name: decorate("`⌚`", "Time"),
value: `<t:${unixDate}:t>`,
inline: true,
},
{
name: decorate("`❓`", "Type"),
value: "Successful",
inline: true,
},
],
timestamp: date.toISOString(),
footer: {
text: "Dokploy Restart Notification",
},
{
name: decorate("`⌚`", "Time"),
value: `<t:${unixDate}:t>`,
inline: true,
},
{
name: decorate("`❓`", "Type"),
value: "Successful",
inline: true,
},
],
timestamp: date.toISOString(),
footer: {
text: "Dokploy Restart Notification",
},
});
});
} catch (error) {
console.log(error);
}
}
if (gotify) {
const decorate = (decoration: string, text: string) =>
`${gotify.decoration ? decoration : ""} ${text}\n`;
await sendGotifyNotification(
gotify,
decorate("✅", "Dokploy Server Restarted"),
`${decorate("🕒", `Date: ${date.toLocaleString()}`)}`,
);
try {
await sendGotifyNotification(
gotify,
decorate("", "Dokploy Server Restarted"),
`${decorate("🕒", `Date: ${date.toLocaleString()}`)}`,
);
} catch (error) {
console.log(error);
}
}
if (telegram) {
await sendTelegramNotification(
telegram,
`<b>✅ Dokploy Server Restarted</b>\n\n<b>Date:</b> ${format(date, "PP")}\n<b>Time:</b> ${format(date, "pp")}`,
);
try {
await sendTelegramNotification(
telegram,
`<b>✅ Dokploy Server Restarted</b>\n\n<b>Date:</b> ${format(date, "PP")}\n<b>Time:</b> ${format(date, "pp")}`,
);
} catch (error) {
console.log(error);
}
}
if (slack) {
const { channel } = slack;
await sendSlackNotification(slack, {
channel: channel,
attachments: [
{
color: "#00FF00",
pretext: ":white_check_mark: *Dokploy Server Restarted*",
fields: [
{
title: "Time",
value: date.toLocaleString(),
short: true,
},
],
},
],
});
try {
await sendSlackNotification(slack, {
channel: channel,
attachments: [
{
color: "#00FF00",
pretext: ":white_check_mark: *Dokploy Server Restarted*",
fields: [
{
title: "Time",
value: date.toLocaleString(),
short: true,
},
],
},
],
});
} catch (error) {
console.log(error);
}
}
}
};

View File

@@ -1,9 +1,48 @@
import { exec } from "node:child_process";
import { exec, execFile } from "node:child_process";
import util from "node:util";
import { findServerById } from "@dokploy/server/services/server";
import { Client } from "ssh2";
export const execAsync = util.promisify(exec);
export const execFileAsync = async (
command: string,
args: string[],
options: { input?: string } = {},
): Promise<{ stdout: string; stderr: string }> => {
const child = execFile(command, args);
if (options.input && child.stdin) {
child.stdin.write(options.input);
child.stdin.end();
}
return new Promise((resolve, reject) => {
let stdout = "";
let stderr = "";
child.stdout?.on("data", (data) => {
stdout += data.toString();
});
child.stderr?.on("data", (data) => {
stderr += data.toString();
});
child.on("close", (code) => {
if (code === 0) {
resolve({ stdout, stderr });
} else {
reject(
new Error(`Command failed with code ${code}. Stderr: ${stderr}`),
);
}
});
child.on("error", reject);
});
};
export const execAsyncRemote = async (
serverId: string | null,
command: string,

View File

@@ -37,6 +37,7 @@ export const cloneBitbucketRepository = async (
bitbucketBranch,
bitbucketId,
bitbucket,
enableSubmodules,
} = entity;
if (!bitbucketId) {
@@ -53,25 +54,23 @@ export const cloneBitbucketRepository = async (
const cloneUrl = `https://${bitbucket?.bitbucketUsername}:${bitbucket?.appPassword}@${repoclone}`;
try {
writeStream.write(`\nCloning Repo ${repoclone} to ${outputPath}: ✅\n`);
await spawnAsync(
"git",
[
"clone",
"--branch",
bitbucketBranch!,
"--depth",
"1",
"--recurse-submodules",
cloneUrl,
outputPath,
"--progress",
],
(data) => {
if (writeStream.writable) {
writeStream.write(data);
}
},
);
const cloneArgs = [
"clone",
"--branch",
bitbucketBranch!,
"--depth",
"1",
...(enableSubmodules ? ["--recurse-submodules"] : []),
cloneUrl,
outputPath,
"--progress",
];
await spawnAsync("git", cloneArgs, (data) => {
if (writeStream.writable) {
writeStream.write(data);
}
});
writeStream.write(`\nCloned ${repoclone} to ${outputPath}: ✅\n`);
} catch (error) {
writeStream.write(`ERROR Clonning: ${error}: ❌`);
@@ -89,6 +88,7 @@ export const cloneRawBitbucketRepository = async (entity: Compose) => {
bitbucketOwner,
bitbucketBranch,
bitbucketId,
enableSubmodules,
} = entity;
if (!bitbucketId) {
@@ -106,17 +106,19 @@ export const cloneRawBitbucketRepository = async (entity: Compose) => {
const cloneUrl = `https://${bitbucketProvider?.bitbucketUsername}:${bitbucketProvider?.appPassword}@${repoclone}`;
try {
await spawnAsync("git", [
const cloneArgs = [
"clone",
"--branch",
bitbucketBranch!,
"--depth",
"1",
"--recurse-submodules",
...(enableSubmodules ? ["--recurse-submodules"] : []),
cloneUrl,
outputPath,
"--progress",
]);
];
await spawnAsync("git", cloneArgs);
} catch (error) {
throw error;
}
@@ -131,6 +133,7 @@ export const cloneRawBitbucketRepositoryRemote = async (compose: Compose) => {
bitbucketBranch,
bitbucketId,
serverId,
enableSubmodules,
} = compose;
if (!serverId) {
@@ -153,11 +156,11 @@ export const cloneRawBitbucketRepositoryRemote = async (compose: Compose) => {
const cloneUrl = `https://${bitbucketProvider?.bitbucketUsername}:${bitbucketProvider?.appPassword}@${repoclone}`;
try {
const command = `
const cloneCommand = `
rm -rf ${outputPath};
git clone --branch ${bitbucketBranch} --depth 1 --recurse-submodules ${cloneUrl} ${outputPath}
git clone --branch ${bitbucketBranch} --depth 1 ${enableSubmodules ? "--recurse-submodules" : ""} ${cloneUrl} ${outputPath}
`;
await execAsyncRemote(serverId, command);
await execAsyncRemote(serverId, cloneCommand);
} catch (error) {
throw error;
}
@@ -176,6 +179,7 @@ export const getBitbucketCloneCommand = async (
bitbucketBranch,
bitbucketId,
serverId,
enableSubmodules,
} = entity;
if (!serverId) {
@@ -207,7 +211,7 @@ export const getBitbucketCloneCommand = async (
const cloneCommand = `
rm -rf ${outputPath};
mkdir -p ${outputPath};
if ! git clone --branch ${bitbucketBranch} --depth 1 --recurse-submodules --progress ${cloneUrl} ${outputPath} >> ${logPath} 2>&1; then
if ! git clone --branch ${bitbucketBranch} --depth 1 ${enableSubmodules ? "--recurse-submodules" : ""} --progress ${cloneUrl} ${outputPath} >> ${logPath} 2>&1; then
echo "❌ [ERROR] Fail to clone the repository ${repoclone}" >> ${logPath};
exit 1;
fi

View File

@@ -17,12 +17,19 @@ export const cloneGitRepository = async (
customGitUrl?: string | null;
customGitBranch?: string | null;
customGitSSHKeyId?: string | null;
enableSubmodules?: boolean;
},
logPath: string,
isCompose = false,
) => {
const { SSH_PATH, COMPOSE_PATH, APPLICATIONS_PATH } = paths();
const { appName, customGitUrl, customGitBranch, customGitSSHKeyId } = entity;
const {
appName,
customGitUrl,
customGitBranch,
customGitSSHKeyId,
enableSubmodules,
} = entity;
if (!customGitUrl || !customGitBranch) {
throw new TRPCError({
@@ -70,19 +77,21 @@ export const cloneGitRepository = async (
}
const { port } = sanitizeRepoPathSSH(customGitUrl);
const cloneArgs = [
"clone",
"--branch",
customGitBranch,
"--depth",
"1",
...(enableSubmodules ? ["--recurse-submodules"] : []),
customGitUrl,
outputPath,
"--progress",
];
await spawnAsync(
"git",
[
"clone",
"--branch",
customGitBranch,
"--depth",
"1",
"--recurse-submodules",
customGitUrl,
outputPath,
"--progress",
],
cloneArgs,
(data) => {
if (writeStream.writable) {
writeStream.write(data);
@@ -114,6 +123,7 @@ export const getCustomGitCloneCommand = async (
customGitBranch?: string | null;
customGitSSHKeyId?: string | null;
serverId: string | null;
enableSubmodules: boolean;
},
logPath: string,
isCompose = false,
@@ -125,6 +135,7 @@ export const getCustomGitCloneCommand = async (
customGitBranch,
customGitSSHKeyId,
serverId,
enableSubmodules,
} = entity;
if (!customGitUrl || !customGitBranch) {
@@ -181,7 +192,7 @@ export const getCustomGitCloneCommand = async (
}
command.push(
`if ! git clone --branch ${customGitBranch} --depth 1 --recurse-submodules --progress ${customGitUrl} ${outputPath} >> ${logPath} 2>&1; then
`if ! git clone --branch ${customGitBranch} --depth 1 ${enableSubmodules ? "--recurse-submodules" : ""} --progress ${customGitUrl} ${outputPath} >> ${logPath} 2>&1; then
echo "❌ [ERROR] Fail to clone the repository ${customGitUrl}" >> ${logPath};
exit 1;
fi
@@ -261,8 +272,15 @@ export const cloneGitRawRepository = async (entity: {
customGitUrl?: string | null;
customGitBranch?: string | null;
customGitSSHKeyId?: string | null;
enableSubmodules?: boolean;
}) => {
const { appName, customGitUrl, customGitBranch, customGitSSHKeyId } = entity;
const {
appName,
customGitUrl,
customGitBranch,
customGitSSHKeyId,
enableSubmodules,
} = entity;
if (!customGitUrl || !customGitBranch) {
throw new TRPCError({
@@ -307,29 +325,26 @@ export const cloneGitRawRepository = async (entity: {
}
const { port } = sanitizeRepoPathSSH(customGitUrl);
await spawnAsync(
"git",
[
"clone",
"--branch",
customGitBranch,
"--depth",
"1",
"--recurse-submodules",
customGitUrl,
outputPath,
"--progress",
],
(_data) => {},
{
env: {
...process.env,
...(customGitSSHKeyId && {
GIT_SSH_COMMAND: `ssh -i ${temporalKeyPath}${port ? ` -p ${port}` : ""} -o UserKnownHostsFile=${knownHostsPath}`,
}),
},
const cloneArgs = [
"clone",
"--branch",
customGitBranch,
"--depth",
"1",
...(enableSubmodules ? ["--recurse-submodules"] : []),
customGitUrl,
outputPath,
"--progress",
];
await spawnAsync("git", cloneArgs, (_data) => {}, {
env: {
...process.env,
...(customGitSSHKeyId && {
GIT_SSH_COMMAND: `ssh -i ${temporalKeyPath}${port ? ` -p ${port}` : ""} -o UserKnownHostsFile=${knownHostsPath}`,
}),
},
);
});
} catch (error) {
throw error;
}
@@ -342,6 +357,7 @@ export const cloneRawGitRepositoryRemote = async (compose: Compose) => {
customGitUrl,
customGitSSHKeyId,
serverId,
enableSubmodules,
} = compose;
if (!serverId) {
@@ -396,7 +412,7 @@ export const cloneRawGitRepositoryRemote = async (compose: Compose) => {
}
command.push(
`if ! git clone --branch ${customGitBranch} --depth 1 --recurse-submodules --progress ${customGitUrl} ${outputPath} ; then
`if ! git clone --branch ${customGitBranch} --depth 1 ${enableSubmodules ? "--recurse-submodules" : ""} --progress ${customGitUrl} ${outputPath} ; then
echo "[ERROR] Fail to clone the repository ";
exit 1;
fi

View File

@@ -119,6 +119,7 @@ export const getGiteaCloneCommand = async (
giteaRepository,
serverId,
gitea,
enableSubmodules,
} = entity;
if (!serverId) {
@@ -155,7 +156,7 @@ export const getGiteaCloneCommand = async (
rm -rf ${outputPath};
mkdir -p ${outputPath};
if ! git clone --branch ${giteaBranch} --depth 1 --recurse-submodules ${cloneUrl} ${outputPath} >> ${logPath} 2>&1; then
if ! git clone --branch ${giteaBranch} --depth 1 ${enableSubmodules ? "--recurse-submodules" : ""} ${cloneUrl} ${outputPath} >> ${logPath} 2>&1; then
echo "❌ [ERROR] Failed to clone the repository ${repoClone}" >> ${logPath};
exit 1;
fi
@@ -174,7 +175,14 @@ export const cloneGiteaRepository = async (
const { APPLICATIONS_PATH, COMPOSE_PATH } = paths();
const writeStream = createWriteStream(logPath, { flags: "a" });
const { appName, giteaBranch, giteaId, giteaOwner, giteaRepository } = entity;
const {
appName,
giteaBranch,
giteaId,
giteaOwner,
giteaRepository,
enableSubmodules,
} = entity;
if (!giteaId) {
throw new TRPCError({
@@ -211,7 +219,7 @@ export const cloneGiteaRepository = async (
giteaBranch!,
"--depth",
"1",
"--recurse-submodules",
...(enableSubmodules ? ["--recurse-submodules"] : []),
cloneUrl,
outputPath,
"--progress",
@@ -232,7 +240,14 @@ export const cloneGiteaRepository = async (
};
export const cloneRawGiteaRepository = async (entity: Compose) => {
const { appName, giteaRepository, giteaOwner, giteaBranch, giteaId } = entity;
const {
appName,
giteaRepository,
giteaOwner,
giteaBranch,
giteaId,
enableSubmodules,
} = entity;
const { COMPOSE_PATH } = paths();
if (!giteaId) {
@@ -265,7 +280,7 @@ export const cloneRawGiteaRepository = async (entity: Compose) => {
giteaBranch!,
"--depth",
"1",
"--recurse-submodules",
...(enableSubmodules ? ["--recurse-submodules"] : []),
cloneUrl,
outputPath,
"--progress",
@@ -283,6 +298,7 @@ export const cloneRawGiteaRepositoryRemote = async (compose: Compose) => {
giteaBranch,
giteaId,
serverId,
enableSubmodules,
} = compose;
if (!serverId) {
@@ -307,7 +323,7 @@ export const cloneRawGiteaRepositoryRemote = async (compose: Compose) => {
try {
const command = `
rm -rf ${outputPath};
git clone --branch ${giteaBranch} --depth 1 ${cloneUrl} ${outputPath}
git clone --branch ${giteaBranch} --depth 1 ${enableSubmodules ? "--recurse-submodules" : ""} ${cloneUrl} ${outputPath}
`;
await execAsyncRemote(serverId, command);
} catch (error) {

View File

@@ -83,6 +83,7 @@ interface CloneGithubRepository {
repository: string | null;
logPath: string;
type?: "application" | "compose";
enableSubmodules: boolean;
}
export const cloneGithubRepository = async ({
logPath,
@@ -92,7 +93,8 @@ export const cloneGithubRepository = async ({
const isCompose = type === "compose";
const { APPLICATIONS_PATH, COMPOSE_PATH } = paths();
const writeStream = createWriteStream(logPath, { flags: "a" });
const { appName, repository, owner, branch, githubId } = entity;
const { appName, repository, owner, branch, githubId, enableSubmodules } =
entity;
if (!githubId) {
throw new TRPCError({
@@ -128,25 +130,23 @@ export const cloneGithubRepository = async ({
try {
writeStream.write(`\nClonning Repo ${repoclone} to ${outputPath}: ✅\n`);
await spawnAsync(
"git",
[
"clone",
"--branch",
branch!,
"--depth",
"1",
"--recurse-submodules",
cloneUrl,
outputPath,
"--progress",
],
(data) => {
if (writeStream.writable) {
writeStream.write(data);
}
},
);
const cloneArgs = [
"clone",
"--branch",
branch!,
"--depth",
"1",
...(enableSubmodules ? ["--recurse-submodules"] : []),
cloneUrl,
outputPath,
"--progress",
];
await spawnAsync("git", cloneArgs, (data) => {
if (writeStream.writable) {
writeStream.write(data);
}
});
writeStream.write(`\nCloned ${repoclone}: ✅\n`);
} catch (error) {
writeStream.write(`ERROR Clonning: ${error}: ❌`);
@@ -161,7 +161,15 @@ export const getGithubCloneCommand = async ({
type = "application",
...entity
}: CloneGithubRepository & { serverId: string }) => {
const { appName, repository, owner, branch, githubId, serverId } = entity;
const {
appName,
repository,
owner,
branch,
githubId,
serverId,
enableSubmodules,
} = entity;
const isCompose = type === "compose";
if (!serverId) {
throw new TRPCError({
@@ -216,7 +224,7 @@ export const getGithubCloneCommand = async ({
const cloneCommand = `
rm -rf ${outputPath};
mkdir -p ${outputPath};
if ! git clone --branch ${branch} --depth 1 --recurse-submodules --progress ${cloneUrl} ${outputPath} >> ${logPath} 2>&1; then
if ! git clone --branch ${branch} --depth 1 ${enableSubmodules ? "--recurse-submodules" : ""} --progress ${cloneUrl} ${outputPath} >> ${logPath} 2>&1; then
echo "❌ [ERROR] Fail to clone repository ${repoclone}" >> ${logPath};
exit 1;
fi
@@ -227,7 +235,8 @@ echo "Cloned ${repoclone} to ${outputPath}: ✅" >> ${logPath};
};
export const cloneRawGithubRepository = async (entity: Compose) => {
const { appName, repository, owner, branch, githubId } = entity;
const { appName, repository, owner, branch, githubId, enableSubmodules } =
entity;
if (!githubId) {
throw new TRPCError({
@@ -245,24 +254,33 @@ export const cloneRawGithubRepository = async (entity: Compose) => {
await recreateDirectory(outputPath);
const cloneUrl = `https://oauth2:${token}@${repoclone}`;
try {
await spawnAsync("git", [
const cloneArgs = [
"clone",
"--branch",
branch!,
"--depth",
"1",
"--recurse-submodules",
...(enableSubmodules ? ["--recurse-submodules"] : []),
cloneUrl,
outputPath,
"--progress",
]);
];
await spawnAsync("git", cloneArgs);
} catch (error) {
throw error;
}
};
export const cloneRawGithubRepositoryRemote = async (compose: Compose) => {
const { appName, repository, owner, branch, githubId, serverId } = compose;
const {
appName,
repository,
owner,
branch,
githubId,
serverId,
enableSubmodules,
} = compose;
if (!serverId) {
throw new TRPCError({
@@ -288,7 +306,7 @@ export const cloneRawGithubRepositoryRemote = async (compose: Compose) => {
try {
const command = `
rm -rf ${outputPath};
git clone --branch ${branch} --depth 1 ${cloneUrl} ${outputPath}
git clone --branch ${branch} --depth 1 ${enableSubmodules ? "--recurse-submodules" : ""} ${cloneUrl} ${outputPath}
`;
await execAsyncRemote(serverId, command);
} catch (error) {

View File

@@ -90,8 +90,14 @@ export const cloneGitlabRepository = async (
isCompose = false,
) => {
const writeStream = createWriteStream(logPath, { flags: "a" });
const { appName, gitlabBranch, gitlabId, gitlab, gitlabPathNamespace } =
entity;
const {
appName,
gitlabBranch,
gitlabId,
gitlab,
gitlabPathNamespace,
enableSubmodules,
} = entity;
if (!gitlabId) {
throw new TRPCError({
@@ -127,25 +133,23 @@ export const cloneGitlabRepository = async (
try {
writeStream.write(`\nClonning Repo ${repoclone} to ${outputPath}: ✅\n`);
await spawnAsync(
"git",
[
"clone",
"--branch",
gitlabBranch!,
"--depth",
"1",
"--recurse-submodules",
cloneUrl,
outputPath,
"--progress",
],
(data) => {
if (writeStream.writable) {
writeStream.write(data);
}
},
);
const cloneArgs = [
"clone",
"--branch",
gitlabBranch!,
"--depth",
"1",
...(enableSubmodules ? ["--recurse-submodules"] : []),
cloneUrl,
outputPath,
"--progress",
];
await spawnAsync("git", cloneArgs, (data) => {
if (writeStream.writable) {
writeStream.write(data);
}
});
writeStream.write(`\nCloned ${repoclone}: ✅\n`);
} catch (error) {
writeStream.write(`ERROR Clonning: ${error}: ❌`);
@@ -167,6 +171,7 @@ export const getGitlabCloneCommand = async (
gitlabId,
serverId,
gitlab,
enableSubmodules,
} = entity;
if (!serverId) {
@@ -222,7 +227,7 @@ export const getGitlabCloneCommand = async (
const cloneCommand = `
rm -rf ${outputPath};
mkdir -p ${outputPath};
if ! git clone --branch ${gitlabBranch} --depth 1 --recurse-submodules --progress ${cloneUrl} ${outputPath} >> ${logPath} 2>&1; then
if ! git clone --branch ${gitlabBranch} --depth 1 ${enableSubmodules ? "--recurse-submodules" : ""} --progress ${cloneUrl} ${outputPath} >> ${logPath} 2>&1; then
echo "❌ [ERROR] Fail to clone the repository ${repoclone}" >> ${logPath};
exit 1;
fi
@@ -264,7 +269,11 @@ export const getGitlabRepositories = async (gitlabId?: string) => {
const groupName = gitlabProvider.groupName?.toLowerCase();
if (groupName) {
return full_path.toLowerCase().includes(groupName) && kind === "group";
const isIncluded = groupName
.split(",")
.some((name) => full_path.toLowerCase().includes(name));
return isIncluded && kind === "group";
}
return kind === "user";
});
@@ -326,7 +335,13 @@ export const getGitlabBranches = async (input: {
};
export const cloneRawGitlabRepository = async (entity: Compose) => {
const { appName, gitlabBranch, gitlabId, gitlabPathNamespace } = entity;
const {
appName,
gitlabBranch,
gitlabId,
gitlabPathNamespace,
enableSubmodules,
} = entity;
if (!gitlabId) {
throw new TRPCError({
@@ -347,24 +362,32 @@ export const cloneRawGitlabRepository = async (entity: Compose) => {
const cloneUrl = `https://oauth2:${gitlabProvider?.accessToken}@${repoclone}`;
try {
await spawnAsync("git", [
const cloneArgs = [
"clone",
"--branch",
gitlabBranch!,
"--depth",
"1",
"--recurse-submodules",
...(enableSubmodules ? ["--recurse-submodules"] : []),
cloneUrl,
outputPath,
"--progress",
]);
];
await spawnAsync("git", cloneArgs);
} catch (error) {
throw error;
}
};
export const cloneRawGitlabRepositoryRemote = async (compose: Compose) => {
const { appName, gitlabPathNamespace, branch, gitlabId, serverId } = compose;
const {
appName,
gitlabPathNamespace,
branch,
gitlabId,
serverId,
enableSubmodules,
} = compose;
if (!serverId) {
throw new TRPCError({
@@ -388,7 +411,7 @@ export const cloneRawGitlabRepositoryRemote = async (compose: Compose) => {
try {
const command = `
rm -rf ${outputPath};
git clone --branch ${branch} --depth 1 --recurse-submodules ${cloneUrl} ${outputPath}
git clone --branch ${branch} --depth 1 ${enableSubmodules ? "--recurse-submodules" : ""} ${cloneUrl} ${outputPath}
`;
await execAsyncRemote(serverId, command);
} catch (error) {
@@ -431,7 +454,9 @@ export const testGitlabConnection = async (
const { full_path, kind } = repo.namespace;
if (groupName) {
return full_path.toLowerCase().includes(groupName) && kind === "group";
return groupName
.split(",")
.some((name) => full_path.toLowerCase().includes(name));
}
return kind === "user";
});

View File

@@ -45,7 +45,7 @@ export const restoreWebServerBackup = async (
// Extract backup
emit("Extracting backup...");
await execAsync(`cd ${tempDir} && unzip ${backupFile}`);
await execAsync(`cd ${tempDir} && unzip ${backupFile} > /dev/null 2>&1`);
// Restore filesystem first
emit("Restoring filesystem...");
@@ -83,44 +83,54 @@ export const restoreWebServerBackup = async (
throw new Error("Database file not found after extraction");
}
const { stdout: postgresContainer } = await execAsync(
`docker ps --filter "name=dokploy-postgres" --filter "status=running" -q | head -n 1`,
);
if (!postgresContainer) {
throw new Error("Dokploy Postgres container not found");
}
const postgresContainerId = postgresContainer.trim();
// Drop and recreate database
emit("Disconnecting all users from database...");
await execAsync(
`docker exec $(docker ps --filter "name=dokploy-postgres" -q) psql -U dokploy postgres -c "SELECT pg_terminate_backend(pg_stat_activity.pid) FROM pg_stat_activity WHERE pg_stat_activity.datname = 'dokploy' AND pid <> pg_backend_pid();"`,
`docker exec ${postgresContainerId} psql -U dokploy postgres -c "SELECT pg_terminate_backend(pg_stat_activity.pid) FROM pg_stat_activity WHERE pg_stat_activity.datname = 'dokploy' AND pid <> pg_backend_pid();"`,
);
emit("Dropping existing database...");
await execAsync(
`docker exec $(docker ps --filter "name=dokploy-postgres" -q) psql -U dokploy postgres -c "DROP DATABASE IF EXISTS dokploy;"`,
`docker exec ${postgresContainerId} psql -U dokploy postgres -c "DROP DATABASE IF EXISTS dokploy;"`,
);
emit("Creating fresh database...");
await execAsync(
`docker exec $(docker ps --filter "name=dokploy-postgres" -q) psql -U dokploy postgres -c "CREATE DATABASE dokploy;"`,
`docker exec ${postgresContainerId} psql -U dokploy postgres -c "CREATE DATABASE dokploy;"`,
);
// Copy the backup file into the container
emit("Copying backup file into container...");
await execAsync(
`docker cp ${tempDir}/database.sql $(docker ps --filter "name=dokploy-postgres" -q):/tmp/database.sql`,
`docker cp ${tempDir}/database.sql ${postgresContainerId}:/tmp/database.sql`,
);
// Verify file in container
emit("Verifying file in container...");
await execAsync(
`docker exec $(docker ps --filter "name=dokploy-postgres" -q) ls -l /tmp/database.sql`,
`docker exec ${postgresContainerId} ls -l /tmp/database.sql`,
);
// Restore from the copied file
emit("Running database restore...");
await execAsync(
`docker exec $(docker ps --filter "name=dokploy-postgres" -q) pg_restore -v -U dokploy -d dokploy /tmp/database.sql`,
`docker exec ${postgresContainerId} pg_restore -v -U dokploy -d dokploy /tmp/database.sql`,
);
// Cleanup the temporary file in the container
emit("Cleaning up container temp file...");
await execAsync(
`docker exec $(docker ps --filter "name=dokploy-postgres" -q) rm /tmp/database.sql`,
`docker exec ${postgresContainerId} rm /tmp/database.sql`,
);
emit("Restore completed successfully!");

View File

@@ -3,7 +3,11 @@ import { join } from "node:path";
import { paths } from "@dokploy/server/constants";
import type { User } from "@dokploy/server/services/user";
import { dump, load } from "js-yaml";
import { loadOrCreateConfig, writeTraefikConfig } from "./application";
import {
loadOrCreateConfig,
removeTraefikConfig,
writeTraefikConfig,
} from "./application";
import type { FileConfig } from "./file-types";
import type { MainTraefikConfig } from "./types";
@@ -11,32 +15,62 @@ export const updateServerTraefik = (
user: User | null,
newHost: string | null,
) => {
const { https, certificateType } = user || {};
const appName = "dokploy";
const config: FileConfig = loadOrCreateConfig(appName);
config.http = config.http || { routers: {}, services: {} };
config.http.routers = config.http.routers || {};
config.http.services = config.http.services || {};
const currentRouterConfig = config.http.routers[`${appName}-router-app`];
const currentRouterConfig = config.http.routers[`${appName}-router-app`] || {
rule: `Host(\`${newHost}\`)`,
service: `${appName}-service-app`,
entryPoints: ["web"],
};
config.http.routers[`${appName}-router-app`] = currentRouterConfig;
if (currentRouterConfig && newHost) {
currentRouterConfig.rule = `Host(\`${newHost}\`)`;
config.http.services = {
...config.http.services,
[`${appName}-service-app`]: {
loadBalancer: {
servers: [
{
url: `http://dokploy:${process.env.PORT || 3000}`,
},
],
passHostHeader: true,
},
},
};
if (user?.certificateType === "letsencrypt") {
if (https) {
currentRouterConfig.middlewares = ["redirect-to-https"];
if (certificateType === "letsencrypt") {
config.http.routers[`${appName}-router-app-secure`] = {
...currentRouterConfig,
rule: `Host(\`${newHost}\`)`,
service: `${appName}-service-app`,
entryPoints: ["websecure"],
tls: { certResolver: "letsencrypt" },
};
currentRouterConfig.middlewares = ["redirect-to-https"];
} else {
delete config.http.routers[`${appName}-router-app-secure`];
currentRouterConfig.middlewares = [];
config.http.routers[`${appName}-router-app-secure`] = {
rule: `Host(\`${newHost}\`)`,
service: `${appName}-service-app`,
entryPoints: ["websecure"],
};
}
} else {
delete config.http.routers[`${appName}-router-app-secure`];
currentRouterConfig.middlewares = [];
}
writeTraefikConfig(config, appName);
if (newHost) {
writeTraefikConfig(config, appName);
} else {
removeTraefikConfig(appName);
}
};
export const updateLetsEncryptEmail = (newEmail: string | null) => {