Compare commits

..

1 Commits

Author SHA1 Message Date
Mauricio Siu
5a8508e623 feat(caddy): add Caddy setup and configuration management
- Introduced a new Caddy setup file to manage Caddy container initialization and configuration.
- Added constants for Caddy paths in the constants index.
- Implemented functions to create default Caddy configurations and middlewares, enhancing the deployment process.
2025-04-25 00:09:37 -06:00
65 changed files with 677 additions and 11921 deletions

View File

@@ -52,7 +52,7 @@ feat: add new feature
Before you start, please make the clone based on the `canary` branch, since the `main` branch is the source of truth and should always reflect the latest stable release, also the PRs will be merged to the `canary` branch.
We use Node v20.9.0 and recommend this specific version. If you have nvm installed, you can run `nvm install 20.9.0 && nvm use` in the root directory.
We use Node v20.9.0 and recommend this specific version. If you have nvm installed, you can run `nvm install 20.9.0 && nvm use` in the root directory.
```bash
git clone https://github.com/dokploy/dokploy.git
@@ -147,9 +147,11 @@ curl -sSL https://railpack.com/install.sh | sh
```bash
# Install Buildpacks
curl -sSL "https://github.com/buildpacks/pack/releases/download/v0.35.0/pack-v0.35.0-linux.tgz" | tar -C /usr/local/bin/ --no-same-owner -xzv pack
curl -sSL "https://github.com/buildpacks/pack/releases/download/v0.32.1/pack-v0.32.1-linux.tgz" | tar -C /usr/local/bin/ --no-same-owner -xzv pack
```
## Pull Request
- The `main` branch is the source of truth and should always reflect the latest stable release.
@@ -167,6 +169,7 @@ Thank you for your contribution!
To add a new template, go to `https://github.com/Dokploy/templates` repository and read the README.md file.
### Recommendations
- Use the same name of the folder as the id of the template.

View File

@@ -49,7 +49,7 @@ RUN curl -fsSL https://get.docker.com -o get-docker.sh && sh get-docker.sh && rm
# Install Nixpacks and tsx
# | VERBOSE=1 VERSION=1.21.0 bash
ARG NIXPACKS_VERSION=1.35.0
ARG NIXPACKS_VERSION=1.29.1
RUN curl -sSL https://nixpacks.com/install.sh -o install.sh \
&& chmod +x install.sh \
&& ./install.sh \

View File

@@ -34,7 +34,6 @@ const baseApp: ApplicationNested = {
giteaRepository: "",
cleanCache: false,
watchPaths: [],
enableSubmodules: false,
applicationStatus: "done",
appName: "",
autoDeploy: true,

View File

@@ -51,35 +51,6 @@ describe("processTemplate", () => {
expect(result.domains).toHaveLength(0);
expect(result.mounts).toHaveLength(0);
});
it("should allow creation of real jwt secret", () => {
const template: CompleteTemplate = {
metadata: {} as any,
variables: {
jwt_secret: "cQsdycq1hDLopQonF6jUTqgQc5WEZTwWLL02J6XJ",
anon_payload: JSON.stringify({
role: "tester",
iss: "dockploy",
iat: "${timestamps:2025-01-01T00:00:00Z}",
exp: "${timestamps:2030-01-01T00:00:00Z}",
}),
anon_key: "${jwt:jwt_secret:anon_payload}",
},
config: {
domains: [],
env: {
ANON_KEY: "${anon_key}",
},
},
};
const result = processTemplate(template, mockSchema);
expect(result.envs).toHaveLength(1);
expect(result.envs).toContain(
"ANON_KEY=eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpYXQiOiIxNzM1Njg5NjAwIiwiZXhwIjoiMTg5MzQ1NjAwMCIsInJvbGUiOiJ0ZXN0ZXIiLCJpc3MiOiJkb2NrcGxveSJ9.BG5JoxL2_NaTFbPgyZdm3kRWenf_O3su_HIRKGCJ_kY",
);
expect(result.mounts).toHaveLength(0);
expect(result.domains).toHaveLength(0);
});
});
describe("domains processing", () => {

View File

@@ -1,232 +0,0 @@
import type { Schema } from "@dokploy/server/templates";
import { processValue } from "@dokploy/server/templates/processors";
import { describe, expect, it } from "vitest";
describe("helpers functions", () => {
// Mock schema for testing
const mockSchema: Schema = {
projectName: "test",
serverIp: "127.0.0.1",
};
// some helpers to test jwt
type JWTParts = [string, string, string];
const jwtMatchExp = /^[A-Za-z0-9-_]+\.[A-Za-z0-9-_]+\.[A-Za-z0-9-_]+$/;
const jwtBase64Decode = (str: string) => {
const base64 = str.replace(/-/g, "+").replace(/_/g, "/");
const padding = "=".repeat((4 - (base64.length % 4)) % 4);
const decoded = Buffer.from(base64 + padding, "base64").toString("utf-8");
return JSON.parse(decoded);
};
const jwtCheckHeader = (jwtHeader: string) => {
const decodedHeader = jwtBase64Decode(jwtHeader);
expect(decodedHeader).toHaveProperty("alg");
expect(decodedHeader).toHaveProperty("typ");
expect(decodedHeader.alg).toEqual("HS256");
expect(decodedHeader.typ).toEqual("JWT");
};
describe("${domain}", () => {
it("should generate a random domain", () => {
const domain = processValue("${domain}", {}, mockSchema);
expect(domain.startsWith(`${mockSchema.projectName}-`)).toBeTruthy();
expect(
domain.endsWith(
`${mockSchema.serverIp.replaceAll(".", "-")}.traefik.me`,
),
).toBeTruthy();
});
});
describe("${base64}", () => {
it("should generate a base64 string", () => {
const base64 = processValue("${base64}", {}, mockSchema);
expect(base64).toMatch(/^[A-Za-z0-9+=/]+={0,2}$/);
});
it.each([
[4, 8],
[8, 12],
[16, 24],
[32, 44],
[64, 88],
[128, 172],
])(
"should generate a base64 string from parameter %d bytes length",
(length, finalLength) => {
const base64 = processValue(`\${base64:${length}}`, {}, mockSchema);
expect(base64).toMatch(/^[A-Za-z0-9+=/]+={0,2}$/);
expect(base64.length).toBe(finalLength);
},
);
});
describe("${password}", () => {
it("should generate a password string", () => {
const password = processValue("${password}", {}, mockSchema);
expect(password).toMatch(/^[A-Za-z0-9]+$/);
});
it.each([6, 8, 12, 16, 32])(
"should generate a password string respecting parameter %d length",
(length) => {
const password = processValue(`\${password:${length}}`, {}, mockSchema);
expect(password).toMatch(/^[A-Za-z0-9]+$/);
expect(password.length).toBe(length);
},
);
});
describe("${hash}", () => {
it("should generate a hash string", () => {
const hash = processValue("${hash}", {}, mockSchema);
expect(hash).toMatch(/^[A-Za-z0-9]+$/);
});
it.each([6, 8, 12, 16, 32])(
"should generate a hash string respecting parameter %d length",
(length) => {
const hash = processValue(`\${hash:${length}}`, {}, mockSchema);
expect(hash).toMatch(/^[A-Za-z0-9]+$/);
expect(hash.length).toBe(length);
},
);
});
describe("${uuid}", () => {
it("should generate a UUID string", () => {
const uuid = processValue("${uuid}", {}, mockSchema);
expect(uuid).toMatch(
/^[0-9a-f]{8}-[0-9a-f]{4}-4[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/,
);
});
});
describe("${timestamp}", () => {
it("should generate a timestamp string in milliseconds", () => {
const timestamp = processValue("${timestamp}", {}, mockSchema);
const nowLength = Math.floor(Date.now()).toString().length;
expect(timestamp).toMatch(/^\d+$/);
expect(timestamp.length).toBe(nowLength);
});
});
describe("${timestampms}", () => {
it("should generate a timestamp string in milliseconds", () => {
const timestamp = processValue("${timestampms}", {}, mockSchema);
const nowLength = Date.now().toString().length;
expect(timestamp).toMatch(/^\d+$/);
expect(timestamp.length).toBe(nowLength);
});
it("should generate a timestamp string in milliseconds from parameter", () => {
const timestamp = processValue(
"${timestampms:2025-01-01}",
{},
mockSchema,
);
expect(timestamp).toEqual("1735689600000");
});
});
describe("${timestamps}", () => {
it("should generate a timestamp string in seconds", () => {
const timestamps = processValue("${timestamps}", {}, mockSchema);
const nowLength = Math.floor(Date.now() / 1000).toString().length;
expect(timestamps).toMatch(/^\d+$/);
expect(timestamps.length).toBe(nowLength);
});
it("should generate a timestamp string in seconds from parameter", () => {
const timestamps = processValue(
"${timestamps:2025-01-01}",
{},
mockSchema,
);
expect(timestamps).toEqual("1735689600");
});
});
describe("${randomPort}", () => {
it("should generate a random port string", () => {
const randomPort = processValue("${randomPort}", {}, mockSchema);
expect(randomPort).toMatch(/^\d+$/);
expect(Number(randomPort)).toBeLessThan(65536);
});
});
describe("${username}", () => {
it("should generate a username string", () => {
const username = processValue("${username}", {}, mockSchema);
expect(username).toMatch(/^[a-zA-Z0-9._-]{3,}$/);
});
});
describe("${email}", () => {
it("should generate an email string", () => {
const email = processValue("${email}", {}, mockSchema);
expect(email).toMatch(/^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}$/);
});
});
describe("${jwt}", () => {
it("should generate a JWT string", () => {
const jwt = processValue("${jwt}", {}, mockSchema);
expect(jwt).toMatch(jwtMatchExp);
const parts = jwt.split(".") as JWTParts;
const decodedPayload = jwtBase64Decode(parts[1]);
jwtCheckHeader(parts[0]);
expect(decodedPayload).toHaveProperty("iat");
expect(decodedPayload).toHaveProperty("iss");
expect(decodedPayload).toHaveProperty("exp");
expect(decodedPayload.iss).toEqual("dokploy");
});
it.each([6, 8, 12, 16, 32])(
"should generate a random hex string from parameter %d byte length",
(length) => {
const jwt = processValue(`\${jwt:${length}}`, {}, mockSchema);
expect(jwt).toMatch(/^[A-Za-z0-9-_.]+$/);
expect(jwt.length).toBeGreaterThanOrEqual(length); // bytes translated to hex can take up to 2x the length
expect(jwt.length).toBeLessThanOrEqual(length * 2);
},
);
});
describe("${jwt:secret}", () => {
it("should generate a JWT string respecting parameter secret from variable", () => {
const jwt = processValue(
"${jwt:secret}",
{ secret: "mysecret" },
mockSchema,
);
expect(jwt).toMatch(jwtMatchExp);
const parts = jwt.split(".") as JWTParts;
const decodedPayload = jwtBase64Decode(parts[1]);
jwtCheckHeader(parts[0]);
expect(decodedPayload).toHaveProperty("iat");
expect(decodedPayload).toHaveProperty("iss");
expect(decodedPayload).toHaveProperty("exp");
expect(decodedPayload.iss).toEqual("dokploy");
});
});
describe("${jwt:secret:payload}", () => {
it("should generate a JWT string respecting parameters secret and payload from variables", () => {
const iat = Math.floor(new Date("2025-01-01T00:00:00Z").getTime() / 1000);
const expiry = iat + 3600;
const jwt = processValue(
"${jwt:secret:payload}",
{
secret: "mysecret",
payload: `{"iss": "test-issuer", "iat": ${iat}, "exp": ${expiry}, "customprop": "customvalue"}`,
},
mockSchema,
);
expect(jwt).toMatch(jwtMatchExp);
const parts = jwt.split(".") as JWTParts;
jwtCheckHeader(parts[0]);
const decodedPayload = jwtBase64Decode(parts[1]);
expect(decodedPayload).toHaveProperty("iat");
expect(decodedPayload.iat).toEqual(iat);
expect(decodedPayload).toHaveProperty("iss");
expect(decodedPayload.iss).toEqual("test-issuer");
expect(decodedPayload).toHaveProperty("exp");
expect(decodedPayload.exp).toEqual(expiry);
expect(decodedPayload).toHaveProperty("customprop");
expect(decodedPayload.customprop).toEqual("customvalue");
expect(jwt).toEqual(
"eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpYXQiOjE3MzU2ODk2MDAsImV4cCI6MTczNTY5MzIwMCwiaXNzIjoidGVzdC1pc3N1ZXIiLCJjdXN0b21wcm9wIjoiY3VzdG9tdmFsdWUifQ.m42U7PZSUSCf7gBOJrxJir0rQmyPq4rA59Dydr_QahI",
);
});
});
});

View File

@@ -16,7 +16,6 @@ const baseApp: ApplicationNested = {
applicationStatus: "done",
appName: "",
autoDeploy: true,
enableSubmodules: false,
serverId: "",
branch: null,
dockerBuildStage: "",

View File

@@ -31,7 +31,6 @@ import {
SelectTrigger,
SelectValue,
} from "@/components/ui/select";
import { Switch } from "@/components/ui/switch";
import {
Tooltip,
TooltipContent,
@@ -59,7 +58,6 @@ const BitbucketProviderSchema = z.object({
branch: z.string().min(1, "Branch is required"),
bitbucketId: z.string().min(1, "Bitbucket Provider is required"),
watchPaths: z.array(z.string()).optional(),
enableSubmodules: z.boolean().optional(),
});
type BitbucketProvider = z.infer<typeof BitbucketProviderSchema>;
@@ -86,7 +84,6 @@ export const SaveBitbucketProvider = ({ applicationId }: Props) => {
bitbucketId: "",
branch: "",
watchPaths: [],
enableSubmodules: false,
},
resolver: zodResolver(BitbucketProviderSchema),
});
@@ -133,7 +130,6 @@ export const SaveBitbucketProvider = ({ applicationId }: Props) => {
buildPath: data.bitbucketBuildPath || "/",
bitbucketId: data.bitbucketId || "",
watchPaths: data.watchPaths || [],
enableSubmodules: data.enableSubmodules || false,
});
}
}, [form.reset, data, form]);
@@ -147,7 +143,6 @@ export const SaveBitbucketProvider = ({ applicationId }: Props) => {
bitbucketId: data.bitbucketId,
applicationId,
watchPaths: data.watchPaths || [],
enableSubmodules: data.enableSubmodules || false,
})
.then(async () => {
toast.success("Service Provided Saved");
@@ -472,21 +467,6 @@ export const SaveBitbucketProvider = ({ applicationId }: Props) => {
</FormItem>
)}
/>
<FormField
control={form.control}
name="enableSubmodules"
render={({ field }) => (
<FormItem className="flex items-center space-x-2">
<FormControl>
<Switch
checked={field.value}
onCheckedChange={field.onChange}
/>
</FormControl>
<FormLabel className="!mt-0">Enable Submodules</FormLabel>
</FormItem>
)}
/>
</div>
<div className="flex w-full justify-end">
<Button

View File

@@ -23,7 +23,6 @@ import {
TooltipProvider,
TooltipTrigger,
} from "@/components/ui/tooltip";
import { Switch } from "@/components/ui/switch";
import { api } from "@/utils/api";
import { zodResolver } from "@hookform/resolvers/zod";
import { KeyRoundIcon, LockIcon, X } from "lucide-react";
@@ -45,7 +44,6 @@ const GitProviderSchema = z.object({
branch: z.string().min(1, "Branch required"),
sshKey: z.string().optional(),
watchPaths: z.array(z.string()).optional(),
enableSubmodules: z.boolean().default(false),
});
type GitProvider = z.infer<typeof GitProviderSchema>;
@@ -69,7 +67,6 @@ export const SaveGitProvider = ({ applicationId }: Props) => {
repositoryURL: "",
sshKey: undefined,
watchPaths: [],
enableSubmodules: false,
},
resolver: zodResolver(GitProviderSchema),
});
@@ -82,7 +79,6 @@ export const SaveGitProvider = ({ applicationId }: Props) => {
buildPath: data.customGitBuildPath || "/",
repositoryURL: data.customGitUrl || "",
watchPaths: data.watchPaths || [],
enableSubmodules: data.enableSubmodules ?? false,
});
}
}, [form.reset, data, form]);
@@ -95,7 +91,6 @@ export const SaveGitProvider = ({ applicationId }: Props) => {
customGitSSHKeyId: values.sshKey === "none" ? null : values.sshKey,
applicationId,
watchPaths: values.watchPaths || [],
enableSubmodules: values.enableSubmodules,
})
.then(async () => {
toast.success("Git Provider Saved");
@@ -299,22 +294,6 @@ export const SaveGitProvider = ({ applicationId }: Props) => {
</FormItem>
)}
/>
<FormField
control={form.control}
name="enableSubmodules"
render={({ field }) => (
<FormItem className="flex items-center space-x-2">
<FormControl>
<Switch
checked={field.value}
onCheckedChange={field.onChange}
/>
</FormControl>
<FormLabel className="!mt-0">Enable Submodules</FormLabel>
</FormItem>
)}
/>
</div>
<div className="flex flex-row justify-end">

View File

@@ -31,7 +31,6 @@ import {
SelectTrigger,
SelectValue,
} from "@/components/ui/select";
import { Switch } from "@/components/ui/switch";
import {
Tooltip,
TooltipContent,
@@ -75,7 +74,6 @@ const GiteaProviderSchema = z.object({
branch: z.string().min(1, "Branch is required"),
giteaId: z.string().min(1, "Gitea Provider is required"),
watchPaths: z.array(z.string()).default([]),
enableSubmodules: z.boolean().optional(),
});
type GiteaProvider = z.infer<typeof GiteaProviderSchema>;
@@ -101,7 +99,6 @@ export const SaveGiteaProvider = ({ applicationId }: Props) => {
giteaId: "",
branch: "",
watchPaths: [],
enableSubmodules: false,
},
resolver: zodResolver(GiteaProviderSchema),
});
@@ -155,7 +152,6 @@ export const SaveGiteaProvider = ({ applicationId }: Props) => {
buildPath: data.giteaBuildPath || "/",
giteaId: data.giteaId || "",
watchPaths: data.watchPaths || [],
enableSubmodules: data.enableSubmodules || false,
});
}
}, [form.reset, data, form]);
@@ -169,7 +165,6 @@ export const SaveGiteaProvider = ({ applicationId }: Props) => {
giteaId: data.giteaId,
applicationId,
watchPaths: data.watchPaths,
enableSubmodules: data.enableSubmodules || false,
})
.then(async () => {
toast.success("Service Provider Saved");
@@ -503,21 +498,6 @@ export const SaveGiteaProvider = ({ applicationId }: Props) => {
</FormItem>
)}
/>
<FormField
control={form.control}
name="enableSubmodules"
render={({ field }) => (
<FormItem className="flex items-center space-x-2">
<FormControl>
<Switch
checked={field.value}
onCheckedChange={field.onChange}
/>
</FormControl>
<FormLabel className="!mt-0">Enable Submodules</FormLabel>
</FormItem>
)}
/>
</div>
<div className="flex w-full justify-end">
<Button

View File

@@ -30,7 +30,6 @@ import {
SelectTrigger,
SelectValue,
} from "@/components/ui/select";
import { Switch } from "@/components/ui/switch";
import {
Tooltip,
TooltipContent,
@@ -58,7 +57,6 @@ const GithubProviderSchema = z.object({
branch: z.string().min(1, "Branch is required"),
githubId: z.string().min(1, "Github Provider is required"),
watchPaths: z.array(z.string()).optional(),
enableSubmodules: z.boolean().default(false),
});
type GithubProvider = z.infer<typeof GithubProviderSchema>;
@@ -83,7 +81,6 @@ export const SaveGithubProvider = ({ applicationId }: Props) => {
},
githubId: "",
branch: "",
enableSubmodules: false,
},
resolver: zodResolver(GithubProviderSchema),
});
@@ -127,7 +124,6 @@ export const SaveGithubProvider = ({ applicationId }: Props) => {
buildPath: data.buildPath || "/",
githubId: data.githubId || "",
watchPaths: data.watchPaths || [],
enableSubmodules: data.enableSubmodules ?? false,
});
}
}, [form.reset, data, form]);
@@ -141,7 +137,6 @@ export const SaveGithubProvider = ({ applicationId }: Props) => {
buildPath: data.buildPath,
githubId: data.githubId,
watchPaths: data.watchPaths || [],
enableSubmodules: data.enableSubmodules,
})
.then(async () => {
toast.success("Service Provided Saved");
@@ -463,22 +458,6 @@ export const SaveGithubProvider = ({ applicationId }: Props) => {
</FormItem>
)}
/>
<FormField
control={form.control}
name="enableSubmodules"
render={({ field }) => (
<FormItem className="flex items-center space-x-2">
<FormControl>
<Switch
checked={field.value}
onCheckedChange={field.onChange}
/>
</FormControl>
<FormLabel className="!mt-0">Enable Submodules</FormLabel>
</FormItem>
)}
/>
</div>
<div className="flex w-full justify-end">
<Button

View File

@@ -31,7 +31,6 @@ import {
SelectTrigger,
SelectValue,
} from "@/components/ui/select";
import { Switch } from "@/components/ui/switch";
import {
Tooltip,
TooltipContent,
@@ -61,7 +60,6 @@ const GitlabProviderSchema = z.object({
branch: z.string().min(1, "Branch is required"),
gitlabId: z.string().min(1, "Gitlab Provider is required"),
watchPaths: z.array(z.string()).optional(),
enableSubmodules: z.boolean().default(false),
});
type GitlabProvider = z.infer<typeof GitlabProviderSchema>;
@@ -88,7 +86,6 @@ export const SaveGitlabProvider = ({ applicationId }: Props) => {
},
gitlabId: "",
branch: "",
enableSubmodules: false,
},
resolver: zodResolver(GitlabProviderSchema),
});
@@ -138,7 +135,6 @@ export const SaveGitlabProvider = ({ applicationId }: Props) => {
buildPath: data.gitlabBuildPath || "/",
gitlabId: data.gitlabId || "",
watchPaths: data.watchPaths || [],
enableSubmodules: data.enableSubmodules ?? false,
});
}
}, [form.reset, data, form]);
@@ -154,7 +150,6 @@ export const SaveGitlabProvider = ({ applicationId }: Props) => {
gitlabProjectId: data.repository.id,
gitlabPathNamespace: data.repository.gitlabPathNamespace,
watchPaths: data.watchPaths || [],
enableSubmodules: data.enableSubmodules,
})
.then(async () => {
toast.success("Service Provided Saved");
@@ -488,21 +483,6 @@ export const SaveGitlabProvider = ({ applicationId }: Props) => {
</FormItem>
)}
/>
<FormField
control={form.control}
name="enableSubmodules"
render={({ field }) => (
<FormItem className="flex items-center space-x-2">
<FormControl>
<Switch
checked={field.value}
onCheckedChange={field.onChange}
/>
</FormControl>
<FormLabel className="!mt-0">Enable Submodules</FormLabel>
</FormItem>
)}
/>
</div>
<div className="flex w-full justify-end">
<Button

View File

@@ -65,7 +65,7 @@ export const ShowProviderForm = ({ applicationId }: Props) => {
setSab(e as TabState);
}}
>
<div className="flex flex-row items-center justify-between w-full gap-4">
<div className="flex flex-row items-center justify-between w-full gap-4">
<TabsList className="md:grid md:w-fit md:grid-cols-7 max-md:overflow-x-scroll justify-start bg-transparent overflow-y-hidden">
<TabsTrigger
value="github"

View File

@@ -79,22 +79,6 @@ export const ComposeFileEditor = ({ composeId }: Props) => {
toast.error("Error updating the Compose config");
});
};
// Add keyboard shortcut for Ctrl+S/Cmd+S
useEffect(() => {
const handleKeyDown = (e: KeyboardEvent) => {
if ((e.ctrlKey || e.metaKey) && e.key === "s" && !isLoading) {
e.preventDefault();
form.handleSubmit(onSubmit)();
}
};
document.addEventListener("keydown", handleKeyDown);
return () => {
document.removeEventListener("keydown", handleKeyDown);
};
}, [form, onSubmit, isLoading]);
return (
<>
<div className="w-full flex flex-col gap-4 ">

View File

@@ -31,7 +31,6 @@ import {
SelectTrigger,
SelectValue,
} from "@/components/ui/select";
import { Switch } from "@/components/ui/switch";
import {
Tooltip,
TooltipContent,
@@ -59,7 +58,6 @@ const BitbucketProviderSchema = z.object({
branch: z.string().min(1, "Branch is required"),
bitbucketId: z.string().min(1, "Bitbucket Provider is required"),
watchPaths: z.array(z.string()).optional(),
enableSubmodules: z.boolean().default(false),
});
type BitbucketProvider = z.infer<typeof BitbucketProviderSchema>;
@@ -86,7 +84,6 @@ export const SaveBitbucketProviderCompose = ({ composeId }: Props) => {
bitbucketId: "",
branch: "",
watchPaths: [],
enableSubmodules: false,
},
resolver: zodResolver(BitbucketProviderSchema),
});
@@ -133,7 +130,6 @@ export const SaveBitbucketProviderCompose = ({ composeId }: Props) => {
composePath: data.composePath,
bitbucketId: data.bitbucketId || "",
watchPaths: data.watchPaths || [],
enableSubmodules: data.enableSubmodules ?? false,
});
}
}, [form.reset, data, form]);
@@ -149,7 +145,6 @@ export const SaveBitbucketProviderCompose = ({ composeId }: Props) => {
sourceType: "bitbucket",
composeStatus: "idle",
watchPaths: data.watchPaths,
enableSubmodules: data.enableSubmodules,
})
.then(async () => {
toast.success("Service Provided Saved");
@@ -474,21 +469,6 @@ export const SaveBitbucketProviderCompose = ({ composeId }: Props) => {
</FormItem>
)}
/>
<FormField
control={form.control}
name="enableSubmodules"
render={({ field }) => (
<FormItem className="flex items-center space-x-2">
<FormControl>
<Switch
checked={field.value}
onCheckedChange={field.onChange}
/>
</FormControl>
<FormLabel className="!mt-0">Enable Submodules</FormLabel>
</FormItem>
)}
/>
</div>
<div className="flex w-full justify-end">
<Button

View File

@@ -19,7 +19,6 @@ import {
SelectTrigger,
SelectValue,
} from "@/components/ui/select";
import { Switch } from "@/components/ui/switch";
import {
Tooltip,
TooltipContent,
@@ -44,7 +43,6 @@ const GitProviderSchema = z.object({
branch: z.string().min(1, "Branch required"),
sshKey: z.string().optional(),
watchPaths: z.array(z.string()).optional(),
enableSubmodules: z.boolean().default(false),
});
type GitProvider = z.infer<typeof GitProviderSchema>;
@@ -67,7 +65,6 @@ export const SaveGitProviderCompose = ({ composeId }: Props) => {
composePath: "./docker-compose.yml",
sshKey: undefined,
watchPaths: [],
enableSubmodules: false,
},
resolver: zodResolver(GitProviderSchema),
});
@@ -80,7 +77,6 @@ export const SaveGitProviderCompose = ({ composeId }: Props) => {
repositoryURL: data.customGitUrl || "",
composePath: data.composePath,
watchPaths: data.watchPaths || [],
enableSubmodules: data.enableSubmodules ?? false,
});
}
}, [form.reset, data, form]);
@@ -95,7 +91,6 @@ export const SaveGitProviderCompose = ({ composeId }: Props) => {
composePath: values.composePath,
composeStatus: "idle",
watchPaths: values.watchPaths || [],
enableSubmodules: values.enableSubmodules,
})
.then(async () => {
toast.success("Git Provider Saved");
@@ -300,21 +295,6 @@ export const SaveGitProviderCompose = ({ composeId }: Props) => {
</FormItem>
)}
/>
<FormField
control={form.control}
name="enableSubmodules"
render={({ field }) => (
<FormItem className="flex items-center space-x-2">
<FormControl>
<Switch
checked={field.value}
onCheckedChange={field.onChange}
/>
</FormControl>
<FormLabel className="!mt-0">Enable Submodules</FormLabel>
</FormItem>
)}
/>
</div>
<div className="flex flex-row justify-end">

View File

@@ -31,7 +31,6 @@ import {
SelectTrigger,
SelectValue,
} from "@/components/ui/select";
import { Switch } from "@/components/ui/switch";
import {
Tooltip,
TooltipContent,
@@ -60,7 +59,6 @@ const GiteaProviderSchema = z.object({
branch: z.string().min(1, "Branch is required"),
giteaId: z.string().min(1, "Gitea Provider is required"),
watchPaths: z.array(z.string()).optional(),
enableSubmodules: z.boolean().default(false),
});
type GiteaProvider = z.infer<typeof GiteaProviderSchema>;
@@ -85,7 +83,6 @@ export const SaveGiteaProviderCompose = ({ composeId }: Props) => {
giteaId: "",
branch: "",
watchPaths: [],
enableSubmodules: false,
},
resolver: zodResolver(GiteaProviderSchema),
});
@@ -139,7 +136,6 @@ export const SaveGiteaProviderCompose = ({ composeId }: Props) => {
composePath: data.composePath || "./docker-compose.yml",
giteaId: data.giteaId || "",
watchPaths: data.watchPaths || [],
enableSubmodules: data.enableSubmodules ?? false,
});
}
}, [form.reset, data, form]);
@@ -155,7 +151,6 @@ export const SaveGiteaProviderCompose = ({ composeId }: Props) => {
sourceType: "gitea",
composeStatus: "idle",
watchPaths: data.watchPaths,
enableSubmodules: data.enableSubmodules,
} as any)
.then(async () => {
toast.success("Service Provider Saved");
@@ -474,21 +469,6 @@ export const SaveGiteaProviderCompose = ({ composeId }: Props) => {
</FormItem>
)}
/>
<FormField
control={form.control}
name="enableSubmodules"
render={({ field }) => (
<FormItem className="flex items-center space-x-2">
<FormControl>
<Switch
checked={field.value}
onCheckedChange={field.onChange}
/>
</FormControl>
<FormLabel className="!mt-0">Enable Submodules</FormLabel>
</FormItem>
)}
/>
</div>
<div className="flex justify-end">

View File

@@ -30,7 +30,6 @@ import {
SelectTrigger,
SelectValue,
} from "@/components/ui/select";
import { Switch } from "@/components/ui/switch";
import {
Tooltip,
TooltipContent,
@@ -58,7 +57,6 @@ const GithubProviderSchema = z.object({
branch: z.string().min(1, "Branch is required"),
githubId: z.string().min(1, "Github Provider is required"),
watchPaths: z.array(z.string()).optional(),
enableSubmodules: z.boolean().default(false),
});
type GithubProvider = z.infer<typeof GithubProviderSchema>;
@@ -84,7 +82,6 @@ export const SaveGithubProviderCompose = ({ composeId }: Props) => {
githubId: "",
branch: "",
watchPaths: [],
enableSubmodules: false,
},
resolver: zodResolver(GithubProviderSchema),
});
@@ -128,7 +125,6 @@ export const SaveGithubProviderCompose = ({ composeId }: Props) => {
composePath: data.composePath,
githubId: data.githubId || "",
watchPaths: data.watchPaths || [],
enableSubmodules: data.enableSubmodules ?? false,
});
}
}, [form.reset, data, form]);
@@ -144,7 +140,6 @@ export const SaveGithubProviderCompose = ({ composeId }: Props) => {
sourceType: "github",
composeStatus: "idle",
watchPaths: data.watchPaths,
enableSubmodules: data.enableSubmodules,
})
.then(async () => {
toast.success("Service Provided Saved");
@@ -465,21 +460,6 @@ export const SaveGithubProviderCompose = ({ composeId }: Props) => {
</FormItem>
)}
/>
<FormField
control={form.control}
name="enableSubmodules"
render={({ field }) => (
<FormItem className="flex items-center space-x-2">
<FormControl>
<Switch
checked={field.value}
onCheckedChange={field.onChange}
/>
</FormControl>
<FormLabel className="!mt-0">Enable Submodules</FormLabel>
</FormItem>
)}
/>
</div>
<div className="flex w-full justify-end">
<Button

View File

@@ -31,7 +31,6 @@ import {
SelectTrigger,
SelectValue,
} from "@/components/ui/select";
import { Switch } from "@/components/ui/switch";
import {
Tooltip,
TooltipContent,
@@ -61,7 +60,6 @@ const GitlabProviderSchema = z.object({
branch: z.string().min(1, "Branch is required"),
gitlabId: z.string().min(1, "Gitlab Provider is required"),
watchPaths: z.array(z.string()).optional(),
enableSubmodules: z.boolean().default(false),
});
type GitlabProvider = z.infer<typeof GitlabProviderSchema>;
@@ -89,7 +87,6 @@ export const SaveGitlabProviderCompose = ({ composeId }: Props) => {
gitlabId: "",
branch: "",
watchPaths: [],
enableSubmodules: false,
},
resolver: zodResolver(GitlabProviderSchema),
});
@@ -139,7 +136,6 @@ export const SaveGitlabProviderCompose = ({ composeId }: Props) => {
composePath: data.composePath,
gitlabId: data.gitlabId || "",
watchPaths: data.watchPaths || [],
enableSubmodules: data.enableSubmodules ?? false,
});
}
}, [form.reset, data, form]);
@@ -157,7 +153,6 @@ export const SaveGitlabProviderCompose = ({ composeId }: Props) => {
sourceType: "gitlab",
composeStatus: "idle",
watchPaths: data.watchPaths,
enableSubmodules: data.enableSubmodules,
})
.then(async () => {
toast.success("Service Provided Saved");
@@ -490,21 +485,6 @@ export const SaveGitlabProviderCompose = ({ composeId }: Props) => {
</FormItem>
)}
/>
<FormField
control={form.control}
name="enableSubmodules"
render={({ field }) => (
<FormItem className="flex items-center space-x-2">
<FormControl>
<Switch
checked={field.value}
onCheckedChange={field.onChange}
/>
</FormControl>
<FormLabel className="!mt-0">Enable Submodules</FormLabel>
</FormItem>
)}
/>
</div>
<div className="flex w-full justify-end">
<Button

View File

@@ -77,14 +77,6 @@ const RestoreBackupSchema = z.object({
type RestoreBackup = z.infer<typeof RestoreBackupSchema>;
const formatBytes = (bytes: number): string => {
if (bytes === 0) return "0 Bytes";
const k = 1024;
const sizes = ["Bytes", "KB", "MB", "GB", "TB"];
const i = Math.floor(Math.log(bytes) / Math.log(k));
return `${Number.parseFloat((bytes / k ** i).toFixed(2))} ${sizes[i]}`;
};
export const RestoreBackup = ({
databaseId,
databaseType,
@@ -109,7 +101,7 @@ export const RestoreBackup = ({
const debouncedSetSearch = debounce((value: string) => {
setDebouncedSearchTerm(value);
}, 350);
}, 150);
const handleSearchChange = (value: string) => {
setSearch(value);
@@ -279,7 +271,7 @@ export const RestoreBackup = ({
</Badge>
)}
</FormLabel>
<Popover modal>
<Popover>
<PopoverTrigger asChild>
<FormControl>
<Button
@@ -316,51 +308,28 @@ export const RestoreBackup = ({
</div>
) : (
<ScrollArea className="h-64">
<CommandGroup className="w-96">
{files?.map((file) => (
<CommandGroup>
{files.map((file) => (
<CommandItem
value={file.Path}
key={file.Path}
value={file}
key={file}
onSelect={() => {
form.setValue("backupFile", file.Path);
if (file.IsDir) {
setSearch(`${file.Path}/`);
setDebouncedSearchTerm(`${file.Path}/`);
} else {
setSearch(file.Path);
setDebouncedSearchTerm(file.Path);
}
form.setValue("backupFile", file);
setSearch(file);
setDebouncedSearchTerm(file);
}}
>
<div className="flex w-full flex-col gap-1">
<div className="flex w-full justify-between">
<span className="font-medium">
{file.Path}
</span>
<CheckIcon
className={cn(
"ml-auto h-4 w-4",
file.Path === field.value
? "opacity-100"
: "opacity-0",
)}
/>
</div>
<div className="flex items-center gap-4 text-xs text-muted-foreground">
<span>
Size: {formatBytes(file.Size)}
</span>
{file.IsDir && (
<span className="text-blue-500">
Directory
</span>
)}
{file.Hashes?.MD5 && (
<span>MD5: {file.Hashes.MD5}</span>
)}
</div>
<div className="flex w-full justify-between">
<span>{file}</span>
</div>
<CheckIcon
className={cn(
"ml-auto h-4 w-4",
file === field.value
? "opacity-100"
: "opacity-0",
)}
/>
</CommandItem>
))}
</CommandGroup>

View File

@@ -55,7 +55,7 @@ export const AiForm = () => {
key={config.aiId}
className="flex items-center justify-between bg-sidebar p-1 w-full rounded-lg"
>
<div className="flex items-center justify-between p-3.5 rounded-lg bg-background border w-full">
<div className="flex items-center justify-between p-3.5 rounded-lg bg-background border w-full">
<div>
<span className="text-sm font-medium">
{config.name}

View File

@@ -70,7 +70,7 @@ export const ShowCertificates = () => {
key={certificate.certificateId}
className="flex items-center justify-between bg-sidebar p-1 w-full rounded-lg"
>
<div className="flex items-center justify-between p-3.5 rounded-lg bg-background border w-full">
<div className="flex items-center justify-between p-3.5 rounded-lg bg-background border w-full">
<div className="flex items-center justify-between">
<div className="flex gap-2 flex-col">
<span className="text-sm font-medium">

View File

@@ -13,65 +13,53 @@ export const extractExpirationDate = (certData: string): Date | null => {
bytes[i] = binaryStr.charCodeAt(i);
}
// ASN.1 tag for UTCTime is 0x17, GeneralizedTime is 0x18
// We need to find the second occurrence of either tag as it's the "not after" (expiration) date
let dateFound = false;
let dateFound = 0;
for (let i = 0; i < bytes.length - 2; i++) {
// Look for sequence containing validity period (0x30)
if (bytes[i] === 0x30) {
// Check next bytes for UTCTime or GeneralizedTime
let j = i + 1;
while (j < bytes.length - 2) {
if (bytes[j] === 0x17 || bytes[j] === 0x18) {
const dateType = bytes[j];
const dateLength = bytes[j + 1];
if (typeof dateLength === "undefined") break;
if (bytes[i] === 0x17 || bytes[i] === 0x18) {
const dateType = bytes[i];
const dateLength = bytes[i + 1];
if (typeof dateLength === "undefined") continue;
if (!dateFound) {
// Skip "not before" date
dateFound = true;
j += dateLength + 2;
continue;
}
// Found "not after" date
let dateStr = "";
for (let k = 0; k < dateLength; k++) {
const charCode = bytes[j + 2 + k];
if (typeof charCode === "undefined") continue;
dateStr += String.fromCharCode(charCode);
}
if (dateType === 0x17) {
// UTCTime (YYMMDDhhmmssZ)
const year = Number.parseInt(dateStr.slice(0, 2));
const fullYear = year >= 50 ? 1900 + year : 2000 + year;
return new Date(
Date.UTC(
fullYear,
Number.parseInt(dateStr.slice(2, 4)) - 1,
Number.parseInt(dateStr.slice(4, 6)),
Number.parseInt(dateStr.slice(6, 8)),
Number.parseInt(dateStr.slice(8, 10)),
Number.parseInt(dateStr.slice(10, 12)),
),
);
}
// GeneralizedTime (YYYYMMDDhhmmssZ)
return new Date(
Date.UTC(
Number.parseInt(dateStr.slice(0, 4)),
Number.parseInt(dateStr.slice(4, 6)) - 1,
Number.parseInt(dateStr.slice(6, 8)),
Number.parseInt(dateStr.slice(8, 10)),
Number.parseInt(dateStr.slice(10, 12)),
Number.parseInt(dateStr.slice(12, 14)),
),
);
}
j++;
if (dateFound === 0) {
dateFound++;
i += dateLength + 1;
continue;
}
let dateStr = "";
for (let j = 0; j < dateLength; j++) {
const charCode = bytes[i + 2 + j];
if (typeof charCode === "undefined") continue;
dateStr += String.fromCharCode(charCode);
}
if (dateType === 0x17) {
// UTCTime (YYMMDDhhmmssZ)
const year = Number.parseInt(dateStr.slice(0, 2));
const fullYear = year >= 50 ? 1900 + year : 2000 + year;
return new Date(
Date.UTC(
fullYear,
Number.parseInt(dateStr.slice(2, 4)) - 1,
Number.parseInt(dateStr.slice(4, 6)),
Number.parseInt(dateStr.slice(6, 8)),
Number.parseInt(dateStr.slice(8, 10)),
Number.parseInt(dateStr.slice(10, 12)),
),
);
}
// GeneralizedTime (YYYYMMDDhhmmssZ)
return new Date(
Date.UTC(
Number.parseInt(dateStr.slice(0, 4)),
Number.parseInt(dateStr.slice(4, 6)) - 1,
Number.parseInt(dateStr.slice(6, 8)),
Number.parseInt(dateStr.slice(8, 10)),
Number.parseInt(dateStr.slice(10, 12)),
Number.parseInt(dateStr.slice(12, 14)),
),
);
}
}
return null;

View File

@@ -54,7 +54,7 @@ export const ShowRegistry = () => {
key={registry.registryId}
className="flex items-center justify-between bg-sidebar p-1 w-full rounded-lg"
>
<div className="flex items-center justify-between p-3.5 rounded-lg bg-background border w-full">
<div className="flex items-center justify-between p-3.5 rounded-lg bg-background border w-full">
<div className="flex items-center justify-between">
<div className="flex gap-2 flex-col">
<span className="text-sm font-medium">

View File

@@ -55,7 +55,7 @@ export const ShowDestinations = () => {
key={destination.destinationId}
className="flex items-center justify-between bg-sidebar p-1 w-full rounded-lg"
>
<div className="flex items-center justify-between p-3.5 rounded-lg bg-background border w-full">
<div className="flex items-center justify-between p-3.5 rounded-lg bg-background border w-full">
<div className="flex flex-col gap-1">
<span className="text-sm">
{index + 1}. {destination.name}

View File

@@ -61,7 +61,7 @@ export const ShowNotifications = () => {
key={notification.notificationId}
className="flex items-center justify-between bg-sidebar p-1 w-full rounded-lg"
>
<div className="flex items-center justify-between p-3.5 rounded-lg bg-background border w-full">
<div className="flex items-center justify-between p-3.5 rounded-lg bg-background border w-full">
<span className="text-sm flex flex-row items-center gap-4">
{notification.notificationType === "slack" && (
<div className="flex items-center justify-center rounded-lg">

View File

@@ -22,9 +22,6 @@ export const ShowDokployActions = () => {
const { mutateAsync: reloadServer, isLoading } =
api.settings.reloadServer.useMutation();
const { mutateAsync: cleanRedis } = api.settings.cleanRedis.useMutation();
const { mutateAsync: reloadRedis } = api.settings.reloadRedis.useMutation();
return (
<DropdownMenu>
<DropdownMenuTrigger asChild disabled={isLoading}>
@@ -72,36 +69,6 @@ export const ShowDokployActions = () => {
{t("settings.server.webServer.updateServerIp")}
</DropdownMenuItem>
</UpdateServerIp>
<DropdownMenuItem
className="cursor-pointer"
onClick={async () => {
await cleanRedis()
.then(async () => {
toast.success("Redis cleaned");
})
.catch(() => {
toast.error("Error cleaning Redis");
});
}}
>
Clean Redis
</DropdownMenuItem>
<DropdownMenuItem
className="cursor-pointer"
onClick={async () => {
await reloadRedis()
.then(async () => {
toast.success("Redis reloaded");
})
.catch(() => {
toast.error("Error reloading Redis");
});
}}
>
Reload Redis
</DropdownMenuItem>
</DropdownMenuGroup>
</DropdownMenuContent>
</DropdownMenu>

View File

@@ -56,7 +56,7 @@ export const ShowDestinations = () => {
key={sshKey.sshKeyId}
className="flex items-center justify-between bg-sidebar p-1 w-full rounded-lg"
>
<div className="flex items-center justify-between p-3.5 rounded-lg bg-background border w-full">
<div className="flex items-center justify-between p-3.5 rounded-lg bg-background border w-full">
<div className="flex items-center justify-between">
<div className="flex flex-col">
<span className="text-sm font-medium">

View File

@@ -133,6 +133,17 @@ export const UserNav = () => {
Servers
</DropdownMenuItem>
)}
{data?.role === "owner" && (
<DropdownMenuItem
className="cursor-pointer"
onClick={() => {
router.push("/dashboard/settings");
}}
>
Settings
</DropdownMenuItem>
)}
</>
)}
</DropdownMenuGroup>

View File

@@ -26,20 +26,15 @@ const dockerComposeServices = [
{ label: "secrets", type: "keyword", info: "Define secrets" },
].map((opt) => ({
...opt,
apply: (
view: EditorView,
completion: Completion,
from: number,
to: number,
) => {
apply: (view: EditorView, completion: Completion) => {
const insert = `${completion.label}:`;
view.dispatch({
changes: {
from,
to,
from: view.state.selection.main.from,
to: view.state.selection.main.to,
insert,
},
selection: { anchor: from + insert.length },
selection: { anchor: view.state.selection.main.from + insert.length },
});
},
}));
@@ -79,20 +74,15 @@ const dockerComposeServiceOptions = [
{ label: "networks", type: "keyword", info: "Networks to join" },
].map((opt) => ({
...opt,
apply: (
view: EditorView,
completion: Completion,
from: number,
to: number,
) => {
apply: (view: EditorView, completion: Completion) => {
const insert = `${completion.label}: `;
view.dispatch({
changes: {
from,
to,
from: view.state.selection.main.from,
to: view.state.selection.main.to,
insert,
},
selection: { anchor: from + insert.length },
selection: { anchor: view.state.selection.main.from + insert.length },
});
},
}));
@@ -109,7 +99,6 @@ function dockerComposeComplete(
const line = context.state.doc.lineAt(context.pos);
const indentation = /^\s*/.exec(line.text)?.[0].length || 0;
// If we're at the root level
if (indentation === 0) {
return {
from: word.from,

View File

@@ -1,2 +0,0 @@
ALTER TABLE "application" ADD COLUMN "enableSubmodules" boolean DEFAULT false;--> statement-breakpoint
ALTER TABLE "compose" ADD COLUMN "enableSubmodules" boolean DEFAULT false;

View File

@@ -1,2 +0,0 @@
ALTER TABLE "application" ALTER COLUMN "enableSubmodules" SET NOT NULL;--> statement-breakpoint
ALTER TABLE "compose" ALTER COLUMN "enableSubmodules" SET NOT NULL;

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@@ -596,20 +596,6 @@
"when": 1743923992280,
"tag": "0084_thin_iron_lad",
"breakpoints": true
},
{
"idx": 85,
"version": "7",
"when": 1745705609181,
"tag": "0085_equal_captain_stacy",
"breakpoints": true
},
{
"idx": 86,
"version": "7",
"when": 1745706676004,
"tag": "0086_rainy_gertrude_yorkes",
"breakpoints": true
}
]
}

View File

@@ -1,6 +1,6 @@
{
"name": "dokploy",
"version": "v0.21.8",
"version": "v0.21.7",
"private": true,
"license": "Apache-2.0",
"type": "module",

View File

@@ -215,7 +215,7 @@ const Service = (
router.push(newPath);
}}
>
<div className="flex flex-row items-center justify-between w-full gap-4 overflow-x-scroll">
<div className="flex flex-row items-center justify-between w-full gap-4">
<TabsList
className={cn(
"flex gap-8 justify-start max-xl:overflow-x-scroll overflow-y-hidden",

View File

@@ -212,15 +212,15 @@ const Service = (
router.push(newPath);
}}
>
<div className="flex flex-row items-center justify-between w-full gap-4 overflow-x-scroll">
<div className="flex flex-row items-center justify-between w-full gap-4">
<TabsList
className={cn(
"lg:grid lg:w-fit max-md:overflow-y-scroll justify-start",
"md:grid md:w-fit max-md:overflow-y-scroll justify-start",
isCloud && data?.serverId
? "lg:grid-cols-7"
? "md:grid-cols-7"
: data?.serverId
? "lg:grid-cols-6"
: "lg:grid-cols-7",
? "md:grid-cols-6"
: "md:grid-cols-7",
)}
>
<TabsTrigger value="general">General</TabsTrigger>

View File

@@ -182,7 +182,7 @@ const Mariadb = (
router.push(newPath, undefined, { shallow: true });
}}
>
<div className="flex flex-row items-center justify-between w-full gap-4 overflow-x-scroll">
<div className="flex flex-row items-center justify-between w-full gap-4">
<TabsList
className={cn(
"md:grid md:w-fit max-md:overflow-y-scroll justify-start",

View File

@@ -183,7 +183,7 @@ const Mongo = (
router.push(newPath, undefined, { shallow: true });
}}
>
<div className="flex flex-row items-center justify-between w-full gap-4 overflow-x-scroll">
<div className="flex flex-row items-center justify-between w-full gap-4">
<TabsList
className={cn(
"md:grid md:w-fit max-md:overflow-y-scroll justify-start",

View File

@@ -183,7 +183,7 @@ const MySql = (
router.push(newPath, undefined, { shallow: true });
}}
>
<div className="flex flex-row items-center justify-between w-full gap-4 overflow-x-scroll">
<div className="flex flex-row items-center justify-between w-full gap-4">
<TabsList
className={cn(
"md:grid md:w-fit max-md:overflow-y-scroll justify-start ",

View File

@@ -182,7 +182,7 @@ const Postgresql = (
router.push(newPath, undefined, { shallow: true });
}}
>
<div className="flex flex-row items-center justify-between w-full gap-4 overflow-x-scroll">
<div className="flex flex-row items-center justify-between w-full gap-4">
<TabsList
className={cn(
"md:grid md:w-fit max-md:overflow-y-scroll justify-start",

View File

@@ -182,7 +182,7 @@ const Redis = (
router.push(newPath, undefined, { shallow: true });
}}
>
<div className="flex flex-row items-center justify-between w-full gap-4 overflow-x-scroll">
<div className="flex flex-row items-center justify-between w-full gap-4">
<TabsList
className={cn(
"md:grid md:w-fit max-md:overflow-y-scroll justify-start",

View File

@@ -355,7 +355,6 @@ export const applicationRouter = createTRPCRouter({
applicationStatus: "idle",
githubId: input.githubId,
watchPaths: input.watchPaths,
enableSubmodules: input.enableSubmodules,
});
return true;
@@ -383,7 +382,6 @@ export const applicationRouter = createTRPCRouter({
gitlabProjectId: input.gitlabProjectId,
gitlabPathNamespace: input.gitlabPathNamespace,
watchPaths: input.watchPaths,
enableSubmodules: input.enableSubmodules,
});
return true;
@@ -409,7 +407,6 @@ export const applicationRouter = createTRPCRouter({
applicationStatus: "idle",
bitbucketId: input.bitbucketId,
watchPaths: input.watchPaths,
enableSubmodules: input.enableSubmodules,
});
return true;
@@ -435,7 +432,6 @@ export const applicationRouter = createTRPCRouter({
applicationStatus: "idle",
giteaId: input.giteaId,
watchPaths: input.watchPaths,
enableSubmodules: input.enableSubmodules,
});
return true;
@@ -483,7 +479,6 @@ export const applicationRouter = createTRPCRouter({
sourceType: "git",
applicationStatus: "idle",
watchPaths: input.watchPaths,
enableSubmodules: input.enableSubmodules,
});
return true;

View File

@@ -50,18 +50,6 @@ import { TRPCError } from "@trpc/server";
import { observable } from "@trpc/server/observable";
import { z } from "zod";
interface RcloneFile {
Path: string;
Name: string;
Size: number;
IsDir: boolean;
Tier?: string;
Hashes?: {
MD5?: string;
SHA1?: string;
};
}
export const backupRouter = createTRPCRouter({
create: protectedProcedure
.input(apiCreateBackup)
@@ -280,7 +268,7 @@ export const backupRouter = createTRPCRouter({
: input.search;
const searchPath = baseDir ? `${bucketPath}/${baseDir}` : bucketPath;
const listCommand = `rclone lsjson ${rcloneFlags.join(" ")} "${searchPath}" --no-mimetype --no-modtime 2>/dev/null`;
const listCommand = `rclone lsf ${rcloneFlags.join(" ")} "${searchPath}" | head -n 100`;
let stdout = "";
@@ -292,35 +280,20 @@ export const backupRouter = createTRPCRouter({
stdout = result.stdout;
}
let files: RcloneFile[] = [];
try {
files = JSON.parse(stdout) as RcloneFile[];
} catch (error) {
console.error("Error parsing JSON response:", error);
console.error("Raw stdout:", stdout);
throw new Error("Failed to parse backup files list");
}
// Limit to first 100 files
const files = stdout.split("\n").filter(Boolean);
const results = baseDir
? files.map((file) => ({
...file,
Path: `${baseDir}${file.Path}`,
}))
? files.map((file) => `${baseDir}${file}`)
: files;
if (searchTerm) {
return results
.filter((file) =>
file.Path.toLowerCase().includes(searchTerm.toLowerCase()),
)
.slice(0, 100);
return results.filter((file) =>
file.toLowerCase().includes(searchTerm.toLowerCase()),
);
}
return results.slice(0, 100);
return results;
} catch (error) {
console.error("Error in listBackupFiles:", error);
throw new TRPCError({
code: "BAD_REQUEST",
message:

View File

@@ -31,6 +31,7 @@ export const mountRouter = createTRPCRouter({
update: protectedProcedure
.input(apiUpdateMount)
.mutation(async ({ input }) => {
return await updateMount(input.mountId, input);
await updateMount(input.mountId, input);
return true;
}),
});

View File

@@ -79,33 +79,6 @@ export const settingsRouter = createTRPCRouter({
await execAsync(`docker service update --force ${stdout.trim()}`);
return true;
}),
cleanRedis: adminProcedure.mutation(async () => {
if (IS_CLOUD) {
return true;
}
const { stdout: containerId } = await execAsync(
`docker ps --filter "name=dokploy-redis" --filter "status=running" -q | head -n 1`,
);
if (!containerId) {
throw new Error("Redis container not found");
}
const redisContainerId = containerId.trim();
await execAsync(`docker exec -i ${redisContainerId} redis-cli flushall`);
return true;
}),
reloadRedis: adminProcedure.mutation(async () => {
if (IS_CLOUD) {
return true;
}
await execAsync("docker service scale dokploy-redis=0");
await execAsync("docker service scale dokploy-redis=1");
return true;
}),
reloadTraefik: adminProcedure
.input(apiServerSchema)
.mutation(async ({ input }) => {

View File

@@ -21,7 +21,6 @@ import { setupTerminalWebSocketServer } from "./wss/terminal";
config({ path: ".env" });
const PORT = Number.parseInt(process.env.PORT || "3000", 10);
const HOST = process.env.HOST || "0.0.0.0";
const dev = process.env.NODE_ENV !== "production";
const app = next({ dev, turbopack: process.env.TURBOPACK === "1" });
const handle = app.getRequestHandler();
@@ -56,8 +55,8 @@ void app.prepare().then(async () => {
await migration();
}
server.listen(PORT, HOST);
console.log(`Server Started on: http://${HOST}:${PORT}`);
server.listen(PORT);
console.log("Server Started:", PORT);
if (!IS_CLOUD) {
console.log("Starting Deployment Worker");
const { deploymentWorker } = await import("./queues/deployments-queue");

View File

@@ -12,10 +12,14 @@ export const paths = (isServer = false) => {
const MAIN_TRAEFIK_PATH = `${BASE_PATH}/traefik`;
const DYNAMIC_TRAEFIK_PATH = `${MAIN_TRAEFIK_PATH}/dynamic`;
const CADDY_PATH = `${BASE_PATH}/caddy`;
const CADDY_DYNAMIC_PATH = `${CADDY_PATH}/dynamic`;
return {
BASE_PATH,
MAIN_TRAEFIK_PATH,
DYNAMIC_TRAEFIK_PATH,
CADDY_PATH,
CADDY_DYNAMIC_PATH,
LOGS_PATH: `${BASE_PATH}/logs`,
APPLICATIONS_PATH: `${BASE_PATH}/applications`,
COMPOSE_PATH: `${BASE_PATH}/compose`,

View File

@@ -182,7 +182,6 @@ export const applications = pgTable("application", {
onDelete: "set null",
},
),
enableSubmodules: boolean("enableSubmodules").notNull().default(false),
dockerfile: text("dockerfile"),
dockerContextPath: text("dockerContextPath"),
dockerBuildStage: text("dockerBuildStage"),
@@ -471,7 +470,6 @@ export const apiSaveGithubProvider = createSchema
buildPath: true,
githubId: true,
watchPaths: true,
enableSubmodules: true,
})
.required();
@@ -486,7 +484,6 @@ export const apiSaveGitlabProvider = createSchema
gitlabProjectId: true,
gitlabPathNamespace: true,
watchPaths: true,
enableSubmodules: true,
})
.required();
@@ -499,7 +496,6 @@ export const apiSaveBitbucketProvider = createSchema
bitbucketId: true,
applicationId: true,
watchPaths: true,
enableSubmodules: true,
})
.required();
@@ -512,7 +508,6 @@ export const apiSaveGiteaProvider = createSchema
giteaRepository: true,
giteaId: true,
watchPaths: true,
enableSubmodules: true,
})
.required();
@@ -533,7 +528,6 @@ export const apiSaveGitProvider = createSchema
customGitBuildPath: true,
customGitUrl: true,
watchPaths: true,
enableSubmodules: true,
})
.required()
.merge(

View File

@@ -72,7 +72,6 @@ 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),

View File

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

View File

@@ -356,7 +356,6 @@ 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,8 +144,7 @@ export const updateMount = async (
await deleteFileMount(mountId);
await createFileMount(mountId);
}
return await findMountById(mountId);
return mount;
});
};

View File

@@ -0,0 +1,350 @@
import { chmodSync, existsSync, mkdirSync, writeFileSync } from "node:fs";
import path from "node:path";
import type { ContainerCreateOptions } from "dockerode";
import { dump } from "js-yaml";
import { paths } from "../constants";
import { getRemoteDocker } from "../utils/servers/remote-docker";
import type { FileConfig } from "../utils/traefik/file-types";
import type { MainTraefikConfig } from "../utils/traefik/types";
export const TRAEFIK_SSL_PORT =
Number.parseInt(process.env.TRAEFIK_SSL_PORT!, 10) || 443;
export const TRAEFIK_PORT =
Number.parseInt(process.env.TRAEFIK_PORT!, 10) || 80;
export const TRAEFIK_HTTP3_PORT =
Number.parseInt(process.env.TRAEFIK_HTTP3_PORT!, 10) || 443;
export const TRAEFIK_VERSION = process.env.TRAEFIK_VERSION || "3.1.2";
interface TraefikOptions {
enableDashboard?: boolean;
env?: string[];
serverId?: string;
additionalPorts?: {
targetPort: number;
publishedPort: number;
}[];
force?: boolean;
}
export const initializeCaddy = async ({
enableDashboard = false,
env,
serverId,
additionalPorts = [],
force = false,
}: TraefikOptions = {}) => {
const { CADDY_PATH, CADDY_DYNAMIC_PATH } = paths(!!serverId);
const imageName = "lucaslorentz/caddy-docker-proxy:ci-alpine";
const containerName = "dokploy-caddy";
const exposedPorts: Record<string, {}> = {
[`${TRAEFIK_PORT}/tcp`]: {},
[`${TRAEFIK_SSL_PORT}/tcp`]: {},
[`${TRAEFIK_HTTP3_PORT}/udp`]: {},
};
const portBindings: Record<string, Array<{ HostPort: string }>> = {
[`${TRAEFIK_PORT}/tcp`]: [{ HostPort: TRAEFIK_PORT.toString() }],
[`${TRAEFIK_SSL_PORT}/tcp`]: [{ HostPort: TRAEFIK_SSL_PORT.toString() }],
[`${TRAEFIK_HTTP3_PORT}/udp`]: [
{ HostPort: TRAEFIK_HTTP3_PORT.toString() },
],
};
if (enableDashboard) {
exposedPorts["8080/tcp"] = {};
portBindings["8080/tcp"] = [{ HostPort: "8080" }];
}
for (const port of additionalPorts) {
const portKey = `${port.targetPort}/tcp`;
exposedPorts[portKey] = {};
portBindings[portKey] = [{ HostPort: port.publishedPort.toString() }];
}
const settings: ContainerCreateOptions = {
name: containerName,
Image: imageName,
NetworkingConfig: {
EndpointsConfig: {
"dokploy-network": {},
},
},
ExposedPorts: exposedPorts,
HostConfig: {
RestartPolicy: {
Name: "always",
},
Binds: [
`${CADDY_PATH}/Caddyfile:/etc/caddy/Caddyfile`,
`${CADDY_DYNAMIC_PATH}:/etc/caddy/dynamic`,
"/var/run/docker.sock:/var/run/docker.sock",
],
PortBindings: portBindings,
},
Env: env,
};
const docker = await getRemoteDocker(serverId);
try {
try {
const service = docker.getService("dokploy-caddy");
await service?.remove({ force: true });
let attempts = 0;
const maxAttempts = 5;
while (attempts < maxAttempts) {
try {
await docker.listServices({
filters: { name: ["dokploy-caddy"] },
});
console.log("Waiting for service cleanup...");
await new Promise((resolve) => setTimeout(resolve, 5000));
attempts++;
} catch (_e) {
break;
}
}
} catch (_err) {
console.log("No existing service to remove");
}
// Then try to remove any existing container
const container = docker.getContainer(containerName);
try {
const inspect = await container.inspect();
if (inspect.State.Status === "running" && !force) {
console.log("Caddy already running");
return;
}
await container.remove({ force: true });
await new Promise((resolve) => setTimeout(resolve, 5000));
} catch (_err) {
console.log("No existing container to remove");
}
// Create and start the new container
try {
await docker.createContainer(settings);
const newContainer = docker.getContainer(containerName);
await newContainer.start();
console.log("Caddy container started successfully");
} catch (error: any) {
if (error?.json?.message?.includes("port is already allocated")) {
console.log("Ports still in use, waiting longer for cleanup...");
await new Promise((resolve) => setTimeout(resolve, 10000));
// Try one more time
await docker.createContainer(settings);
const newContainer = docker.getContainer(containerName);
await newContainer.start();
console.log("Caddy container started successfully after retry");
}
}
} catch (error) {
console.error("Failed to initialize Caddy:", error);
throw error;
}
};
export const createDefaultServerCaddyConfig = () => {
const { CADDY_DYNAMIC_PATH } = paths();
const configFilePath = path.join(CADDY_DYNAMIC_PATH, "Caddyfile");
if (existsSync(configFilePath)) {
console.log("Default caddy config already exists");
return;
}
const appName = "dokploy";
const serviceURLDefault = `http://${appName}:${process.env.PORT || 3000}`;
const config: FileConfig = {
http: {
routers: {
[`${appName}-router-app`]: {
rule: `Host(\`${appName}.docker.localhost\`) && PathPrefix(\`/\`)`,
service: `${appName}-service-app`,
entryPoints: ["web"],
},
},
services: {
[`${appName}-service-app`]: {
loadBalancer: {
servers: [{ url: serviceURLDefault }],
passHostHeader: true,
},
},
},
},
};
const yamlStr = dump(config);
mkdirSync(CADDY_DYNAMIC_PATH, { recursive: true });
writeFileSync(path.join(CADDY_DYNAMIC_PATH, `Caddyfile`), yamlStr, "utf8");
};
export const getDefaultCaddyConfig = () => {
const configObject: MainTraefikConfig = {
providers: {
...(process.env.NODE_ENV === "development"
? {
docker: {
defaultRule:
"Host(`{{ trimPrefix `/` .Name }}.docker.localhost`)",
},
}
: {
swarm: {
exposedByDefault: false,
watch: true,
},
docker: {
exposedByDefault: false,
watch: true,
network: "dokploy-network",
},
}),
file: {
directory: "/etc/dokploy/traefik/dynamic",
watch: true,
},
},
entryPoints: {
web: {
address: `:${TRAEFIK_PORT}`,
},
websecure: {
address: `:${TRAEFIK_SSL_PORT}`,
http3: {
advertisedPort: TRAEFIK_HTTP3_PORT,
},
...(process.env.NODE_ENV === "production" && {
http: {
tls: {
certResolver: "letsencrypt",
},
},
}),
},
},
api: {
insecure: true,
},
...(process.env.NODE_ENV === "production" && {
certificatesResolvers: {
letsencrypt: {
acme: {
email: "test@localhost.com",
storage: "/etc/dokploy/traefik/dynamic/acme.json",
httpChallenge: {
entryPoint: "web",
},
},
},
},
}),
};
const yamlStr = dump(configObject);
return yamlStr;
};
export const getDefaultServerTraefikConfig = () => {
const configObject: MainTraefikConfig = {
providers: {
swarm: {
exposedByDefault: false,
watch: true,
},
docker: {
exposedByDefault: false,
watch: true,
network: "dokploy-network",
},
file: {
directory: "/etc/dokploy/traefik/dynamic",
watch: true,
},
},
entryPoints: {
web: {
address: `:${TRAEFIK_PORT}`,
},
websecure: {
address: `:${TRAEFIK_SSL_PORT}`,
http3: {
advertisedPort: TRAEFIK_HTTP3_PORT,
},
http: {
tls: {
certResolver: "letsencrypt",
},
},
},
},
api: {
insecure: true,
},
certificatesResolvers: {
letsencrypt: {
acme: {
email: "test@localhost.com",
storage: "/etc/dokploy/traefik/dynamic/acme.json",
httpChallenge: {
entryPoint: "web",
},
},
},
},
};
const yamlStr = dump(configObject);
return yamlStr;
};
export const createDefaultCaddyConfig = () => {
const { CADDY_PATH, CADDY_DYNAMIC_PATH } = paths();
const mainConfig = path.join(CADDY_PATH, "Caddyfile");
const acmeJsonPath = path.join(CADDY_DYNAMIC_PATH, "acme.json");
if (existsSync(acmeJsonPath)) {
chmodSync(acmeJsonPath, "600");
}
if (existsSync(mainConfig)) {
console.log("Main config already exists");
return;
}
const yamlStr = getDefaultCaddyConfig();
mkdirSync(CADDY_PATH, { recursive: true });
writeFileSync(mainConfig, yamlStr, "utf8");
};
export const getDefaultMiddlewares = () => {
const defaultMiddlewares = {
http: {
middlewares: {
"redirect-to-https": {
redirectScheme: {
scheme: "https",
permanent: true,
},
},
},
},
};
const yamlStr = dump(defaultMiddlewares);
return yamlStr;
};
export const createDefaultMiddlewares = () => {
const { DYNAMIC_TRAEFIK_PATH } = paths();
const middlewaresPath = path.join(DYNAMIC_TRAEFIK_PATH, "middlewares.yml");
if (existsSync(middlewaresPath)) {
console.log("Default middlewares already exists");
return;
}
const yamlStr = getDefaultMiddlewares();
mkdirSync(DYNAMIC_TRAEFIK_PATH, { recursive: true });
writeFileSync(middlewaresPath, yamlStr, "utf8");
};

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.35.0
export NIXPACKS_VERSION=1.29.1
bash -c "$(curl -fsSL https://nixpacks.com/install.sh)"
echo "Nixpacks version $NIXPACKS_VERSION installed ✅"
fi

View File

@@ -1,4 +1,4 @@
import { randomBytes, createHmac } from "node:crypto";
import { randomBytes } from "node:crypto";
import { existsSync } from "node:fs";
import { mkdir, readFile, writeFile } from "node:fs/promises";
import { join } from "node:path";
@@ -24,12 +24,6 @@ export interface Template {
domains: DomainSchema[];
}
export interface GenerateJWTOptions {
length?: number;
secret?: string;
payload?: Record<string, unknown> | undefined;
}
export const generateRandomDomain = ({
serverIp,
projectName,
@@ -67,48 +61,8 @@ export function generateBase64(bytes = 32): string {
return randomBytes(bytes).toString("base64");
}
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}`;
export function generateJwt(length = 256): string {
return randomBytes(length).toString("hex");
}
/**

View File

@@ -65,7 +65,7 @@ export interface Template {
/**
* Process a string value and replace variables
*/
export function processValue(
function processValue(
value: string,
variables: Record<string, string>,
schema: Schema,
@@ -84,11 +84,11 @@ export 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,31 +97,14 @@ export 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" || varName === "timestampms") {
if (varName === "timestamp") {
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();
}
@@ -131,34 +114,8 @@ export function processValue(
}
if (varName.startsWith("jwt:")) {
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,
});
const length = Number.parseInt(varName.split(":")[1], 10) || 256;
return generateJwt(length);
}
if (varName === "username") {
@@ -190,7 +147,7 @@ export function processVariables(
): Record<string, string> {
const variables: Record<string, string> = {};
// First pass: Process some variables that don't depend on other variables
// First pass: Process variables that don't depend on other variables
for (const [key, value] of Object.entries(template.variables)) {
if (typeof value !== "string") continue;
@@ -204,8 +161,6 @@ 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

@@ -25,23 +25,21 @@ export const runWebServerBackup = async (backup: BackupSchedule) => {
// First get the container ID
const { stdout: containerId } = await execAsync(
`docker ps --filter "name=dokploy-postgres" --filter "status=running" -q | head -n 1`,
"docker ps --filter 'name=dokploy-postgres' -q",
);
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'`;
// Then run pg_dump with the container ID
const postgresCommand = `docker exec ${containerId.trim()} pg_dump -v -Fc -U dokploy -d dokploy > '${tempDir}/database.sql'`;
await execAsync(postgresCommand);
await execAsync(`cp -r ${BASE_PATH}/* ${tempDir}/filesystem/`);
await execAsync(
// Zip all .sql files since we created more than one
`cd ${tempDir} && zip -r ${backupFileName} *.sql filesystem/ > /dev/null 2>&1`,
`cd ${tempDir} && zip -r ${backupFileName} database.sql filesystem/ > /dev/null 2>&1`,
);
const uploadCommand = `rclone copyto ${rcloneFlags.join(" ")} "${tempDir}/${backupFileName}" "${s3Path}"`;

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -90,14 +90,8 @@ export const cloneGitlabRepository = async (
isCompose = false,
) => {
const writeStream = createWriteStream(logPath, { flags: "a" });
const {
appName,
gitlabBranch,
gitlabId,
gitlab,
gitlabPathNamespace,
enableSubmodules,
} = entity;
const { appName, gitlabBranch, gitlabId, gitlab, gitlabPathNamespace } =
entity;
if (!gitlabId) {
throw new TRPCError({
@@ -133,23 +127,25 @@ export const cloneGitlabRepository = async (
try {
writeStream.write(`\nClonning Repo ${repoclone} to ${outputPath}: ✅\n`);
const cloneArgs = [
"clone",
"--branch",
gitlabBranch!,
"--depth",
"1",
...(enableSubmodules ? ["--recurse-submodules"] : []),
cloneUrl,
outputPath,
"--progress",
];
await spawnAsync("git", cloneArgs, (data) => {
if (writeStream.writable) {
writeStream.write(data);
}
});
await spawnAsync(
"git",
[
"clone",
"--branch",
gitlabBranch!,
"--depth",
"1",
"--recurse-submodules",
cloneUrl,
outputPath,
"--progress",
],
(data) => {
if (writeStream.writable) {
writeStream.write(data);
}
},
);
writeStream.write(`\nCloned ${repoclone}: ✅\n`);
} catch (error) {
writeStream.write(`ERROR Clonning: ${error}: ❌`);
@@ -171,7 +167,6 @@ export const getGitlabCloneCommand = async (
gitlabId,
serverId,
gitlab,
enableSubmodules,
} = entity;
if (!serverId) {
@@ -227,7 +222,7 @@ export const getGitlabCloneCommand = async (
const cloneCommand = `
rm -rf ${outputPath};
mkdir -p ${outputPath};
if ! git clone --branch ${gitlabBranch} --depth 1 ${enableSubmodules ? "--recurse-submodules" : ""} --progress ${cloneUrl} ${outputPath} >> ${logPath} 2>&1; then
if ! git clone --branch ${gitlabBranch} --depth 1 --recurse-submodules --progress ${cloneUrl} ${outputPath} >> ${logPath} 2>&1; then
echo "❌ [ERROR] Fail to clone the repository ${repoclone}" >> ${logPath};
exit 1;
fi
@@ -335,13 +330,7 @@ export const getGitlabBranches = async (input: {
};
export const cloneRawGitlabRepository = async (entity: Compose) => {
const {
appName,
gitlabBranch,
gitlabId,
gitlabPathNamespace,
enableSubmodules,
} = entity;
const { appName, gitlabBranch, gitlabId, gitlabPathNamespace } = entity;
if (!gitlabId) {
throw new TRPCError({
@@ -362,32 +351,24 @@ export const cloneRawGitlabRepository = async (entity: Compose) => {
const cloneUrl = `https://oauth2:${gitlabProvider?.accessToken}@${repoclone}`;
try {
const cloneArgs = [
await spawnAsync("git", [
"clone",
"--branch",
gitlabBranch!,
"--depth",
"1",
...(enableSubmodules ? ["--recurse-submodules"] : []),
"--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,
enableSubmodules,
} = compose;
const { appName, gitlabPathNamespace, branch, gitlabId, serverId } = compose;
if (!serverId) {
throw new TRPCError({
@@ -411,7 +392,7 @@ export const cloneRawGitlabRepositoryRemote = async (compose: Compose) => {
try {
const command = `
rm -rf ${outputPath};
git clone --branch ${branch} --depth 1 ${enableSubmodules ? "--recurse-submodules" : ""} ${cloneUrl} ${outputPath}
git clone --branch ${branch} --depth 1 --recurse-submodules ${cloneUrl} ${outputPath}
`;
await execAsyncRemote(serverId, command);
} catch (error) {

View File

@@ -83,54 +83,44 @@ 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 ${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();"`,
`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();"`,
);
emit("Dropping existing database...");
await execAsync(
`docker exec ${postgresContainerId} psql -U dokploy postgres -c "DROP DATABASE IF EXISTS dokploy;"`,
`docker exec $(docker ps --filter "name=dokploy-postgres" -q) psql -U dokploy postgres -c "DROP DATABASE IF EXISTS dokploy;"`,
);
emit("Creating fresh database...");
await execAsync(
`docker exec ${postgresContainerId} psql -U dokploy postgres -c "CREATE DATABASE dokploy;"`,
`docker exec $(docker ps --filter "name=dokploy-postgres" -q) 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 ${postgresContainerId}:/tmp/database.sql`,
`docker cp ${tempDir}/database.sql $(docker ps --filter "name=dokploy-postgres" -q):/tmp/database.sql`,
);
// Verify file in container
emit("Verifying file in container...");
await execAsync(
`docker exec ${postgresContainerId} ls -l /tmp/database.sql`,
`docker exec $(docker ps --filter "name=dokploy-postgres" -q) ls -l /tmp/database.sql`,
);
// Restore from the copied file
emit("Running database restore...");
await execAsync(
`docker exec ${postgresContainerId} pg_restore -v -U dokploy -d dokploy /tmp/database.sql`,
`docker exec $(docker ps --filter "name=dokploy-postgres" -q) 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 ${postgresContainerId} rm /tmp/database.sql`,
`docker exec $(docker ps --filter "name=dokploy-postgres" -q) rm /tmp/database.sql`,
);
emit("Restore completed successfully!");