mirror of
https://github.com/andwati/dokploy.git
synced 2026-11-11 22:45:18 +00:00
feat: integrate build server functionality and enhance deployment process
- Added support for build server configuration in the application dashboard, including new UI elements and validation. - Updated database schema to include build server associations and foreign key constraints. - Enhanced deployment logic to utilize build server IDs, improving deployment flexibility. - Improved logging and user feedback during the build and deployment processes, including new alerts for image download status. - Refactored application and deployment services to accommodate build server integration.
This commit is contained in:
@@ -113,6 +113,14 @@ export const ShowBuildServer = ({ applicationId }: Props) => {
|
|||||||
application.
|
application.
|
||||||
</AlertBlock>
|
</AlertBlock>
|
||||||
|
|
||||||
|
<AlertBlock type="info">
|
||||||
|
📊 <strong>Important:</strong> Once the build finishes, you'll need to
|
||||||
|
wait a few seconds for the deployment server to download the image.
|
||||||
|
These download logs will <strong>NOT</strong> appear in the build
|
||||||
|
deployment logs. Check the <strong>Logs</strong> tab to see when the
|
||||||
|
container starts running.
|
||||||
|
</AlertBlock>
|
||||||
|
|
||||||
{!registries || registries.length === 0 ? (
|
{!registries || registries.length === 0 ? (
|
||||||
<AlertBlock type="warning">
|
<AlertBlock type="warning">
|
||||||
You need to add at least one registry to use build servers. Please
|
You need to add at least one registry to use build servers. Please
|
||||||
|
|||||||
@@ -407,7 +407,7 @@ export const ShowDeployments = ({
|
|||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
<ShowDeployment
|
<ShowDeployment
|
||||||
serverId={serverId}
|
serverId={activeLog?.buildServerId || serverId}
|
||||||
open={Boolean(activeLog && activeLog.logPath !== null)}
|
open={Boolean(activeLog && activeLog.logPath !== null)}
|
||||||
onClose={() => setActiveLog(null)}
|
onClose={() => setActiveLog(null)}
|
||||||
logPath={activeLog?.logPath || ""}
|
logPath={activeLog?.logPath || ""}
|
||||||
|
|||||||
@@ -0,0 +1,2 @@
|
|||||||
|
ALTER TABLE "deployment" ADD COLUMN "buildServerId" text;--> statement-breakpoint
|
||||||
|
ALTER TABLE "deployment" ADD CONSTRAINT "deployment_buildServerId_server_serverId_fk" FOREIGN KEY ("buildServerId") REFERENCES "public"."server"("serverId") ON DELETE cascade ON UPDATE no action;
|
||||||
File diff suppressed because it is too large
Load Diff
@@ -869,6 +869,13 @@
|
|||||||
"when": 1764472942038,
|
"when": 1764472942038,
|
||||||
"tag": "0123_careless_odin",
|
"tag": "0123_careless_odin",
|
||||||
"breakpoints": true
|
"breakpoints": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"idx": 124,
|
||||||
|
"version": "7",
|
||||||
|
"when": 1764474904317,
|
||||||
|
"tag": "0124_faulty_synch",
|
||||||
|
"breakpoints": true
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
@@ -235,6 +235,7 @@ export const applicationsRelations = relations(
|
|||||||
registry: one(registry, {
|
registry: one(registry, {
|
||||||
fields: [applications.registryId],
|
fields: [applications.registryId],
|
||||||
references: [registry.registryId],
|
references: [registry.registryId],
|
||||||
|
relationName: "applicationRegistry",
|
||||||
}),
|
}),
|
||||||
github: one(github, {
|
github: one(github, {
|
||||||
fields: [applications.githubId],
|
fields: [applications.githubId],
|
||||||
@@ -255,14 +256,17 @@ export const applicationsRelations = relations(
|
|||||||
server: one(server, {
|
server: one(server, {
|
||||||
fields: [applications.serverId],
|
fields: [applications.serverId],
|
||||||
references: [server.serverId],
|
references: [server.serverId],
|
||||||
|
relationName: "applicationServer",
|
||||||
}),
|
}),
|
||||||
buildServer: one(server, {
|
buildServer: one(server, {
|
||||||
fields: [applications.buildServerId],
|
fields: [applications.buildServerId],
|
||||||
references: [server.serverId],
|
references: [server.serverId],
|
||||||
|
relationName: "applicationBuildServer",
|
||||||
}),
|
}),
|
||||||
buildRegistry: one(registry, {
|
buildRegistry: one(registry, {
|
||||||
fields: [applications.buildRegistryId],
|
fields: [applications.buildRegistryId],
|
||||||
references: [registry.registryId],
|
references: [registry.registryId],
|
||||||
|
relationName: "applicationBuildRegistry",
|
||||||
}),
|
}),
|
||||||
previewDeployments: many(previewDeployments),
|
previewDeployments: many(previewDeployments),
|
||||||
}),
|
}),
|
||||||
|
|||||||
@@ -70,6 +70,9 @@ export const deployments = pgTable("deployment", {
|
|||||||
(): AnyPgColumn => volumeBackups.volumeBackupId,
|
(): AnyPgColumn => volumeBackups.volumeBackupId,
|
||||||
{ onDelete: "cascade" },
|
{ onDelete: "cascade" },
|
||||||
),
|
),
|
||||||
|
buildServerId: text("buildServerId").references(() => server.serverId, {
|
||||||
|
onDelete: "cascade",
|
||||||
|
}),
|
||||||
});
|
});
|
||||||
|
|
||||||
export const deploymentsRelations = relations(deployments, ({ one }) => ({
|
export const deploymentsRelations = relations(deployments, ({ one }) => ({
|
||||||
@@ -84,6 +87,12 @@ export const deploymentsRelations = relations(deployments, ({ one }) => ({
|
|||||||
server: one(server, {
|
server: one(server, {
|
||||||
fields: [deployments.serverId],
|
fields: [deployments.serverId],
|
||||||
references: [server.serverId],
|
references: [server.serverId],
|
||||||
|
relationName: "deploymentServer",
|
||||||
|
}),
|
||||||
|
buildServer: one(server, {
|
||||||
|
fields: [deployments.buildServerId],
|
||||||
|
references: [server.serverId],
|
||||||
|
relationName: "deploymentBuildServer",
|
||||||
}),
|
}),
|
||||||
previewDeployment: one(previewDeployments, {
|
previewDeployment: one(previewDeployments, {
|
||||||
fields: [deployments.previewDeploymentId],
|
fields: [deployments.previewDeploymentId],
|
||||||
@@ -115,6 +124,7 @@ const schema = createInsertSchema(deployments, {
|
|||||||
composeId: z.string(),
|
composeId: z.string(),
|
||||||
description: z.string().optional(),
|
description: z.string().optional(),
|
||||||
previewDeploymentId: z.string(),
|
previewDeploymentId: z.string(),
|
||||||
|
buildServerId: z.string(),
|
||||||
});
|
});
|
||||||
|
|
||||||
export const apiCreateDeployment = schema
|
export const apiCreateDeployment = schema
|
||||||
|
|||||||
@@ -33,7 +33,12 @@ export const registry = pgTable("registry", {
|
|||||||
});
|
});
|
||||||
|
|
||||||
export const registryRelations = relations(registry, ({ many }) => ({
|
export const registryRelations = relations(registry, ({ many }) => ({
|
||||||
applications: many(applications),
|
applications: many(applications, {
|
||||||
|
relationName: "applicationRegistry",
|
||||||
|
}),
|
||||||
|
buildApplications: many(applications, {
|
||||||
|
relationName: "applicationBuildRegistry",
|
||||||
|
}),
|
||||||
}));
|
}));
|
||||||
|
|
||||||
const createSchema = createInsertSchema(registry, {
|
const createSchema = createInsertSchema(registry, {
|
||||||
|
|||||||
@@ -99,12 +99,22 @@ export const server = pgTable("server", {
|
|||||||
});
|
});
|
||||||
|
|
||||||
export const serverRelations = relations(server, ({ one, many }) => ({
|
export const serverRelations = relations(server, ({ one, many }) => ({
|
||||||
deployments: many(deployments),
|
deployments: many(deployments, {
|
||||||
|
relationName: "deploymentServer",
|
||||||
|
}),
|
||||||
|
buildDeployments: many(deployments, {
|
||||||
|
relationName: "deploymentBuildServer",
|
||||||
|
}),
|
||||||
sshKey: one(sshKeys, {
|
sshKey: one(sshKeys, {
|
||||||
fields: [server.sshKeyId],
|
fields: [server.sshKeyId],
|
||||||
references: [sshKeys.sshKeyId],
|
references: [sshKeys.sshKeyId],
|
||||||
}),
|
}),
|
||||||
applications: many(applications),
|
applications: many(applications, {
|
||||||
|
relationName: "applicationServer",
|
||||||
|
}),
|
||||||
|
buildApplications: many(applications, {
|
||||||
|
relationName: "applicationBuildServer",
|
||||||
|
}),
|
||||||
compose: many(compose),
|
compose: many(compose),
|
||||||
redis: many(redis),
|
redis: many(redis),
|
||||||
mariadb: many(mariadb),
|
mariadb: many(mariadb),
|
||||||
|
|||||||
@@ -112,6 +112,7 @@ export const findApplicationById = async (applicationId: string) => {
|
|||||||
gitea: true,
|
gitea: true,
|
||||||
server: true,
|
server: true,
|
||||||
previewDeployments: true,
|
previewDeployments: true,
|
||||||
|
buildRegistry: true,
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
if (!application) {
|
if (!application) {
|
||||||
@@ -172,6 +173,7 @@ export const deployApplication = async ({
|
|||||||
descriptionLog: string;
|
descriptionLog: string;
|
||||||
}) => {
|
}) => {
|
||||||
const application = await findApplicationById(applicationId);
|
const application = await findApplicationById(applicationId);
|
||||||
|
const serverId = application.buildServerId || application.serverId;
|
||||||
|
|
||||||
const buildLink = `${await getDokployUrl()}/dashboard/project/${application.environment.projectId}/environment/${application.environmentId}/services/application/${application.applicationId}?tab=deployments`;
|
const buildLink = `${await getDokployUrl()}/dashboard/project/${application.environment.projectId}/environment/${application.environmentId}/services/application/${application.applicationId}?tab=deployments`;
|
||||||
const deployment = await createDeployment({
|
const deployment = await createDeployment({
|
||||||
@@ -199,8 +201,8 @@ export const deployApplication = async ({
|
|||||||
command += getBuildCommand(application);
|
command += getBuildCommand(application);
|
||||||
|
|
||||||
const commandWithLog = `(${command}) >> ${deployment.logPath} 2>&1`;
|
const commandWithLog = `(${command}) >> ${deployment.logPath} 2>&1`;
|
||||||
if (application.serverId) {
|
if (serverId) {
|
||||||
await execAsyncRemote(application.serverId, commandWithLog);
|
await execAsyncRemote(serverId, commandWithLog);
|
||||||
} else {
|
} else {
|
||||||
await execAsync(commandWithLog);
|
await execAsync(commandWithLog);
|
||||||
}
|
}
|
||||||
@@ -240,8 +242,8 @@ export const deployApplication = async ({
|
|||||||
}
|
}
|
||||||
|
|
||||||
command += `echo "\nError occurred ❌, check the logs for details." >> ${deployment.logPath};`;
|
command += `echo "\nError occurred ❌, check the logs for details." >> ${deployment.logPath};`;
|
||||||
if (application.serverId) {
|
if (serverId) {
|
||||||
await execAsyncRemote(application.serverId, command);
|
await execAsyncRemote(serverId, command);
|
||||||
} else {
|
} else {
|
||||||
await execAsync(command);
|
await execAsync(command);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -80,7 +80,7 @@ export const createDeployment = async (
|
|||||||
"application",
|
"application",
|
||||||
application.serverId,
|
application.serverId,
|
||||||
);
|
);
|
||||||
const serverId = application.serverId;
|
const serverId = application.buildServerId || application.serverId;
|
||||||
|
|
||||||
const { LOGS_PATH } = paths(!!serverId);
|
const { LOGS_PATH } = paths(!!serverId);
|
||||||
const formattedDateTime = format(new Date(), "yyyy-MM-dd:HH:mm:ss");
|
const formattedDateTime = format(new Date(), "yyyy-MM-dd:HH:mm:ss");
|
||||||
@@ -93,6 +93,7 @@ export const createDeployment = async (
|
|||||||
const command = `
|
const command = `
|
||||||
mkdir -p ${LOGS_PATH}/${application.appName};
|
mkdir -p ${LOGS_PATH}/${application.appName};
|
||||||
echo "Initializing deployment" >> ${logFilePath};
|
echo "Initializing deployment" >> ${logFilePath};
|
||||||
|
echo "Building on ${serverId ? "Build Server" : "Dokploy Server"}" >> ${logFilePath};
|
||||||
`;
|
`;
|
||||||
|
|
||||||
await execAsyncRemote(server.serverId, command);
|
await execAsyncRemote(server.serverId, command);
|
||||||
@@ -112,6 +113,9 @@ export const createDeployment = async (
|
|||||||
logPath: logFilePath,
|
logPath: logFilePath,
|
||||||
description: deployment.description || "",
|
description: deployment.description || "",
|
||||||
startedAt: new Date().toISOString(),
|
startedAt: new Date().toISOString(),
|
||||||
|
...(application.buildServerId && {
|
||||||
|
buildServerId: application.buildServerId,
|
||||||
|
}),
|
||||||
})
|
})
|
||||||
.returning();
|
.returning();
|
||||||
if (deploymentCreate.length === 0 || !deploymentCreate[0]) {
|
if (deploymentCreate.length === 0 || !deploymentCreate[0]) {
|
||||||
|
|||||||
@@ -29,13 +29,14 @@ export type ApplicationNested = InferResultType<
|
|||||||
redirects: true;
|
redirects: true;
|
||||||
ports: true;
|
ports: true;
|
||||||
registry: true;
|
registry: true;
|
||||||
|
buildRegistry: true;
|
||||||
environment: { with: { project: true } };
|
environment: { with: { project: true } };
|
||||||
}
|
}
|
||||||
>;
|
>;
|
||||||
|
|
||||||
export const getBuildCommand = (application: ApplicationNested) => {
|
export const getBuildCommand = (application: ApplicationNested) => {
|
||||||
let command = "";
|
let command = "";
|
||||||
const { buildType, registry } = application;
|
const { buildType } = application;
|
||||||
|
|
||||||
if (application.sourceType === "docker") {
|
if (application.sourceType === "docker") {
|
||||||
return "";
|
return "";
|
||||||
@@ -60,7 +61,7 @@ export const getBuildCommand = (application: ApplicationNested) => {
|
|||||||
command = getRailpackCommand(application);
|
command = getRailpackCommand(application);
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
if (registry) {
|
if (application.registry || application.buildRegistry) {
|
||||||
command += uploadImageRemoteCommand(application);
|
command += uploadImageRemoteCommand(application);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -169,13 +170,15 @@ export const mechanizeDockerContainer = async (
|
|||||||
ForceUpdate: inspect.Spec.TaskTemplate.ForceUpdate + 1,
|
ForceUpdate: inspect.Spec.TaskTemplate.ForceUpdate + 1,
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
} catch {
|
} catch (error) {
|
||||||
|
console.log(error);
|
||||||
await docker.createService(settings);
|
await docker.createService(settings);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const getImageName = (application: ApplicationNested) => {
|
const getImageName = (application: ApplicationNested) => {
|
||||||
const { appName, sourceType, dockerImage, registry } = application;
|
const { appName, sourceType, dockerImage, registry, buildRegistry } =
|
||||||
|
application;
|
||||||
const imageName = `${appName}:latest`;
|
const imageName = `${appName}:latest`;
|
||||||
if (sourceType === "docker") {
|
if (sourceType === "docker") {
|
||||||
return dockerImage || "ERROR-NO-IMAGE-PROVIDED";
|
return dockerImage || "ERROR-NO-IMAGE-PROVIDED";
|
||||||
@@ -188,12 +191,26 @@ const getImageName = (application: ApplicationNested) => {
|
|||||||
: `${registryUrl ? `${registryUrl}/` : ""}${username}/${imageName}`;
|
: `${registryUrl ? `${registryUrl}/` : ""}${username}/${imageName}`;
|
||||||
return registryTag;
|
return registryTag;
|
||||||
}
|
}
|
||||||
|
if (buildRegistry) {
|
||||||
|
const { registryUrl, imagePrefix, username } = buildRegistry;
|
||||||
|
const registryTag = imagePrefix
|
||||||
|
? `${registryUrl ? `${registryUrl}/` : ""}${imagePrefix}/${imageName}`
|
||||||
|
: `${registryUrl ? `${registryUrl}/` : ""}${username}/${imageName}`;
|
||||||
|
return registryTag;
|
||||||
|
}
|
||||||
|
|
||||||
return imageName;
|
return imageName;
|
||||||
};
|
};
|
||||||
|
|
||||||
export const getAuthConfig = (application: ApplicationNested) => {
|
export const getAuthConfig = (application: ApplicationNested) => {
|
||||||
const { registry, username, password, sourceType, registryUrl } = application;
|
const {
|
||||||
|
registry,
|
||||||
|
buildRegistry,
|
||||||
|
username,
|
||||||
|
password,
|
||||||
|
sourceType,
|
||||||
|
registryUrl,
|
||||||
|
} = application;
|
||||||
|
|
||||||
if (sourceType === "docker") {
|
if (sourceType === "docker") {
|
||||||
if (username && password) {
|
if (username && password) {
|
||||||
@@ -209,6 +226,12 @@ export const getAuthConfig = (application: ApplicationNested) => {
|
|||||||
username: registry.username,
|
username: registry.username,
|
||||||
serveraddress: registry.registryUrl,
|
serveraddress: registry.registryUrl,
|
||||||
};
|
};
|
||||||
|
} else if (buildRegistry) {
|
||||||
|
return {
|
||||||
|
password: buildRegistry.password,
|
||||||
|
username: buildRegistry.username,
|
||||||
|
serveraddress: buildRegistry.registryUrl,
|
||||||
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
return undefined;
|
return undefined;
|
||||||
|
|||||||
@@ -1,44 +1,77 @@
|
|||||||
|
import type { Registry } from "@dokploy/server/services/registry";
|
||||||
import type { ApplicationNested } from "../builders";
|
import type { ApplicationNested } from "../builders";
|
||||||
|
|
||||||
export const uploadImageRemoteCommand = (application: ApplicationNested) => {
|
export const uploadImageRemoteCommand = (application: ApplicationNested) => {
|
||||||
const registry = application.registry;
|
const registry = application.registry;
|
||||||
|
const buildRegistry = application.buildRegistry;
|
||||||
|
|
||||||
if (!registry) {
|
if (!registry && !buildRegistry) {
|
||||||
throw new Error("Registry not found");
|
throw new Error("No registry found");
|
||||||
}
|
}
|
||||||
|
|
||||||
const { registryUrl, imagePrefix, username } = registry;
|
|
||||||
const { appName } = application;
|
const { appName } = application;
|
||||||
const imageName = `${appName}:latest`;
|
const imageName = `${appName}:latest`;
|
||||||
|
|
||||||
const finalURL = registryUrl;
|
const commands: string[] = [];
|
||||||
|
if (registry) {
|
||||||
// Build registry tag in correct format: registry.com/owner/image:tag
|
const registryTag = getRegistryTag(registry, imageName);
|
||||||
const registryTag = imagePrefix
|
if (registryTag) {
|
||||||
? `${registryUrl ? `${registryUrl}/` : ""}${imagePrefix}/${imageName}`
|
commands.push(`echo "📦 [Enabled Registry Swarm]"`);
|
||||||
: `${registryUrl ? `${registryUrl}/` : ""}${username}/${imageName}`;
|
commands.push(getRegistryCommands(registry, imageName, registryTag));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (buildRegistry) {
|
||||||
|
const buildRegistryTag = getRegistryTag(buildRegistry, imageName);
|
||||||
|
if (buildRegistryTag) {
|
||||||
|
commands.push(`echo "🔑 [Enabled Build Registry]"`);
|
||||||
|
commands.push(
|
||||||
|
getRegistryCommands(buildRegistry, imageName, buildRegistryTag),
|
||||||
|
);
|
||||||
|
commands.push(
|
||||||
|
`echo "⚠️ INFO: After the build is finished, you need to wait a few seconds for the server to download the image and run the container."`,
|
||||||
|
);
|
||||||
|
commands.push(
|
||||||
|
`echo "📊 Check the Logs tab to see when the container starts running."`,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
try {
|
try {
|
||||||
const command = `
|
return commands.join("\n");
|
||||||
echo "📦 [Enabled Registry] Uploading image to '${registry.registryType}' | '${registryTag}'" ;
|
|
||||||
echo "${registry.password}" | docker login ${finalURL} -u ${registry.username} --password-stdin || {
|
|
||||||
echo "❌ DockerHub Failed" ;
|
|
||||||
exit 1;
|
|
||||||
}
|
|
||||||
echo "✅ Registry Login Success" ;
|
|
||||||
docker tag ${imageName} ${registryTag} || {
|
|
||||||
echo "❌ Error tagging image" ;
|
|
||||||
exit 1;
|
|
||||||
}
|
|
||||||
echo "✅ Image Tagged" ;
|
|
||||||
docker push ${registryTag} || {
|
|
||||||
echo "❌ Error pushing image" ;
|
|
||||||
exit 1;
|
|
||||||
}
|
|
||||||
echo "✅ Image Pushed" ;
|
|
||||||
`;
|
|
||||||
return command;
|
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
throw error;
|
throw error;
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
const getRegistryTag = (registry: Registry | null, imageName: string) => {
|
||||||
|
if (!registry) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
const { registryUrl, imagePrefix, username } = registry;
|
||||||
|
return imagePrefix
|
||||||
|
? `${registryUrl ? `${registryUrl}/` : ""}${imagePrefix}/${imageName}`
|
||||||
|
: `${registryUrl ? `${registryUrl}/` : ""}${username}/${imageName}`;
|
||||||
|
};
|
||||||
|
|
||||||
|
const getRegistryCommands = (
|
||||||
|
registry: Registry,
|
||||||
|
imageName: string,
|
||||||
|
registryTag: string,
|
||||||
|
): string => {
|
||||||
|
return `
|
||||||
|
echo "📦 [Enabled Registry] Uploading image to '${registry.registryType}' | '${registryTag}'" ;
|
||||||
|
echo "${registry.password}" | docker login ${registry.registryUrl} -u ${registry.username} --password-stdin || {
|
||||||
|
echo "❌ DockerHub Failed" ;
|
||||||
|
exit 1;
|
||||||
|
}
|
||||||
|
echo "✅ Registry Login Success" ;
|
||||||
|
docker tag ${imageName} ${registryTag} || {
|
||||||
|
echo "❌ Error tagging image" ;
|
||||||
|
exit 1;
|
||||||
|
}
|
||||||
|
echo "✅ Image Tagged" ;
|
||||||
|
docker push ${registryTag} || {
|
||||||
|
echo "❌ Error pushing image" ;
|
||||||
|
exit 1;
|
||||||
|
}
|
||||||
|
echo "✅ Image Pushed" ;
|
||||||
|
`;
|
||||||
|
};
|
||||||
|
|||||||
Reference in New Issue
Block a user