diff --git a/apps/dokploy/__test__/deploy/railpack.command.test.ts b/apps/dokploy/__test__/deploy/railpack.command.test.ts
new file mode 100644
index 0000000000..94c95c950a
--- /dev/null
+++ b/apps/dokploy/__test__/deploy/railpack.command.test.ts
@@ -0,0 +1,104 @@
+import type { ApplicationNested } from "@dokploy/server/utils/builders";
+import { getRailpackCommand } from "@dokploy/server/utils/builders/railpack";
+import { describe, expect, it } from "vitest";
+
+const createApplication = (
+ overrides: Partial = {},
+): ApplicationNested =>
+ ({
+ appName: "test-app",
+ buildType: "railpack",
+ sourceType: "git",
+ buildPath: "/",
+ railpackVersion: "0.15.4",
+ env: "TEST_VAR=one",
+ cleanCache: false,
+ environment: {
+ project: {
+ env: "",
+ },
+ env: "",
+ },
+ ...overrides,
+ }) as unknown as ApplicationNested;
+
+const getSecretsHash = (command: string) => {
+ const match = command.match(/secrets-hash=([a-f0-9]{64})/);
+ if (!match?.[1]) {
+ throw new Error("secrets-hash build arg was not found");
+ }
+
+ return match[1];
+};
+
+describe("getRailpackCommand", () => {
+ it("includes secrets-hash without clean cache", () => {
+ const command = getRailpackCommand(createApplication());
+
+ expect(command).toContain("--build-arg secrets-hash=");
+ expect(command).not.toContain("cache-key=");
+ });
+
+ it("includes cache-key only when clean cache is enabled", () => {
+ const command = getRailpackCommand(
+ createApplication({
+ cleanCache: true,
+ }),
+ );
+
+ expect(command).toContain("--build-arg secrets-hash=");
+ expect(command).toContain("--build-arg cache-key=");
+ });
+
+ it("changes secrets-hash when an environment value changes", () => {
+ const firstCommand = getRailpackCommand(
+ createApplication({
+ env: "TEST_VAR=one",
+ }),
+ );
+ const secondCommand = getRailpackCommand(
+ createApplication({
+ env: "TEST_VAR=two",
+ }),
+ );
+
+ expect(getSecretsHash(firstCommand)).not.toEqual(
+ getSecretsHash(secondCommand),
+ );
+ });
+
+ it("changes secrets-hash when referenced project or environment values change", () => {
+ const firstCommand = getRailpackCommand(
+ createApplication({
+ env: [
+ "PROJECT_VALUE=${{project.SHARED_VALUE}}",
+ "ENVIRONMENT_VALUE=${{environment.SHARED_VALUE}}",
+ ].join("\n"),
+ environment: {
+ project: {
+ env: "SHARED_VALUE=one",
+ },
+ env: "SHARED_VALUE=alpha",
+ },
+ } as Partial),
+ );
+ const secondCommand = getRailpackCommand(
+ createApplication({
+ env: [
+ "PROJECT_VALUE=${{project.SHARED_VALUE}}",
+ "ENVIRONMENT_VALUE=${{environment.SHARED_VALUE}}",
+ ].join("\n"),
+ environment: {
+ project: {
+ env: "SHARED_VALUE=two",
+ },
+ env: "SHARED_VALUE=beta",
+ },
+ } as Partial),
+ );
+
+ expect(getSecretsHash(firstCommand)).not.toEqual(
+ getSecretsHash(secondCommand),
+ );
+ });
+});
diff --git a/apps/dokploy/__test__/env/rollback-environment.test.ts b/apps/dokploy/__test__/env/rollback-environment.test.ts
new file mode 100644
index 0000000000..7d71059e05
--- /dev/null
+++ b/apps/dokploy/__test__/env/rollback-environment.test.ts
@@ -0,0 +1,108 @@
+import { prepareEnvironmentVariables } from "@dokploy/server/index";
+import { describe, expect, it } from "vitest";
+
+const projectEnv = `
+ENVIRONMENT=staging
+DATABASE_URL=postgres://postgres:postgres@localhost:5432/project_db
+`;
+
+const environmentEnv = `
+NODE_ENV=production
+POSTGRES_HOST=postgres.internal
+POSTGRES_PORT=5432
+REDIS_URL=redis://redis.internal:6379
+`;
+
+const serviceEnv = `
+NODE_ENV=\${{environment.NODE_ENV}}
+REDIS_URL=\${{environment.REDIS_URL}}
+PORT=3000
+`;
+
+/**
+ * A rollback replays the snapshot stored in `rollbacks.fullContext`, which keeps
+ * the service env, the environment env and the project env captured at deploy time.
+ */
+const fullContext = {
+ env: serviceEnv,
+ environment: {
+ env: environmentEnv,
+ project: {
+ env: projectEnv,
+ },
+ },
+};
+
+describe("prepareEnvironmentVariables for application rollback", () => {
+ it("resolves environment variables from the rollback snapshot", () => {
+ const result = prepareEnvironmentVariables(
+ fullContext.env,
+ fullContext.environment.project.env,
+ fullContext.environment.env,
+ );
+
+ expect(result).toEqual([
+ "NODE_ENV=production",
+ "REDIS_URL=redis://redis.internal:6379",
+ "PORT=3000",
+ ]);
+ });
+
+ it("resolves project and environment variables together on rollback", () => {
+ const rollbackEnv = `
+DATABASE_URL=\${{project.DATABASE_URL}}
+POSTGRES_URL=postgres://\${{environment.POSTGRES_HOST}}:\${{environment.POSTGRES_PORT}}/app
+ENVIRONMENT=\${{project.ENVIRONMENT}}
+`;
+
+ const result = prepareEnvironmentVariables(
+ rollbackEnv,
+ projectEnv,
+ environmentEnv,
+ );
+
+ expect(result).toEqual([
+ "DATABASE_URL=postgres://postgres:postgres@localhost:5432/project_db",
+ "POSTGRES_URL=postgres://postgres.internal:5432/app",
+ "ENVIRONMENT=staging",
+ ]);
+ });
+
+ it("throws when the environment env of the snapshot is not passed", () => {
+ expect(() =>
+ prepareEnvironmentVariables(fullContext.env, projectEnv),
+ ).toThrow("Invalid environment variable: environment.NODE_ENV");
+ });
+
+ it("maintains precedence: service > environment > project on rollback", () => {
+ const conflictingProjectEnv = `
+NODE_ENV=project
+API_URL=https://project.api.com
+`;
+
+ const conflictingEnvironmentEnv = `
+NODE_ENV=environment
+API_URL=https://environment.api.com
+`;
+
+ const rollbackEnv = `
+NODE_ENV=service
+PROJECT_API_URL=\${{project.API_URL}}
+ENVIRONMENT_API_URL=\${{environment.API_URL}}
+SELF_REFERENCE=\${{NODE_ENV}}
+`;
+
+ const result = prepareEnvironmentVariables(
+ rollbackEnv,
+ conflictingProjectEnv,
+ conflictingEnvironmentEnv,
+ );
+
+ expect(result).toEqual([
+ "NODE_ENV=service",
+ "PROJECT_API_URL=https://project.api.com",
+ "ENVIRONMENT_API_URL=https://environment.api.com",
+ "SELF_REFERENCE=service",
+ ]);
+ });
+});
diff --git a/apps/dokploy/__test__/requests/request.test.ts b/apps/dokploy/__test__/requests/request.test.ts
index 3f58ac439a..844458fd24 100644
--- a/apps/dokploy/__test__/requests/request.test.ts
+++ b/apps/dokploy/__test__/requests/request.test.ts
@@ -55,6 +55,28 @@ describe("processLogs", () => {
expect(result.data).toHaveLength(2);
});
+ it("should not throw when filtering by hostname and an entry has no RequestHost", () => {
+ const entryWithoutRequestHost = sampleLogEntry.replace(
+ /"RequestHost":"[^"]*",/,
+ "",
+ );
+
+ const mixedEntries = `${sampleLogEntry}\n${entryWithoutRequestHost}`;
+
+ expect(() =>
+ parseRawConfig(mixedEntries, undefined, undefined, "traefik.me"),
+ ).not.toThrow();
+
+ const result = parseRawConfig(
+ mixedEntries,
+ undefined,
+ undefined,
+ "traefik.me",
+ );
+ expect(result.totalCount).toBe(1);
+ expect(result.data[0]?.RequestHost).toBe("s222-umami-c381af.traefik.me");
+ });
+
it("should filter out Dokploy dashboard requests", () => {
const dokployDashboardEntry = `{"ClientAddr":"172.71.187.131:9485","ClientHost":"172.71.187.131","ClientPort":"9485","ClientUsername":"-","DownstreamContentSize":14550,"DownstreamStatus":200,"Duration":57681682,"OriginContentSize":14550,"OriginDuration":57612242,"OriginStatus":200,"Overhead":69440,"RequestAddr":"hostinger.dokploy.com","RequestContentSize":0,"RequestCount":20142,"RequestHost":"hostinger.dokploy.com","RequestMethod":"GET","RequestPath":"/_next/data/cb_zzI4Rp9G7Q7djrFKh0/en/dashboard/traefik.json","RequestPort":"-","RequestProtocol":"HTTP/2.0","RequestScheme":"https","RetryAttempts":0,"RouterName":"dokploy-router-app-secure@file","ServiceAddr":"dokploy:3000","ServiceName":"dokploy-service-app@file","ServiceURL":"http://dokploy:3000","StartLocal":"2025-12-10T05:10:41.957755949Z","StartUTC":"2025-12-10T05:10:41.957755949Z","TLSCipher":"TLS_AES_128_GCM_SHA256","TLSVersion":"1.3","entryPointName":"websecure","level":"info","msg":"","time":"2025-12-10T05:10:42Z"}`;
diff --git a/apps/dokploy/components/dashboard/application/deployments/show-deployments.tsx b/apps/dokploy/components/dashboard/application/deployments/show-deployments.tsx
index 48c8ec981e..1a531e24cf 100644
--- a/apps/dokploy/components/dashboard/application/deployments/show-deployments.tsx
+++ b/apps/dokploy/components/dashboard/application/deployments/show-deployments.tsx
@@ -63,6 +63,9 @@ export const ShowDeployments = ({
const [activeLog, setActiveLog] = useState<
RouterOutputs["deployment"]["all"][number] | null
>(null);
+ const [removingDeploymentIds, setRemovingDeploymentIds] = useState<
+ Set
+ >(new Set());
const { data: deployments, isPending: isLoadingDeployments } =
api.deployment.allByType.useQuery(
{
@@ -81,7 +84,7 @@ export const ShowDeployments = ({
api.rollback.rollback.useMutation();
const { mutateAsync: killProcess, isPending: isKillingProcess } =
api.deployment.killProcess.useMutation();
- const { mutateAsync: removeDeployment, isPending: isRemovingDeployment } =
+ const { mutateAsync: removeDeployment } =
api.deployment.removeDeployment.useMutation();
// Cancel deployment mutations
@@ -408,6 +411,11 @@ export const ShowDeployments = ({
description="Are you sure you want to delete this deployment? This action cannot be undone."
type="default"
onClick={async () => {
+ setRemovingDeploymentIds((deploymentIds) => {
+ const nextDeploymentIds = new Set(deploymentIds);
+ nextDeploymentIds.add(deployment.deploymentId);
+ return nextDeploymentIds;
+ });
try {
await removeDeployment({
deploymentId: deployment.deploymentId,
@@ -415,13 +423,25 @@ export const ShowDeployments = ({
toast.success("Deployment deleted successfully");
} catch (error) {
toast.error("Error deleting deployment");
+ } finally {
+ setRemovingDeploymentIds((deploymentIds) => {
+ const nextDeploymentIds = new Set(
+ deploymentIds,
+ );
+ nextDeploymentIds.delete(
+ deployment.deploymentId,
+ );
+ return nextDeploymentIds;
+ });
}
}}
>
))}
@@ -531,14 +535,14 @@ export const SaveGiteaProvider = ({ applicationId }: Props) => {
control={form.control}
name="enableSubmodules"
render={({ field }) => (
-
+
- Enable Submodules
+ Enable Submodules
)}
/>
diff --git a/apps/dokploy/components/dashboard/application/general/generic/save-github-provider.tsx b/apps/dokploy/components/dashboard/application/general/generic/save-github-provider.tsx
index 29df617b1d..c842655953 100644
--- a/apps/dokploy/components/dashboard/application/general/generic/save-github-provider.tsx
+++ b/apps/dokploy/components/dashboard/application/general/generic/save-github-provider.tsx
@@ -438,8 +438,12 @@ export const SaveGithubProvider = ({ applicationId }: Props) => {
)}
- {error && (
-
- )}
-
{
- const statusCode = res ? res.statusCode : err ? err.statusCode : 404;
- return { statusCode, error: err };
+ErrorPage.getInitialProps = ({ res, err }: NextPageContext) => {
+ const statusCode = res ? res.statusCode : err ? (err.statusCode ?? 500) : 404;
+ return { statusCode };
};
diff --git a/apps/dokploy/pages/api/deploy/[refreshToken].ts b/apps/dokploy/pages/api/deploy/[refreshToken].ts
index bb6eb06d37..9cf6142d25 100644
--- a/apps/dokploy/pages/api/deploy/[refreshToken].ts
+++ b/apps/dokploy/pages/api/deploy/[refreshToken].ts
@@ -119,9 +119,11 @@ export default async function handler(
}
// If webhook doesn't provide image info, we'll use the configured image (old behavior)
} else if (sourceType === "github") {
- const normalizedCommits = req.body?.commits?.flatMap(
- (commit: any) => commit.modified,
- );
+ const normalizedCommits = req.body?.commits?.flatMap((commit: any) => [
+ ...(commit.added || []),
+ ...(commit.modified || []),
+ ...(commit.removed || []),
+ ]);
const shouldDeployPaths = shouldDeploy(
application.watchPaths,
@@ -150,21 +152,29 @@ export default async function handler(
let normalizedCommits: string[] = [];
if (provider === "github") {
- normalizedCommits = req.body?.commits?.flatMap(
- (commit: any) => commit.modified,
- );
+ normalizedCommits = req.body?.commits?.flatMap((commit: any) => [
+ ...(commit.added || []),
+ ...(commit.modified || []),
+ ...(commit.removed || []),
+ ]);
} else if (provider === "gitlab") {
- normalizedCommits = req.body?.commits?.flatMap(
- (commit: any) => commit.modified,
- );
+ normalizedCommits = req.body?.commits?.flatMap((commit: any) => [
+ ...(commit.added || []),
+ ...(commit.modified || []),
+ ...(commit.removed || []),
+ ]);
} else if (provider === "gitea") {
- normalizedCommits = req.body?.commits?.flatMap(
- (commit: any) => commit.modified,
- );
+ normalizedCommits = req.body?.commits?.flatMap((commit: any) => [
+ ...(commit.added || []),
+ ...(commit.modified || []),
+ ...(commit.removed || []),
+ ]);
} else if (provider === "soft-serve") {
- normalizedCommits = req.body?.commits?.flatMap(
- (commit: any) => commit.modified,
- );
+ normalizedCommits = req.body?.commits?.flatMap((commit: any) => [
+ ...(commit.added || []),
+ ...(commit.modified || []),
+ ...(commit.removed || []),
+ ]);
}
const shouldDeployPaths = shouldDeploy(
@@ -179,9 +189,11 @@ export default async function handler(
} else if (sourceType === "gitlab") {
const branchName = extractBranchName(req.headers, req.body);
- const normalizedCommits = req.body?.commits?.flatMap(
- (commit: any) => commit.modified,
- );
+ const normalizedCommits = req.body?.commits?.flatMap((commit: any) => [
+ ...(commit.added || []),
+ ...(commit.modified || []),
+ ...(commit.removed || []),
+ ]);
const shouldDeployPaths = shouldDeploy(
application.watchPaths,
@@ -225,9 +237,11 @@ export default async function handler(
} else if (sourceType === "gitea") {
const branchName = extractBranchName(req.headers, req.body);
- const normalizedCommits = req.body?.commits?.flatMap(
- (commit: any) => commit.modified,
- );
+ const normalizedCommits = req.body?.commits?.flatMap((commit: any) => [
+ ...(commit.added || []),
+ ...(commit.modified || []),
+ ...(commit.removed || []),
+ ]);
const shouldDeployPaths = shouldDeploy(
application.watchPaths,
diff --git a/apps/dokploy/pages/api/deploy/compose/[refreshToken].ts b/apps/dokploy/pages/api/deploy/compose/[refreshToken].ts
index 85a379eb3e..0d83c10174 100644
--- a/apps/dokploy/pages/api/deploy/compose/[refreshToken].ts
+++ b/apps/dokploy/pages/api/deploy/compose/[refreshToken].ts
@@ -54,9 +54,11 @@ export default async function handler(
if (sourceType === "github") {
const branchName = extractBranchName(req.headers, req.body);
- const normalizedCommits = req.body?.commits?.flatMap(
- (commit: any) => commit.modified,
- );
+ const normalizedCommits = req.body?.commits?.flatMap((commit: any) => [
+ ...(commit.added || []),
+ ...(commit.modified || []),
+ ...(commit.removed || []),
+ ]);
const shouldDeployPaths = shouldDeploy(
composeResult.watchPaths,
@@ -74,9 +76,11 @@ export default async function handler(
}
} else if (sourceType === "gitlab") {
const branchName = extractBranchName(req.headers, req.body);
- const normalizedCommits = req.body?.commits?.flatMap(
- (commit: any) => commit.modified,
- );
+ const normalizedCommits = req.body?.commits?.flatMap((commit: any) => [
+ ...(commit.added || []),
+ ...(commit.modified || []),
+ ...(commit.removed || []),
+ ]);
const shouldDeployPaths = shouldDeploy(
composeResult.watchPaths,
@@ -125,17 +129,23 @@ export default async function handler(
let normalizedCommits: string[] = [];
if (provider === "github") {
- normalizedCommits = req.body?.commits?.flatMap(
- (commit: any) => commit.modified,
- );
+ normalizedCommits = req.body?.commits?.flatMap((commit: any) => [
+ ...(commit.added || []),
+ ...(commit.modified || []),
+ ...(commit.removed || []),
+ ]);
} else if (provider === "gitlab") {
- normalizedCommits = req.body?.commits?.flatMap(
- (commit: any) => commit.modified,
- );
+ normalizedCommits = req.body?.commits?.flatMap((commit: any) => [
+ ...(commit.added || []),
+ ...(commit.modified || []),
+ ...(commit.removed || []),
+ ]);
} else if (provider === "gitea") {
- normalizedCommits = req.body?.commits?.flatMap(
- (commit: any) => commit.modified,
- );
+ normalizedCommits = req.body?.commits?.flatMap((commit: any) => [
+ ...(commit.added || []),
+ ...(commit.modified || []),
+ ...(commit.removed || []),
+ ]);
}
const shouldDeployPaths = shouldDeploy(
@@ -150,9 +160,11 @@ export default async function handler(
} else if (sourceType === "gitea") {
const branchName = extractBranchName(req.headers, req.body);
- const normalizedCommits = req.body?.commits?.flatMap(
- (commit: any) => commit.modified,
- );
+ const normalizedCommits = req.body?.commits?.flatMap((commit: any) => [
+ ...(commit.added || []),
+ ...(commit.modified || []),
+ ...(commit.removed || []),
+ ]);
const shouldDeployPaths = shouldDeploy(
composeResult.watchPaths,
diff --git a/apps/dokploy/pages/api/deploy/github.ts b/apps/dokploy/pages/api/deploy/github.ts
index f03bf786ec..4fc75fc50c 100644
--- a/apps/dokploy/pages/api/deploy/github.ts
+++ b/apps/dokploy/pages/api/deploy/github.ts
@@ -223,9 +223,11 @@ export default async function handler(
const deploymentTitle = extractCommitMessage(req.headers, req.body);
const deploymentHash = extractHash(req.headers, req.body);
const owner = getGithubRepositoryOwner(githubBody);
- const normalizedCommits = githubBody?.commits?.flatMap(
- (commit: any) => commit.modified,
- );
+ const normalizedCommits = githubBody?.commits?.flatMap((commit: any) => [
+ ...(commit.added || []),
+ ...(commit.modified || []),
+ ...(commit.removed || []),
+ ]);
const apps = await db.query.applications.findMany({
where: and(
diff --git a/apps/dokploy/server/api/routers/application.ts b/apps/dokploy/server/api/routers/application.ts
index e1bbedcb9e..b6e2a3ee43 100644
--- a/apps/dokploy/server/api/routers/application.ts
+++ b/apps/dokploy/server/api/routers/application.ts
@@ -4,6 +4,7 @@ import {
deleteAllMiddlewares,
findApplicationById,
findEnvironmentById,
+ findPreviewDeploymentsByApplicationId,
findProjectById,
getAccessibleServerIds,
getApplicationStats,
@@ -16,6 +17,7 @@ import {
removeDeployments,
removeDirectoryCode,
removeMonitoringDirectory,
+ removePreviewDeployment,
removeService,
removeTraefikConfig,
startService,
@@ -234,6 +236,15 @@ export const applicationRouter = createTRPCRouter({
});
}
+ const previewDeploymentsList =
+ await findPreviewDeploymentsByApplicationId(input.applicationId);
+
+ for (const previewDeployment of previewDeploymentsList) {
+ try {
+ await removePreviewDeployment(previewDeployment.previewDeploymentId);
+ } catch (_) {}
+ }
+
const result = await db
.delete(applications)
.where(eq(applications.applicationId, input.applicationId))
diff --git a/apps/dokploy/server/api/routers/schedule.ts b/apps/dokploy/server/api/routers/schedule.ts
index 9f0c16b1ee..c770717902 100644
--- a/apps/dokploy/server/api/routers/schedule.ts
+++ b/apps/dokploy/server/api/routers/schedule.ts
@@ -315,13 +315,17 @@ export const scheduleRouter = createTRPCRouter({
await checkPermission(ctx, { schedule: ["create"] });
}
try {
- await runCommand(input.scheduleId);
+ const deployment = await runCommand(input.scheduleId);
await audit(ctx, {
action: "run",
resourceType: "schedule",
resourceId: input.scheduleId,
});
- return true;
+ return {
+ status: deployment.status,
+ deploymentId: deployment.deploymentId,
+ logPath: deployment.logPath,
+ };
} catch (error) {
throw new TRPCError({
code: "INTERNAL_SERVER_ERROR",
diff --git a/apps/dokploy/styles/globals.css b/apps/dokploy/styles/globals.css
index 50c516bd17..d212474c75 100644
--- a/apps/dokploy/styles/globals.css
+++ b/apps/dokploy/styles/globals.css
@@ -269,6 +269,12 @@
@apply bg-background text-foreground;
}
+ /* Cursor pointer on buttons */
+ button:not([disabled]),
+ [role="button"]:not([disabled]) {
+ cursor: pointer;
+ }
+
/* Custom scrollbar styling */
::-webkit-scrollbar {
width: 0.3125rem;
diff --git a/apps/schedules/src/utils.ts b/apps/schedules/src/utils.ts
index 7caa106afc..85de85f9a9 100644
--- a/apps/schedules/src/utils.ts
+++ b/apps/schedules/src/utils.ts
@@ -180,6 +180,9 @@ export const initializeJobs = async () => {
where: eq(schedules.enabled, true),
with: {
application: {
+ columns: {
+ applicationId: true,
+ },
with: {
server: true,
},
@@ -227,6 +230,9 @@ export const initializeJobs = async () => {
where: eq(volumeBackups.enabled, true),
with: {
application: {
+ columns: {
+ applicationId: true,
+ },
with: {
server: true,
},
diff --git a/packages/server/src/db/schema/rollbacks.ts b/packages/server/src/db/schema/rollbacks.ts
index ec27d683f8..5e3d407768 100644
--- a/packages/server/src/db/schema/rollbacks.ts
+++ b/packages/server/src/db/schema/rollbacks.ts
@@ -1,4 +1,5 @@
import type { Application } from "@dokploy/server/services/application";
+import type { Environment } from "@dokploy/server/services/environment";
import type { Mount } from "@dokploy/server/services/mount";
import type { Port } from "@dokploy/server/services/port";
import type { Project } from "@dokploy/server/services/project";
@@ -27,7 +28,7 @@ export const rollbacks = pgTable("rollback", {
.$defaultFn(() => new Date().toISOString()),
fullContext: jsonb("fullContext").$type<
Application & {
- environment: {
+ environment: Environment & {
project: Project;
};
mounts: Mount[];
diff --git a/packages/server/src/services/port.ts b/packages/server/src/services/port.ts
index 3ba1b73766..bcc5831a3a 100644
--- a/packages/server/src/services/port.ts
+++ b/packages/server/src/services/port.ts
@@ -30,12 +30,8 @@ export const finPortById = async (portId: string) => {
where: eq(ports.portId, portId),
with: {
application: {
- with: {
- environment: {
- with: {
- project: true,
- },
- },
+ columns: {
+ applicationId: true,
},
},
},
diff --git a/packages/server/src/services/preview-deployment.ts b/packages/server/src/services/preview-deployment.ts
index 20f64259bc..cc3d3dc020 100644
--- a/packages/server/src/services/preview-deployment.ts
+++ b/packages/server/src/services/preview-deployment.ts
@@ -17,7 +17,7 @@ import { manageDomain } from "../utils/traefik/domain";
import { findApplicationById } from "./application";
import { removeDeploymentsByPreviewDeploymentId } from "./deployment";
import { createDomain } from "./domain";
-import { type Github, getIssueComment } from "./github";
+import { findGithubById, getIssueComment } from "./github";
import { getWebServerSettings } from "./web-server-settings";
export type PreviewDeployment = typeof previewDeployments.$inferSelect;
@@ -142,7 +142,17 @@ export const createPreviewDeployment = async (
org?.ownerId || "",
);
- const octokit = authGithub(application?.github as Github);
+ if (!application.githubId) {
+ throw new TRPCError({
+ code: "NOT_FOUND",
+ message: "Github Account not configured correctly",
+ });
+ }
+
+ // `findApplicationById` redacts `githubPrivateKey` from the `github`
+ // relation, so the provider must be refetched to authenticate.
+ const githubProvider = await findGithubById(application.githubId);
+ const octokit = authGithub(githubProvider);
const runningComment = getIssueComment(
application.name,
diff --git a/packages/server/src/services/rollbacks.ts b/packages/server/src/services/rollbacks.ts
index 97fa4b1c5f..b3ccc5a5f8 100644
--- a/packages/server/src/services/rollbacks.ts
+++ b/packages/server/src/services/rollbacks.ts
@@ -19,6 +19,7 @@ import { execAsync, execAsyncRemote } from "../utils/process/execAsync";
import { getRemoteDocker } from "../utils/servers/remote-docker";
import { type Application, findApplicationById } from "./application";
import { findDeploymentById } from "./deployment";
+import type { Environment } from "./environment";
import type { Mount } from "./mount";
import type { Port } from "./port";
import type { Project } from "./project";
@@ -105,19 +106,7 @@ export const findRollbackById = async (rollbackId: string) => {
const result = await db.query.rollbacks.findFirst({
where: eq(rollbacks.rollbackId, rollbackId),
with: {
- deployment: {
- with: {
- application: {
- with: {
- environment: {
- with: {
- project: true,
- },
- },
- },
- },
- },
- },
+ deployment: true,
},
});
@@ -213,7 +202,7 @@ const rollbackApplication = async (
image: string,
serverId?: string | null,
fullContext?: Application & {
- environment: {
+ environment: Environment & {
project: Project;
};
mounts: Mount[];
@@ -275,6 +264,7 @@ const rollbackApplication = async (
const envVariables = prepareEnvironmentVariables(
env,
fullContext.environment.project.env,
+ fullContext.environment.env,
);
let rollbackImage = image;
diff --git a/packages/server/src/services/schedule.ts b/packages/server/src/services/schedule.ts
index c94973e024..59b75cc0da 100644
--- a/packages/server/src/services/schedule.ts
+++ b/packages/server/src/services/schedule.ts
@@ -81,6 +81,11 @@ export const findScheduleById = async (scheduleId: string) => {
where: eq(schedules.scheduleId, scheduleId),
with: {
application: {
+ columns: {
+ applicationId: true,
+ appName: true,
+ serverId: true,
+ },
with: {
environment: {
with: {
diff --git a/packages/server/src/services/volume-backups.ts b/packages/server/src/services/volume-backups.ts
index abd29df1f4..359ccf5f0d 100644
--- a/packages/server/src/services/volume-backups.ts
+++ b/packages/server/src/services/volume-backups.ts
@@ -13,6 +13,11 @@ export const findVolumeBackupById = async (volumeBackupId: string) => {
where: eq(volumeBackups.volumeBackupId, volumeBackupId),
with: {
application: {
+ columns: {
+ applicationId: true,
+ appName: true,
+ serverId: true,
+ },
with: {
environment: {
with: {
diff --git a/packages/server/src/utils/access-log/utils.ts b/packages/server/src/utils/access-log/utils.ts
index 9b472ee27d..ddaef3ec42 100644
--- a/packages/server/src/utils/access-log/utils.ts
+++ b/packages/server/src/utils/access-log/utils.ts
@@ -120,7 +120,7 @@ export function parseRawConfig(
if (search) {
parsedLogs = parsedLogs.filter((log) =>
- log.RequestHost.toLowerCase().includes(search.toLowerCase()),
+ (log.RequestHost ?? "").toLowerCase().includes(search.toLowerCase()),
);
}
diff --git a/packages/server/src/utils/builders/railpack.ts b/packages/server/src/utils/builders/railpack.ts
index b12296a566..b393f44603 100644
--- a/packages/server/src/utils/builders/railpack.ts
+++ b/packages/server/src/utils/builders/railpack.ts
@@ -53,14 +53,9 @@ export const getRailpackCommand = (application: ApplicationNested) => {
"build",
"--builder",
builderName,
- ...(cacheKey
- ? [
- "--build-arg",
- `secrets-hash=${secretsHash}`,
- "--build-arg",
- `cache-key=${cacheKey}`,
- ]
- : []),
+ "--build-arg",
+ `secrets-hash=${secretsHash}`,
+ ...(cacheKey ? ["--build-arg", `cache-key=${cacheKey}`] : []),
"--build-arg",
`BUILDKIT_SYNTAX=ghcr.io/railwayapp/railpack-frontend:v${application.railpackVersion}`,
"-f",
diff --git a/packages/server/src/utils/schedules/utils.ts b/packages/server/src/utils/schedules/utils.ts
index 8107839d8b..e81f9a5823 100644
--- a/packages/server/src/utils/schedules/utils.ts
+++ b/packages/server/src/utils/schedules/utils.ts
@@ -55,25 +55,46 @@ export const runCommand = async (scheduleId: string) => {
description: "Schedule",
});
- if (scheduleType === "application" || scheduleType === "compose") {
- let containerId = "";
- let serverId = "";
- if (scheduleType === "application" && application) {
- const container = await getServiceContainer(
- application.appName,
- application.serverId,
- );
- containerId = container?.Id || "";
- serverId = application.serverId || "";
- }
- if (scheduleType === "compose" && compose) {
- const container = await getComposeContainer(compose, serviceName || "");
- containerId = container?.Id || "";
- serverId = compose.serverId || "";
- }
+ try {
+ if (scheduleType === "application" || scheduleType === "compose") {
+ let containerId = "";
+ let serverId = "";
+ if (scheduleType === "application" && application) {
+ const container = await getServiceContainer(
+ application.appName,
+ application.serverId,
+ );
+ containerId = container?.Id || "";
+ serverId = application.serverId || "";
+ }
+ if (scheduleType === "compose" && compose) {
+ const container = await getComposeContainer(compose, serviceName || "");
+ containerId = container?.Id || "";
+ serverId = compose.serverId || "";
+ }
+
+ if (!containerId) {
+ const target =
+ scheduleType === "compose"
+ ? `service '${serviceName}' of compose '${compose?.name}'`
+ : `application '${application?.appName}'`;
+ const message = `Container not found for ${target}, make sure the service is running`;
+ if (serverId) {
+ await execAsyncRemote(
+ serverId,
+ `echo ${quote([`❌ ${message}`])} >> ${quote([deployment.logPath])}`,
+ );
+ } else {
+ const writeStream = createWriteStream(deployment.logPath, {
+ flags: "a",
+ });
+ writeStream.write(`❌ ${message}\n`);
+ writeStream.end();
+ }
+ throw new Error(message);
+ }
- if (serverId) {
- try {
+ if (serverId) {
await execAsyncRemote(
serverId,
`
@@ -86,47 +107,45 @@ export const runCommand = async (scheduleId: string) => {
echo "✅ Command executed successfully" >> ${quote([deployment.logPath])};
`,
);
- } catch (error) {
- await updateDeploymentStatus(deployment.deploymentId, "error");
- throw error;
- }
- } else {
- const writeStream = createWriteStream(deployment.logPath, { flags: "a" });
+ } else {
+ const writeStream = createWriteStream(deployment.logPath, {
+ flags: "a",
+ });
- try {
- if (IS_CLOUD) {
+ try {
+ if (IS_CLOUD) {
+ writeStream.write(
+ "This feature is not available in the cloud version.",
+ );
+ writeStream.end();
+ await updateDeploymentStatus(deployment.deploymentId, "error");
+ return { ...deployment, status: "error" as const };
+ }
writeStream.write(
- "This feature is not available in the cloud version.",
+ `docker exec ${containerId} ${shellType} -c ${command}\n`,
+ );
+ await spawnAsync(
+ "docker",
+ ["exec", containerId, shellType, "-c", command],
+ (data) => {
+ if (writeStream.writable) {
+ writeStream.write(data);
+ }
+ },
);
+
+ writeStream.write("✅ Command executed successfully\n");
writeStream.end();
- return;
+ } catch (error) {
+ writeStream.write("❌ Command failed\n");
+ writeStream.write(
+ error instanceof Error ? error.message : "Unknown error",
+ );
+ writeStream.end();
+ throw error;
}
- writeStream.write(
- `docker exec ${containerId} ${shellType} -c ${command}\n`,
- );
- await spawnAsync(
- "docker",
- ["exec", containerId, shellType, "-c", command],
- (data) => {
- if (writeStream.writable) {
- writeStream.write(data);
- }
- },
- );
-
- writeStream.write("✅ Command executed successfully\n");
- } catch (error) {
- writeStream.write("❌ Command failed\n");
- writeStream.write(
- error instanceof Error ? error.message : "Unknown error",
- );
- writeStream.end();
- await updateDeploymentStatus(deployment.deploymentId, "error");
- throw error;
}
- }
- } else if (scheduleType === "dokploy-server") {
- try {
+ } else if (scheduleType === "dokploy-server") {
const writeStream = createWriteStream(deployment.logPath, { flags: "a" });
const { SCHEDULES_PATH } = paths();
const fullPath = path.join(SCHEDULES_PATH, appName || "");
@@ -151,18 +170,13 @@ export const runCommand = async (scheduleId: string) => {
cwd: fullPath,
},
);
- } catch (error) {
- await updateDeploymentStatus(deployment.deploymentId, "error");
- throw error;
- }
- } else if (scheduleType === "server") {
- try {
+ } else if (scheduleType === "server") {
const { SCHEDULES_PATH } = paths(true);
const fullPath = path.join(SCHEDULES_PATH, appName || "");
const command = `
set -e
echo "Running script" >> ${deployment.logPath};
- bash -c ${fullPath}/script.sh 2>&1 | tee -a ${deployment.logPath} || {
+ bash -c ${fullPath}/script.sh 2>&1 | tee -a ${deployment.logPath} || {
echo "❌ Command failed" >> ${deployment.logPath};
exit 1;
}
@@ -177,10 +191,11 @@ export const runCommand = async (scheduleId: string) => {
});
}
});
- } catch (error) {
- await updateDeploymentStatus(deployment.deploymentId, "error");
- throw error;
}
+ await updateDeploymentStatus(deployment.deploymentId, "done");
+ return { ...deployment, status: "done" as const };
+ } catch {
+ await updateDeploymentStatus(deployment.deploymentId, "error");
+ return { ...deployment, status: "error" as const };
}
- await updateDeploymentStatus(deployment.deploymentId, "done");
};