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:
Mauricio Siu
2025-11-29 23:04:02 -06:00
parent 704582f6de
commit 15634c9f10
13 changed files with 6946 additions and 43 deletions
@@ -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
+7
View File
@@ -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
+6 -1
View File
@@ -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, {
+12 -2
View File
@@ -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),
+6 -4
View File
@@ -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);
} }
+5 -1
View File
@@ -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]) {
+28 -5
View File
@@ -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;
+62 -29
View File
@@ -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" ;
`;
};