Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -60,6 +60,23 @@ export const commonCronExpressions = [
{ label: "Custom", value: "custom" },
];

const commonTimezones = [
{ label: "UTC (Default)", value: "__UTC__" },
{ label: "America/Los_Angeles (PST/PDT)", value: "America/Los_Angeles" },
{ label: "America/New_York (EST/EDT)", value: "America/New_York" },
{ label: "America/Chicago (CST/CDT)", value: "America/Chicago" },
{ label: "America/Denver (MST/MDT)", value: "America/Denver" },
{ label: "Europe/London (GMT/BST)", value: "Europe/London" },
{ label: "Europe/Paris (CET/CEST)", value: "Europe/Paris" },
{ label: "Europe/Berlin (CET/CEST)", value: "Europe/Berlin" },
{ label: "Asia/Tokyo (JST)", value: "Asia/Tokyo" },
{ label: "Asia/Shanghai (CST)", value: "Asia/Shanghai" },
{ label: "Asia/Kolkata (IST)", value: "Asia/Kolkata" },
{ label: "Asia/Dubai (GST)", value: "Asia/Dubai" },
{ label: "Australia/Sydney (AEST/AEDT)", value: "Australia/Sydney" },
{ label: "Australia/Melbourne (AEST/AEDT)", value: "Australia/Melbourne" },
];

const formSchema = z
.object({
name: z.string().min(1, "Name is required"),
Expand All @@ -75,6 +92,7 @@ const formSchema = z
"dokploy-server",
]),
script: z.string(),
timezone: z.string().nullable().optional(),
})
.superRefine((data, ctx) => {
if (data.scheduleType === "compose" && !data.serviceName) {
Expand Down Expand Up @@ -213,6 +231,7 @@ export const HandleSchedules = ({ id, scheduleId, scheduleType }: Props) => {
serviceName: "",
scheduleType: scheduleType || "application",
script: "",
timezone: null,
},
});

Expand Down Expand Up @@ -251,6 +270,7 @@ export const HandleSchedules = ({ id, scheduleId, scheduleType }: Props) => {
serviceName: schedule.serviceName || "",
scheduleType: schedule.scheduleType,
script: schedule.script || "",
timezone: schedule.timezone || null,
});
}
}, [form, schedule, scheduleId]);
Expand Down Expand Up @@ -464,6 +484,58 @@ export const HandleSchedules = ({ id, scheduleId, scheduleType }: Props) => {
formControl={form.control}
/>

<FormField
control={form.control}
name="timezone"
render={({ field }) => (
<FormItem>
<FormLabel className="flex items-center gap-2">
Timezone
<TooltipProvider>
<Tooltip>
<TooltipTrigger asChild>
<Info className="w-4 h-4 text-muted-foreground cursor-help" />
</TooltipTrigger>
<TooltipContent>
<p>
Select the timezone for this schedule. If not
specified, UTC will be used.
</p>
<p className="mt-1">
Example: Setting "9:00 AM" with
"America/Los_Angeles" will run at 9 AM Pacific Time.
</p>
</TooltipContent>
</Tooltip>
</TooltipProvider>
</FormLabel>
<Select
value={field.value || "__UTC__"}
onValueChange={(value) => {
field.onChange(value === "__UTC__" ? null : value);
}}
>
<FormControl>
<SelectTrigger>
<SelectValue placeholder="UTC (Default)" />
</SelectTrigger>
</FormControl>
<SelectContent>
{commonTimezones.map((tz) => (
<SelectItem key={tz.value} value={tz.value}>
{tz.label}
</SelectItem>
))}
</SelectContent>
</Select>
<FormDescription>
The timezone for the cron schedule. Leave as UTC if unsure.
</FormDescription>
<FormMessage />
</FormItem>
)}
/>

{(scheduleTypeForm === "application" ||
scheduleTypeForm === "compose") && (
<>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -132,6 +132,19 @@ export const ShowSchedules = ({ id, scheduleType = "application" }: Props) => {
>
Cron: {schedule.cronExpression}
</Badge>
{schedule.timezone && (
<>
<span className="text-xs text-muted-foreground/50">
</span>
<Badge
variant="outline"
className="text-[10px] bg-transparent"
>
TZ: {schedule.timezone}
</Badge>
</>
)}
{schedule.scheduleType !== "server" &&
schedule.scheduleType !== "dokploy-server" && (
<>
Expand Down
2 changes: 2 additions & 0 deletions apps/dokploy/drizzle/0114_add_timezone_to_schedule.sql
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
ALTER TABLE "schedule" ADD COLUMN "timezone" text;

2 changes: 2 additions & 0 deletions apps/dokploy/server/api/routers/schedule.ts
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@ export const scheduleRouter = createTRPCRouter({
scheduleId: newSchedule.scheduleId,
type: "schedule",
cronSchedule: newSchedule.cronExpression,
timezone: newSchedule.timezone,
});
} else {
scheduleJob(newSchedule);
Expand All @@ -49,6 +50,7 @@ export const scheduleRouter = createTRPCRouter({
scheduleId: updatedSchedule.scheduleId,
type: "schedule",
cronSchedule: updatedSchedule.cronExpression,
timezone: updatedSchedule.timezone,
});
} else {
await removeJob({
Expand Down
1 change: 1 addition & 0 deletions apps/dokploy/server/utils/backup.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ type QueueJob =
type: "schedule";
cronSchedule: string;
scheduleId: string;
timezone?: string | null;
}
| {
type: "volume-backup";
Expand Down
1 change: 1 addition & 0 deletions apps/schedules/src/queue.ts
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,7 @@ export const scheduleJob = (job: QueueJob) => {
jobQueue.add(job.scheduleId, job, {
repeat: {
pattern: job.cronSchedule,
tz: job.timezone || "UTC",
},
});
} else if (job.type === "volume-backup") {
Expand Down
1 change: 1 addition & 0 deletions apps/schedules/src/schema.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ export const jobQueueSchema = z.discriminatedUnion("type", [
cronSchedule: z.string(),
type: z.literal("schedule"),
scheduleId: z.string(),
timezone: z.string().nullable().optional(),
}),
z.object({
cronSchedule: z.string(),
Expand Down
1 change: 1 addition & 0 deletions apps/schedules/src/utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -191,6 +191,7 @@ export const initializeJobs = async () => {
scheduleId: schedule.scheduleId,
type: "schedule",
cronSchedule: schedule.cronExpression,
timezone: schedule.timezone,
});
}
logger.info(
Expand Down
29 changes: 20 additions & 9 deletions packages/server/package.json
Original file line number Diff line number Diff line change
@@ -1,21 +1,32 @@
{
"name": "@dokploy/server",
"version": "1.0.0",
"main": "./src/index.ts",
"main": "./dist/index.js",
"type": "module",
"exports": {
".": "./src/index.ts",
".": {
"import": "./dist/index.js",
"require": "./dist/index.cjs.js"
},
"./db": {
"import": "./src/db/index.ts",
"import": "./dist/db/index.js",
"require": "./dist/db/index.cjs.js"
},
"./setup/*": {
"import": "./src/setup/*.ts",
"require": "./dist/setup/index.cjs.js"
"./*": {
"import": "./dist/*",
"require": "./dist/*.cjs"
},
"./dist": {
"import": "./dist/index.js",
"require": "./dist/index.cjs.js"
},
"./dist/db": {
"import": "./dist/db/index.js",
"require": "./dist/db/index.cjs.js"
},
"./constants": {
"import": "./src/constants/index.ts",
"require": "./dist/constants.cjs.js"
"./dist/db/schema": {
"import": "./dist/db/schema/index.js",
"require": "./dist/db/schema/index.cjs.js"
}
},
"scripts": {
Expand Down
24 changes: 23 additions & 1 deletion packages/server/src/db/schema/schedule.ts
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,7 @@ export const schedules = pgTable("schedule", {
onDelete: "cascade",
}),
enabled: boolean("enabled").notNull().default(true),
timezone: text("timezone"),
createdAt: text("createdAt")
.notNull()
.$defaultFn(() => new Date().toISOString()),
Expand Down Expand Up @@ -76,7 +77,28 @@ export const schedulesRelations = relations(schedules, ({ one, many }) => ({
deployments: many(deployments),
}));

export const createScheduleSchema = createInsertSchema(schedules);
export const createScheduleSchema = createInsertSchema(schedules).extend({
timezone: z
.string()
.nullable()
.optional()
.refine(
(val) => {
if (!val || val === "") return true;
try {
// Validate IANA timezone string
Intl.DateTimeFormat(undefined, { timeZone: val });
return true;
} catch {
return false;
}
},
{
message:
"Invalid timezone. Must be a valid IANA timezone identifier (e.g., America/Los_Angeles, Europe/London)",
},
),
});

export const updateScheduleSchema = createScheduleSchema.extend({
scheduleId: z.string().min(1),
Expand Down
Loading