Compare commits

...

23 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
Mauricio Siu
e14f2780af Merge pull request #1656 from lorenzomigliorero/support-multiple-gitlab-groups
feat(gitlab): add support for multiple group names with a single provider
2025-04-17 02:20:29 -06:00
Mauricio Siu
33ab87f3db fix(gitlab): enhance group name matching logic to support multiple names
- Updated the group name check to allow for a comma-separated list of names, improving flexibility in group name validation.
2025-04-17 02:20:03 -06:00
Mauricio Siu
571d73a5b6 Merge pull request #1720 from unleashit/docs/contributing-edits
docs(contributing): tips added for nvm use and biome ide addon
2025-04-17 02:04:05 -06:00
Mauricio Siu
a630909612 Merge pull request #1634 from f3liiix/canary
Update language "Simplified Chinese"
2025-04-17 01:59:35 -06:00
Mauricio Siu
8eaa006f0f Merge pull request #1723 from Dokploy/1682-dokploy-backup-fails
Enhance PostgreSQL backup command in web server utility
2025-04-17 01:59:05 -06:00
Mauricio Siu
8e8bc3e71e Enhance PostgreSQL backup command in web server utility
- Added error handling to check for the existence of the PostgreSQL container before executing the backup command.
- Updated the backup command to use the retrieved container ID, ensuring the command runs correctly.
2025-04-17 01:58:25 -06:00
Mauricio Siu
f4cd617107 Merge pull request #1722 from Dokploy/1715-pin-field-auto-populated-with-issuer-name-and-cannot-be-cleared-during-2fa-setup
Refactor 2FA enablement flow in Enable2FA component
2025-04-17 00:27:04 -06:00
Mauricio Siu
48cfe66a6b Refactor 2FA enablement flow in Enable2FA component
- Simplified the password submission and verification processes.
- Introduced a new state for OTP input, allowing for direct user input.
- Updated error handling to provide clearer feedback during the verification process.
- Enhanced the user experience by resetting the OTP value when switching steps and modifying the form structure for better clarity.
2025-04-17 00:25:27 -06:00
unleashit
bdc10cacef docs(contributing): recommendations to use biome IDE addons and nvm for node version
- emphasis added since using later versions of Node cause errors in the terminal
- since the project uses biome but most IDEs have eslint/prettier addons, this tip will avoid commit/build suprises
2025-04-15 14:38:37 -07:00
Mauricio Siu
8fbad8a26e Merge pull request #1709 from Axodouble/fix-missing-nl
Fix: #1708, missing Dutch translation and sorted list by population
2025-04-14 23:26:06 -06:00
F3LIIIX
0f36bcb04e Merge branch 'Dokploy:canary' into canary 2025-04-15 05:09:46 +08:00
autofix-ci[bot]
f4054453b4 [autofix.ci] apply automated fixes 2025-04-14 06:46:16 +00:00
Axodouble
dbd36fc024 Fix for #1708, missing dutch translation and sorted list by population 2025-04-14 08:42:30 +02:00
Mauricio Siu
850d06a32c chore: bump version to v0.21.7 in package.json 2025-04-13 21:59:36 -06:00
Mauricio Siu
dfd3dc180d Merge pull request #1706 from Dokploy/1649-webserver-restore-backups-bug
feat: implement debounced search functionality in RestoreBackup compo…
2025-04-13 20:30:55 -06:00
Mauricio Siu
3d42bfc81b feat: implement debounced search functionality in RestoreBackup component
- Added a new state for debounced search term to improve search performance.
- Updated search handling to use the debounced value when querying backup files.
- Modified the search input to reflect the current search value and handle changes accordingly.
2025-04-13 20:30:31 -06:00
autofix-ci[bot]
8e97c63faa [autofix.ci] apply automated fixes 2025-04-13 03:23:00 +00:00
autofix-ci[bot]
1279fac137 [autofix.ci] apply automated fixes 2025-04-07 19:46:14 +00:00
Lorenzo Migliorero
0e1f0b42ee fix(gitlab): update group name label and enhance group name handling
- Updated the label for the group name input field to indicate it accepts a comma-separated list.
- Modified the logic for checking group name inclusion to support multiple names separated by commas.
2025-04-07 21:43:56 +02:00
Hoofei
350bed217c Delete the extract script 2025-04-06 12:37:52 +08:00
Hoofei
7ac7481343 Update Simplified Chinese 2025-04-06 12:34:47 +08:00
Hoofei
d9c34c4524 Update Simplified Chinese 2025-04-06 12:09:30 +08:00
15 changed files with 681 additions and 208 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
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
@@ -87,6 +87,8 @@ pnpm run dokploy:dev
Go to http://localhost:3000 to see the development server
Note: this project uses Biome. If your editor is configured to use another formatter such as Prettier, it's recommended to either change it to use Biome or turn it off.
## Build
```bash

View File

@@ -84,6 +84,7 @@ export const RestoreBackup = ({
}: Props) => {
const [isOpen, setIsOpen] = useState(false);
const [search, setSearch] = useState("");
const [debouncedSearchTerm, setDebouncedSearchTerm] = useState("");
const { data: destinations = [] } = api.destination.all.useQuery();
@@ -99,13 +100,18 @@ export const RestoreBackup = ({
const destionationId = form.watch("destinationId");
const debouncedSetSearch = debounce((value: string) => {
setDebouncedSearchTerm(value);
}, 150);
const handleSearchChange = (value: string) => {
setSearch(value);
}, 300);
debouncedSetSearch(value);
};
const { data: files = [], isLoading } = api.backup.listBackupFiles.useQuery(
{
destinationId: destionationId,
search,
search: debouncedSearchTerm,
serverId: serverId ?? "",
},
{
@@ -284,7 +290,8 @@ export const RestoreBackup = ({
<Command>
<CommandInput
placeholder="Search backup files..."
onValueChange={debouncedSetSearch}
value={search}
onValueChange={handleSearchChange}
className="h-9"
/>
{isLoading ? (
@@ -308,6 +315,8 @@ export const RestoreBackup = ({
key={file}
onSelect={() => {
form.setValue("backupFile", file);
setSearch(file);
setDebouncedSearchTerm(file);
}}
>
<div className="flex w-full justify-between">

View File

@@ -248,7 +248,9 @@ export const AddGitlabProvider = () => {
name="groupName"
render={({ field }) => (
<FormItem>
<FormLabel>Group Name (Optional)</FormLabel>
<FormLabel>
Group Name (Optional, Comma-Separated List)
</FormLabel>
<FormControl>
<Input
placeholder="For organization/group access use the slugish name of the group eg: my-org"

View File

@@ -156,7 +156,9 @@ export const EditGitlabProvider = ({ gitlabId }: Props) => {
name="groupName"
render={({ field }) => (
<FormItem>
<FormLabel>Group Name (Optional)</FormLabel>
<FormLabel>
Group Name (Optional, Comma-Separated List)
</FormLabel>
<FormControl>
<Input
placeholder="For organization/group access use the slugish name of the group eg: my-org"

View File

@@ -61,6 +61,79 @@ export const Enable2FA = () => {
const [isDialogOpen, setIsDialogOpen] = useState(false);
const [step, setStep] = useState<"password" | "verify">("password");
const [isPasswordLoading, setIsPasswordLoading] = useState(false);
const [otpValue, setOtpValue] = useState("");
const handleVerifySubmit = async (e: React.FormEvent) => {
e.preventDefault();
try {
const result = await authClient.twoFactor.verifyTotp({
code: otpValue,
});
if (result.error) {
if (result.error.code === "INVALID_TWO_FACTOR_AUTHENTICATION") {
toast.error("Invalid verification code");
return;
}
throw result.error;
}
if (!result.data) {
throw new Error("No response received from server");
}
toast.success("2FA configured successfully");
utils.user.get.invalidate();
setIsDialogOpen(false);
} catch (error) {
if (error instanceof Error) {
const errorMessage =
error.message === "Failed to fetch"
? "Connection error. Please check your internet connection."
: error.message;
toast.error(errorMessage);
} else {
toast.error("Error verifying 2FA code", {
description: error instanceof Error ? error.message : "Unknown error",
});
}
}
};
const passwordForm = useForm<PasswordForm>({
resolver: zodResolver(PasswordSchema),
defaultValues: {
password: "",
},
});
const pinForm = useForm<PinForm>({
resolver: zodResolver(PinSchema),
defaultValues: {
pin: "",
},
});
useEffect(() => {
if (!isDialogOpen) {
setStep("password");
setData(null);
setBackupCodes([]);
setOtpValue("");
passwordForm.reset({
password: "",
issuer: "",
});
}
}, [isDialogOpen, passwordForm]);
useEffect(() => {
if (step === "verify") {
setOtpValue("");
}
}, [step]);
const handlePasswordSubmit = async (formData: PasswordForm) => {
setIsPasswordLoading(true);
@@ -105,75 +178,6 @@ export const Enable2FA = () => {
}
};
const handleVerifySubmit = async (formData: PinForm) => {
try {
const result = await authClient.twoFactor.verifyTotp({
code: formData.pin,
});
if (result.error) {
if (result.error.code === "INVALID_TWO_FACTOR_AUTHENTICATION") {
pinForm.setError("pin", {
message: "Invalid code. Please try again.",
});
toast.error("Invalid verification code");
return;
}
throw result.error;
}
if (!result.data) {
throw new Error("No response received from server");
}
toast.success("2FA configured successfully");
utils.user.get.invalidate();
setIsDialogOpen(false);
} catch (error) {
if (error instanceof Error) {
const errorMessage =
error.message === "Failed to fetch"
? "Connection error. Please check your internet connection."
: error.message;
pinForm.setError("pin", {
message: errorMessage,
});
toast.error(errorMessage);
} else {
pinForm.setError("pin", {
message: "Error verifying code",
});
toast.error("Error verifying 2FA code");
}
}
};
const passwordForm = useForm<PasswordForm>({
resolver: zodResolver(PasswordSchema),
defaultValues: {
password: "",
},
});
const pinForm = useForm<PinForm>({
resolver: zodResolver(PinSchema),
defaultValues: {
pin: "",
},
});
useEffect(() => {
if (!isDialogOpen) {
setStep("password");
setData(null);
setBackupCodes([]);
passwordForm.reset();
pinForm.reset();
}
}, [isDialogOpen, passwordForm, pinForm]);
return (
<Dialog open={isDialogOpen} onOpenChange={setIsDialogOpen}>
<DialogTrigger asChild>
@@ -233,7 +237,8 @@ export const Enable2FA = () => {
/>
</FormControl>
<FormDescription>
Enter your password to enable 2FA
Use a custom issuer to identify the service you're
authenticating with.
</FormDescription>
<FormMessage />
</FormItem>
@@ -250,11 +255,7 @@ export const Enable2FA = () => {
</Form>
) : (
<Form {...pinForm}>
<form
id="pin-form"
onSubmit={pinForm.handleSubmit(handleVerifySubmit)}
className="space-y-6"
>
<form onSubmit={handleVerifySubmit} className="space-y-6">
<div className="flex flex-col gap-6 justify-center items-center">
{data?.qrCodeUrl ? (
<>
@@ -306,36 +307,33 @@ export const Enable2FA = () => {
)}
</div>
<FormField
control={pinForm.control}
name="pin"
render={({ field }) => (
<FormItem className="flex flex-col justify-center items-center">
<FormLabel>Verification Code</FormLabel>
<FormControl>
<InputOTP maxLength={6} {...field}>
<InputOTPGroup>
<InputOTPSlot index={0} />
<InputOTPSlot index={1} />
<InputOTPSlot index={2} />
<InputOTPSlot index={3} />
<InputOTPSlot index={4} />
<InputOTPSlot index={5} />
</InputOTPGroup>
</InputOTP>
</FormControl>
<FormDescription>
Enter the 6-digit code from your authenticator app
</FormDescription>
<FormMessage />
</FormItem>
)}
/>
<div className="flex flex-col justify-center items-center">
<FormLabel>Verification Code</FormLabel>
<InputOTP
maxLength={6}
value={otpValue}
onChange={setOtpValue}
autoComplete="off"
>
<InputOTPGroup>
<InputOTPSlot index={0} />
<InputOTPSlot index={1} />
<InputOTPSlot index={2} />
<InputOTPSlot index={3} />
<InputOTPSlot index={4} />
<InputOTPSlot index={5} />
</InputOTPGroup>
</InputOTP>
<FormDescription>
Enter the 6-digit code from your authenticator app
</FormDescription>
</div>
<Button
type="submit"
className="w-full"
isLoading={isPasswordLoading}
disabled={otpValue.length !== 6}
>
Enable 2FA
</Button>

View File

@@ -1,23 +1,27 @@
/**
* Sorted list based off of population of the country / speakers of the language.
*/
export const Languages = {
english: { code: "en", name: "English" },
spanish: { code: "es", name: "Español" },
chineseSimplified: { code: "zh-Hans", name: "简体中文" },
chineseTraditional: { code: "zh-Hant", name: "繁體中文" },
portuguese: { code: "pt-br", name: "Português" },
russian: { code: "ru", name: "Русский" },
japanese: { code: "ja", name: "日本語" },
german: { code: "de", name: "Deutsch" },
korean: { code: "ko", name: "한국어" },
french: { code: "fr", name: "Français" },
turkish: { code: "tr", name: "Türkçe" },
italian: { code: "it", name: "Italiano" },
polish: { code: "pl", name: "Polski" },
ukrainian: { code: "uk", name: "Українська" },
russian: { code: "ru", name: "Русский" },
french: { code: "fr", name: "Français" },
german: { code: "de", name: "Deutsch" },
chineseTraditional: { code: "zh-Hant", name: "繁體中文" },
chineseSimplified: { code: "zh-Hans", name: "简体中文" },
turkish: { code: "tr", name: "Türkçe" },
kazakh: { code: "kz", name: "Қазақ" },
persian: { code: "fa", name: "فارسی" },
korean: { code: "ko", name: "한국어" },
portuguese: { code: "pt-br", name: "Português" },
italian: { code: "it", name: "Italiano" },
japanese: { code: "ja", name: "日本語" },
spanish: { code: "es", name: "Español" },
dutch: { code: "nl", name: "Nederlands" },
indonesian: { code: "id", name: "Bahasa Indonesia" },
kazakh: { code: "kz", name: "Қазақ" },
norwegian: { code: "no", name: "Norsk" },
azerbaijani: { code: "az", name: "Azərbaycan" },
indonesian: { code: "id", name: "Bahasa Indonesia" },
malayalam: { code: "ml", name: "മലയാളം" },
};

View File

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

View File

@@ -1 +1 @@
{}
{}

View File

@@ -1,58 +1,58 @@
{
"settings.common.save": "Opslaan",
"settings.common.enterTerminal": "Terminal",
"settings.server.domain.title": "Server Domein",
"settings.server.domain.description": "Voeg een domein toe aan jouw server applicatie.",
"settings.server.domain.form.domain": "Domein",
"settings.server.domain.form.letsEncryptEmail": "Let's Encrypt Email",
"settings.server.domain.form.certificate.label": "Certificaat Aanbieder",
"settings.server.domain.form.certificate.placeholder": "Select een certificaat",
"settings.server.domain.form.certificateOptions.none": "Geen",
"settings.server.domain.form.certificateOptions.letsencrypt": "Let's Encrypt",
"settings.server.webServer.title": "Web Server",
"settings.server.webServer.description": "Herlaad of maak de web server schoon.",
"settings.server.webServer.actions": "Acties",
"settings.server.webServer.reload": "Herladen",
"settings.server.webServer.watchLogs": "Bekijk Logs",
"settings.server.webServer.updateServerIp": "Update de Server IP",
"settings.server.webServer.server.label": "Server",
"settings.server.webServer.traefik.label": "Traefik",
"settings.server.webServer.traefik.modifyEnv": "Bewerk Omgeving",
"settings.server.webServer.traefik.managePorts": "Extra Poort Mappings",
"settings.server.webServer.traefik.managePortsDescription": "Bewerk extra Poorten voor Traefik",
"settings.server.webServer.traefik.targetPort": "Doel Poort",
"settings.server.webServer.traefik.publishedPort": "Gepubliceerde Poort",
"settings.server.webServer.traefik.addPort": "Voeg Poort toe",
"settings.server.webServer.traefik.portsUpdated": "Poorten succesvol aangepast",
"settings.server.webServer.traefik.portsUpdateError": "Poorten niet succesvol aangepast",
"settings.server.webServer.traefik.publishMode": "Publiceer Mode",
"settings.server.webServer.storage.label": "Opslag",
"settings.server.webServer.storage.cleanUnusedImages": "Maak ongebruikte images schoon",
"settings.server.webServer.storage.cleanUnusedVolumes": "Maak ongebruikte volumes schoon",
"settings.server.webServer.storage.cleanStoppedContainers": "Maak gestopte containers schoon",
"settings.server.webServer.storage.cleanDockerBuilder": "Maak Docker Builder & Systeem schoon",
"settings.server.webServer.storage.cleanMonitoring": "Maak monitoor schoon",
"settings.server.webServer.storage.cleanAll": "Maak alles schoon",
"settings.profile.title": "Account",
"settings.profile.description": "Veramder details van account.",
"settings.profile.email": "Email",
"settings.profile.password": "Wachtwoord",
"settings.profile.avatar": "Profiel Icoon",
"settings.appearance.title": "Uiterlijk",
"settings.appearance.description": "Verander het thema van je dashboard.",
"settings.appearance.theme": "Thema",
"settings.appearance.themeDescription": "Selecteer een thema voor je dashboard.",
"settings.appearance.themes.light": "Licht",
"settings.appearance.themes.dark": "Donker",
"settings.appearance.themes.system": "Systeem",
"settings.appearance.language": "Taal",
"settings.appearance.languageDescription": "Selecteer een taal voor je dashboard.",
"settings.terminal.connectionSettings": "Verbindings instellingen",
"settings.terminal.ipAddress": "IP Address",
"settings.terminal.port": "Poort",
"settings.terminal.username": "Gebruikersnaam"
}
{
"settings.common.save": "Opslaan",
"settings.common.enterTerminal": "Terminal",
"settings.server.domain.title": "Server Domein",
"settings.server.domain.description": "Voeg een domein toe aan jouw server applicatie.",
"settings.server.domain.form.domain": "Domein",
"settings.server.domain.form.letsEncryptEmail": "Let's Encrypt Email",
"settings.server.domain.form.certificate.label": "Certificaat Aanbieder",
"settings.server.domain.form.certificate.placeholder": "Select een certificaat",
"settings.server.domain.form.certificateOptions.none": "Geen",
"settings.server.domain.form.certificateOptions.letsencrypt": "Let's Encrypt",
"settings.server.webServer.title": "Web Server",
"settings.server.webServer.description": "Herlaad of maak de web server schoon.",
"settings.server.webServer.actions": "Acties",
"settings.server.webServer.reload": "Herladen",
"settings.server.webServer.watchLogs": "Bekijk Logs",
"settings.server.webServer.updateServerIp": "Update de Server IP",
"settings.server.webServer.server.label": "Server",
"settings.server.webServer.traefik.label": "Traefik",
"settings.server.webServer.traefik.modifyEnv": "Bewerk Omgeving",
"settings.server.webServer.traefik.managePorts": "Extra Poort Mappings",
"settings.server.webServer.traefik.managePortsDescription": "Bewerk extra Poorten voor Traefik",
"settings.server.webServer.traefik.targetPort": "Doel Poort",
"settings.server.webServer.traefik.publishedPort": "Gepubliceerde Poort",
"settings.server.webServer.traefik.addPort": "Voeg Poort toe",
"settings.server.webServer.traefik.portsUpdated": "Poorten succesvol aangepast",
"settings.server.webServer.traefik.portsUpdateError": "Poorten niet succesvol aangepast",
"settings.server.webServer.traefik.publishMode": "Publiceer Mode",
"settings.server.webServer.storage.label": "Opslag",
"settings.server.webServer.storage.cleanUnusedImages": "Maak ongebruikte images schoon",
"settings.server.webServer.storage.cleanUnusedVolumes": "Maak ongebruikte volumes schoon",
"settings.server.webServer.storage.cleanStoppedContainers": "Maak gestopte containers schoon",
"settings.server.webServer.storage.cleanDockerBuilder": "Maak Docker Builder & Systeem schoon",
"settings.server.webServer.storage.cleanMonitoring": "Maak monitoor schoon",
"settings.server.webServer.storage.cleanAll": "Maak alles schoon",
"settings.profile.title": "Account",
"settings.profile.description": "Veramder details van account.",
"settings.profile.email": "Email",
"settings.profile.password": "Wachtwoord",
"settings.profile.avatar": "Profiel Icoon",
"settings.appearance.title": "Uiterlijk",
"settings.appearance.description": "Verander het thema van je dashboard.",
"settings.appearance.theme": "Thema",
"settings.appearance.themeDescription": "Selecteer een thema voor je dashboard.",
"settings.appearance.themes.light": "Licht",
"settings.appearance.themes.dark": "Donker",
"settings.appearance.themes.system": "Systeem",
"settings.appearance.language": "Taal",
"settings.appearance.languageDescription": "Selecteer een taal voor je dashboard.",
"settings.terminal.connectionSettings": "Verbindings instellingen",
"settings.terminal.ipAddress": "IP Address",
"settings.terminal.port": "Poort",
"settings.terminal.username": "Gebruikersnaam"
}

View File

@@ -1 +1,78 @@
{}
{
"dashboard.title": "仪表盘",
"dashboard.overview": "概览",
"dashboard.projects": "项目",
"dashboard.servers": "服务器",
"dashboard.docker": "Docker",
"dashboard.monitoring": "监控",
"dashboard.settings": "设置",
"dashboard.logout": "退出登录",
"dashboard.profile": "个人资料",
"dashboard.terminal": "终端",
"dashboard.containers": "容器",
"dashboard.images": "镜像",
"dashboard.volumes": "卷",
"dashboard.networks": "网络",
"button.create": "创建",
"button.edit": "编辑",
"button.delete": "删除",
"button.cancel": "取消",
"button.save": "保存",
"button.confirm": "确认",
"button.back": "返回",
"button.next": "下一步",
"button.finish": "完成",
"status.running": "运行中",
"status.stopped": "已停止",
"status.error": "错误",
"status.pending": "等待中",
"status.success": "成功",
"status.failed": "失败",
"form.required": "必填",
"form.invalid": "无效",
"form.submit": "提交",
"form.reset": "重置",
"notification.success": "操作成功",
"notification.error": "操作失败",
"notification.warning": "警告",
"notification.info": "信息",
"time.now": "刚刚",
"time.minutes": "分钟前",
"time.hours": "小时前",
"time.days": "天前",
"filter.all": "全部",
"filter.active": "活跃",
"filter.inactive": "不活跃",
"sort.asc": "升序",
"sort.desc": "降序",
"search.placeholder": "搜索...",
"search.noResults": "无结果",
"pagination.prev": "上一页",
"pagination.next": "下一页",
"pagination.of": "共 {0} 页",
"error.notFound": "未找到",
"error.serverError": "服务器错误",
"error.unauthorized": "未授权",
"error.forbidden": "禁止访问",
"loading": "加载中...",
"empty": "暂无数据",
"more": "更多",
"less": "收起",
"project.create": "创建项目",
"project.edit": "编辑项目",
"project.delete": "删除项目",
"project.name": "项目名称",
"project.description": "项目描述",
"service.create": "创建服务",
"service.edit": "编辑服务",
"service.delete": "删除服务",
"service.name": "服务名称",
"service.type": "服务类型",
"domain.add": "添加域名",
"domain.remove": "移除域名",
"environment.variables": "环境变量",
"environment.add": "添加环境变量",
"environment.edit": "编辑环境变量",
"environment.name": "变量名",
"environment.value": "变量值"
}

View File

@@ -1,17 +1,16 @@
{
"settings.common.save": "保存",
"settings.common.enterTerminal": "进入终端",
"settings.server.domain.title": "域名设置",
"settings.server.domain.description": "添加域名到服务器",
"settings.common.enterTerminal": "终端",
"settings.server.domain.title": "服务器域名",
"settings.server.domain.description": "为您的服务器应用添加域名。",
"settings.server.domain.form.domain": "域名",
"settings.server.domain.form.letsEncryptEmail": "Let's Encrypt 邮箱",
"settings.server.domain.form.certificate.label": "证书",
"settings.server.domain.form.certificate.placeholder": "选择一个证书",
"settings.server.domain.form.certificate.label": "证书提供商",
"settings.server.domain.form.certificate.placeholder": "选择证书",
"settings.server.domain.form.certificateOptions.none": "无",
"settings.server.domain.form.certificateOptions.letsencrypt": "Let's Encrypt",
"settings.server.webServer.title": "服务器设置",
"settings.server.webServer.description": "管理服务器",
"settings.server.webServer.title": "Web 服务器",
"settings.server.webServer.description": "重载或清理 Web 服务器",
"settings.server.webServer.actions": "操作",
"settings.server.webServer.reload": "重新加载",
"settings.server.webServer.watchLogs": "查看日志",
@@ -19,40 +18,50 @@
"settings.server.webServer.server.label": "服务器",
"settings.server.webServer.traefik.label": "Traefik",
"settings.server.webServer.traefik.modifyEnv": "修改环境变量",
"settings.server.webServer.traefik.managePorts": "端口转发",
"settings.server.webServer.traefik.managePortsDescription": "添加或删除 Traefik 的其他端口",
"settings.server.webServer.traefik.managePorts": "额外端口映射",
"settings.server.webServer.traefik.managePortsDescription": " Traefik 添加或删除额外端口",
"settings.server.webServer.traefik.targetPort": "目标端口",
"settings.server.webServer.traefik.publishedPort": "对外端口",
"settings.server.webServer.traefik.publishedPort": "发布端口",
"settings.server.webServer.traefik.addPort": "添加端口",
"settings.server.webServer.traefik.portsUpdated": "端口更新成功",
"settings.server.webServer.traefik.portsUpdateError": "端口更新失败",
"settings.server.webServer.traefik.publishMode": "端口映射",
"settings.server.webServer.traefik.publishMode": "发布模式",
"settings.server.webServer.storage.label": "存储空间",
"settings.server.webServer.storage.cleanUnusedImages": "清理未使用的镜像",
"settings.server.webServer.storage.cleanUnusedVolumes": "清理未使用的卷",
"settings.server.webServer.storage.cleanStoppedContainers": "清理已停止的容器",
"settings.server.webServer.storage.cleanDockerBuilder": "清理 Docker Builder 与 系统缓存",
"settings.server.webServer.storage.cleanDockerBuilder": "清理 Docker Builder 和系统",
"settings.server.webServer.storage.cleanMonitoring": "清理监控数据",
"settings.server.webServer.storage.cleanAll": "清理所有内容",
"settings.profile.title": "账户",
"settings.profile.description": "更改您的个人资料",
"settings.profile.description": "在此更改您的个人资料详情。",
"settings.profile.email": "邮箱",
"settings.profile.password": "密码",
"settings.profile.avatar": "头像",
"settings.appearance.title": "外观",
"settings.appearance.description": "自定义面板主题",
"settings.appearance.description": "自定义您的仪表盘主题",
"settings.appearance.theme": "主题",
"settings.appearance.themeDescription": "选择面板主题",
"settings.appearance.themeDescription": "为您的仪表盘选择主题",
"settings.appearance.themes.light": "明亮",
"settings.appearance.themes.dark": "暗",
"settings.appearance.themes.system": "系统主题",
"settings.appearance.themes.dark": "暗",
"settings.appearance.themes.system": "跟随系统",
"settings.appearance.language": "语言",
"settings.appearance.languageDescription": "选择面板语言",
"settings.terminal.connectionSettings": "终端设置",
"settings.terminal.ipAddress": "IP",
"settings.appearance.languageDescription": "为您的仪表盘选择语言",
"settings.terminal.connectionSettings": "连接设置",
"settings.terminal.ipAddress": "IP 地址",
"settings.terminal.port": "端口",
"settings.terminal.username": "用户名"
"settings.terminal.username": "用户名",
"settings.settings": "设置",
"settings.general": "通用设置",
"settings.security": "安全",
"settings.users": "用户管理",
"settings.roles": "角色管理",
"settings.permissions": "权限",
"settings.api": "API设置",
"settings.certificates": "证书管理",
"settings.ssh": "SSH密钥",
"settings.backups": "备份",
"settings.logs": "日志",
"settings.updates": "更新",
"settings.network": "网络"
}

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

@@ -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

@@ -23,7 +23,17 @@ export const runWebServerBackup = async (backup: BackupSchedule) => {
try {
await execAsync(`mkdir -p ${tempDir}/filesystem`);
const postgresCommand = `docker exec $(docker ps --filter "name=dokploy-postgres" -q) pg_dump -v -Fc -U dokploy -d dokploy > ${tempDir}/database.sql`;
// First get the container ID
const { stdout: containerId } = await execAsync(
"docker ps --filter 'name=dokploy-postgres' -q",
);
if (!containerId) {
throw new Error("PostgreSQL container not found");
}
// 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/`);

View File

@@ -264,7 +264,11 @@ export const getGitlabRepositories = async (gitlabId?: string) => {
const groupName = gitlabProvider.groupName?.toLowerCase();
if (groupName) {
return full_path.toLowerCase().includes(groupName) && kind === "group";
const isIncluded = groupName
.split(",")
.some((name) => full_path.toLowerCase().includes(name));
return isIncluded && kind === "group";
}
return kind === "user";
});
@@ -431,7 +435,9 @@ export const testGitlabConnection = async (
const { full_path, kind } = repo.namespace;
if (groupName) {
return full_path.toLowerCase().includes(groupName) && kind === "group";
return groupName
.split(",")
.some((name) => full_path.toLowerCase().includes(name));
}
return kind === "user";
});