Compare commits

...

22 Commits

Author SHA1 Message Date
Mauricio Siu
acd722678e Merge pull request #521 from Dokploy/canary
v0.9.4
2024-10-03 02:12:03 -06:00
Mauricio Siu
3750977f41 Merge pull request #520 from Dokploy/487-private-docker-container-pull-failed-despite-having-docker-registry-configured-in-registry
fix(registry): add option to login the registry in the remote server
2024-10-03 02:02:51 -06:00
Mauricio Siu
9b401059b0 fix(registry): add option to login the registry in the remote server 2024-10-03 01:56:50 -06:00
Mauricio Siu
6a3ef5c860 Merge pull request #519 from Dokploy/514-failing-to-refresh-docker-composeyml-from-github-repo
fix(compose): delete content when is remote server
2024-10-03 01:32:50 -06:00
Mauricio Siu
a5eb4b0a72 fix(compose): delete content when is remote server 2024-10-03 01:00:35 -06:00
Mauricio Siu
b5c0876dd4 Merge pull request #518 from Dokploy/515-non-admin-users-are-not-able-to-set-up-database-backup
fix(destinations): change admin to protected procedure
2024-10-03 00:53:23 -06:00
Mauricio Siu
9745d12ac8 fix(destinations): change admin to protected procedure 2024-10-03 00:48:00 -06:00
Mauricio Siu
4aaf04ce74 Merge pull request #506 from AprilNEA/fix/domin-port-number-convert
Fix port input value becoming NaN
2024-10-02 13:10:17 -06:00
Mauricio Siu
ecfca9419a refactor: remove innecessary conversion 2024-10-02 13:02:20 -06:00
AprilNEA
dfd6764320 styles: format code with prettier 2024-10-02 18:22:21 +00:00
Mauricio Siu
73bf5274f5 chore(version): bump version 2024-10-01 14:28:11 -06:00
AprilNEA
fc38a42587 fix: convert final value 2024-10-01 14:36:45 +00:00
Mauricio Siu
9b255964fe Merge pull request #511 from Dokploy/509-create-compose-modal-remains-open-after-clicking-create
509 create compose modal remains open after clicking create
2024-09-30 21:33:56 -06:00
Mauricio Siu
29f55ca1a0 Merge pull request #496 from missuo/canary
feat: add update option
2024-09-30 15:04:03 -06:00
Mauricio Siu
6a5fb8faff fix(multi-server): show the servers ip instead of the main ip #502 2024-09-30 15:00:32 -06:00
Mauricio Siu
5c225c8d42 fix(modal): close the modal after the creation #509 2024-09-30 15:00:01 -06:00
AprilNEA
c1c5fc978b fix: fix number convert when string empty 2024-09-30 08:35:49 +00:00
Vincent Yang
18b4b23f79 feat: add update option for canary and feature tag 2024-09-29 22:45:39 -04:00
Mauricio Siu
727e50648e Merge pull request #501 from Dokploy/canary
v0.9.3
2024-09-29 16:30:43 -06:00
Mauricio Siu
0b2b20caeb chore(version): bump version 2024-09-29 16:24:35 -06:00
Mauricio Siu
6cc64b4454 refactior(terminal): add port to server connect 2024-09-29 16:24:09 -06:00
Vincent Young
7027f39c48 feat: add update option 2024-09-28 15:06:20 -04:00
28 changed files with 4453 additions and 424 deletions

View File

@@ -17,7 +17,7 @@ import {
FormLabel, FormLabel,
FormMessage, FormMessage,
} from "@/components/ui/form"; } from "@/components/ui/form";
import { Input } from "@/components/ui/input"; import { Input, NumberInput } from "@/components/ui/input";
import { import {
Select, Select,
SelectContent, SelectContent,
@@ -125,28 +125,14 @@ export const UpdatePort = ({ portId }: Props) => {
<FormItem> <FormItem>
<FormLabel>Published Port</FormLabel> <FormLabel>Published Port</FormLabel>
<FormControl> <FormControl>
<Input <NumberInput placeholder="1-65535" {...field} />
placeholder="1-65535"
{...field}
value={field.value?.toString() || ""}
onChange={(e) => {
const value = e.target.value;
if (value === "") {
field.onChange(0);
} else {
const number = Number.parseInt(value, 10);
if (!Number.isNaN(number)) {
field.onChange(number);
}
}
}}
/>
</FormControl> </FormControl>
<FormMessage /> <FormMessage />
</FormItem> </FormItem>
)} )}
/> />
<FormField <FormField
control={form.control} control={form.control}
name="targetPort" name="targetPort"
@@ -154,22 +140,7 @@ export const UpdatePort = ({ portId }: Props) => {
<FormItem> <FormItem>
<FormLabel>Target Port</FormLabel> <FormLabel>Target Port</FormLabel>
<FormControl> <FormControl>
<Input <Input placeholder="1-65535" {...field} />
placeholder="1-65535"
{...field}
value={field.value?.toString() || ""}
onChange={(e) => {
const value = e.target.value;
if (value === "") {
field.onChange(0);
} else {
const number = Number.parseInt(value, 10);
if (!Number.isNaN(number)) {
field.onChange(number);
}
}
}}
/>
</FormControl> </FormControl>
<FormMessage /> <FormMessage />

View File

@@ -18,7 +18,7 @@ import {
FormLabel, FormLabel,
FormMessage, FormMessage,
} from "@/components/ui/form"; } from "@/components/ui/form";
import { Input } from "@/components/ui/input"; import { Input, NumberInput } from "@/components/ui/input";
import { import {
Select, Select,
SelectContent, SelectContent,
@@ -228,13 +228,7 @@ export const AddDomain = ({
<FormItem> <FormItem>
<FormLabel>Container Port</FormLabel> <FormLabel>Container Port</FormLabel>
<FormControl> <FormControl>
<Input <NumberInput placeholder={"3000"} {...field} />
placeholder={"3000"}
{...field}
onChange={(e) => {
field.onChange(Number.parseInt(e.target.value));
}}
/>
</FormControl> </FormControl>
<FormMessage /> <FormMessage />
</FormItem> </FormItem>

View File

@@ -18,7 +18,7 @@ import {
FormLabel, FormLabel,
FormMessage, FormMessage,
} from "@/components/ui/form"; } from "@/components/ui/form";
import { Input } from "@/components/ui/input"; import { Input, NumberInput } from "@/components/ui/input";
import { import {
Select, Select,
SelectContent, SelectContent,
@@ -364,13 +364,7 @@ export const AddDomainCompose = ({
<FormItem> <FormItem>
<FormLabel>Container Port</FormLabel> <FormLabel>Container Port</FormLabel>
<FormControl> <FormControl>
<Input <NumberInput placeholder={"3000"} {...field} />
placeholder={"3000"}
{...field}
onChange={(e) => {
field.onChange(Number.parseInt(e.target.value));
}}
/>
</FormControl> </FormControl>
<FormMessage /> <FormMessage />
</FormItem> </FormItem>

View File

@@ -48,6 +48,7 @@ export const ShowExternalMariadbCredentials = ({ mariadbId }: Props) => {
const { data, refetch } = api.mariadb.one.useQuery({ mariadbId }); const { data, refetch } = api.mariadb.one.useQuery({ mariadbId });
const { mutateAsync, isLoading } = api.mariadb.saveExternalPort.useMutation(); const { mutateAsync, isLoading } = api.mariadb.saveExternalPort.useMutation();
const [connectionUrl, setConnectionUrl] = useState(""); const [connectionUrl, setConnectionUrl] = useState("");
const getIp = data?.server?.ipAddress || ip;
const form = useForm<DockerProvider>({ const form = useForm<DockerProvider>({
defaultValues: {}, defaultValues: {},
resolver: zodResolver(DockerProviderSchema), resolver: zodResolver(DockerProviderSchema),
@@ -79,7 +80,7 @@ export const ShowExternalMariadbCredentials = ({ mariadbId }: Props) => {
const buildConnectionUrl = () => { const buildConnectionUrl = () => {
const port = form.watch("externalPort") || data?.externalPort; const port = form.watch("externalPort") || data?.externalPort;
return `mariadb://${data?.databaseUser}:${data?.databasePassword}@${ip}:${port}/${data?.databaseName}`; return `mariadb://${data?.databaseUser}:${data?.databasePassword}@${getIp}:${port}/${data?.databaseName}`;
}; };
setConnectionUrl(buildConnectionUrl()); setConnectionUrl(buildConnectionUrl());
@@ -90,7 +91,7 @@ export const ShowExternalMariadbCredentials = ({ mariadbId }: Props) => {
form, form,
data?.databaseName, data?.databaseName,
data?.databaseUser, data?.databaseUser,
ip, getIp,
]); ]);
return ( return (
<> <>

View File

@@ -48,7 +48,7 @@ export const ShowExternalMongoCredentials = ({ mongoId }: Props) => {
const { data, refetch } = api.mongo.one.useQuery({ mongoId }); const { data, refetch } = api.mongo.one.useQuery({ mongoId });
const { mutateAsync, isLoading } = api.mongo.saveExternalPort.useMutation(); const { mutateAsync, isLoading } = api.mongo.saveExternalPort.useMutation();
const [connectionUrl, setConnectionUrl] = useState(""); const [connectionUrl, setConnectionUrl] = useState("");
const getIp = data?.server?.ipAddress || ip;
const form = useForm<DockerProvider>({ const form = useForm<DockerProvider>({
defaultValues: {}, defaultValues: {},
resolver: zodResolver(DockerProviderSchema), resolver: zodResolver(DockerProviderSchema),
@@ -80,7 +80,7 @@ export const ShowExternalMongoCredentials = ({ mongoId }: Props) => {
const buildConnectionUrl = () => { const buildConnectionUrl = () => {
const port = form.watch("externalPort") || data?.externalPort; const port = form.watch("externalPort") || data?.externalPort;
return `mongodb://${data?.databaseUser}:${data?.databasePassword}@${ip}:${port}`; return `mongodb://${data?.databaseUser}:${data?.databasePassword}@${getIp}:${port}`;
}; };
setConnectionUrl(buildConnectionUrl()); setConnectionUrl(buildConnectionUrl());
@@ -90,7 +90,7 @@ export const ShowExternalMongoCredentials = ({ mongoId }: Props) => {
data?.databasePassword, data?.databasePassword,
form, form,
data?.databaseUser, data?.databaseUser,
ip, getIp,
]); ]);
return ( return (

View File

@@ -48,7 +48,7 @@ export const ShowExternalMysqlCredentials = ({ mysqlId }: Props) => {
const { data, refetch } = api.mysql.one.useQuery({ mysqlId }); const { data, refetch } = api.mysql.one.useQuery({ mysqlId });
const { mutateAsync, isLoading } = api.mysql.saveExternalPort.useMutation(); const { mutateAsync, isLoading } = api.mysql.saveExternalPort.useMutation();
const [connectionUrl, setConnectionUrl] = useState(""); const [connectionUrl, setConnectionUrl] = useState("");
const getIp = data?.server?.ipAddress || ip;
const form = useForm<DockerProvider>({ const form = useForm<DockerProvider>({
defaultValues: {}, defaultValues: {},
resolver: zodResolver(DockerProviderSchema), resolver: zodResolver(DockerProviderSchema),
@@ -80,7 +80,7 @@ export const ShowExternalMysqlCredentials = ({ mysqlId }: Props) => {
const buildConnectionUrl = () => { const buildConnectionUrl = () => {
const port = form.watch("externalPort") || data?.externalPort; const port = form.watch("externalPort") || data?.externalPort;
return `mysql://${data?.databaseUser}:${data?.databasePassword}@${ip}:${port}/${data?.databaseName}`; return `mysql://${data?.databaseUser}:${data?.databasePassword}@${getIp}:${port}/${data?.databaseName}`;
}; };
setConnectionUrl(buildConnectionUrl()); setConnectionUrl(buildConnectionUrl());
@@ -91,7 +91,7 @@ export const ShowExternalMysqlCredentials = ({ mysqlId }: Props) => {
data?.databaseName, data?.databaseName,
data?.databaseUser, data?.databaseUser,
form, form,
ip, getIp,
]); ]);
return ( return (
<> <>

View File

@@ -48,6 +48,7 @@ export const ShowExternalPostgresCredentials = ({ postgresId }: Props) => {
const { data, refetch } = api.postgres.one.useQuery({ postgresId }); const { data, refetch } = api.postgres.one.useQuery({ postgresId });
const { mutateAsync, isLoading } = const { mutateAsync, isLoading } =
api.postgres.saveExternalPort.useMutation(); api.postgres.saveExternalPort.useMutation();
const getIp = data?.server?.ipAddress || ip;
const [connectionUrl, setConnectionUrl] = useState(""); const [connectionUrl, setConnectionUrl] = useState("");
const form = useForm<DockerProvider>({ const form = useForm<DockerProvider>({
@@ -79,10 +80,9 @@ export const ShowExternalPostgresCredentials = ({ postgresId }: Props) => {
useEffect(() => { useEffect(() => {
const buildConnectionUrl = () => { const buildConnectionUrl = () => {
const hostname = window.location.hostname;
const port = form.watch("externalPort") || data?.externalPort; const port = form.watch("externalPort") || data?.externalPort;
return `postgresql://${data?.databaseUser}:${data?.databasePassword}@${ip}:${port}/${data?.databaseName}`; return `postgresql://${data?.databaseUser}:${data?.databasePassword}@${getIp}:${port}/${data?.databaseName}`;
}; };
setConnectionUrl(buildConnectionUrl()); setConnectionUrl(buildConnectionUrl());
@@ -92,7 +92,7 @@ export const ShowExternalPostgresCredentials = ({ postgresId }: Props) => {
data?.databasePassword, data?.databasePassword,
form, form,
data?.databaseName, data?.databaseName,
ip, getIp,
]); ]);
return ( return (

View File

@@ -39,7 +39,7 @@ import { slugify } from "@/lib/slug";
import { api } from "@/utils/api"; import { api } from "@/utils/api";
import { zodResolver } from "@hookform/resolvers/zod"; import { zodResolver } from "@hookform/resolvers/zod";
import { CircuitBoard, HelpCircle } from "lucide-react"; import { CircuitBoard, HelpCircle } from "lucide-react";
import { useEffect } from "react"; import { useEffect, useState } from "react";
import { useForm } from "react-hook-form"; import { useForm } from "react-hook-form";
import { toast } from "sonner"; import { toast } from "sonner";
import { z } from "zod"; import { z } from "zod";
@@ -71,6 +71,7 @@ interface Props {
export const AddCompose = ({ projectId, projectName }: Props) => { export const AddCompose = ({ projectId, projectName }: Props) => {
const utils = api.useUtils(); const utils = api.useUtils();
const [visible, setVisible] = useState(false);
const slug = slugify(projectName); const slug = slugify(projectName);
const { data: servers } = api.server.withSSHKey.useQuery(); const { data: servers } = api.server.withSSHKey.useQuery();
const { mutateAsync, isLoading, error, isError } = const { mutateAsync, isLoading, error, isError } =
@@ -101,6 +102,7 @@ export const AddCompose = ({ projectId, projectName }: Props) => {
}) })
.then(async () => { .then(async () => {
toast.success("Compose Created"); toast.success("Compose Created");
setVisible(false);
await utils.project.one.invalidate({ await utils.project.one.invalidate({
projectId, projectId,
}); });
@@ -111,7 +113,7 @@ export const AddCompose = ({ projectId, projectName }: Props) => {
}; };
return ( return (
<Dialog> <Dialog open={visible} onOpenChange={setVisible}>
<DialogTrigger className="w-full"> <DialogTrigger className="w-full">
<DropdownMenuItem <DropdownMenuItem
className="w-full cursor-pointer space-x-3" className="w-full cursor-pointer space-x-3"

View File

@@ -48,6 +48,7 @@ export const ShowExternalRedisCredentials = ({ redisId }: Props) => {
const { data, refetch } = api.redis.one.useQuery({ redisId }); const { data, refetch } = api.redis.one.useQuery({ redisId });
const { mutateAsync, isLoading } = api.redis.saveExternalPort.useMutation(); const { mutateAsync, isLoading } = api.redis.saveExternalPort.useMutation();
const [connectionUrl, setConnectionUrl] = useState(""); const [connectionUrl, setConnectionUrl] = useState("");
const getIp = data?.server?.ipAddress || ip;
const form = useForm<DockerProvider>({ const form = useForm<DockerProvider>({
defaultValues: {}, defaultValues: {},
@@ -81,11 +82,11 @@ export const ShowExternalRedisCredentials = ({ redisId }: Props) => {
const hostname = window.location.hostname; const hostname = window.location.hostname;
const port = form.watch("externalPort") || data?.externalPort; const port = form.watch("externalPort") || data?.externalPort;
return `redis://default:${data?.databasePassword}@${ip}:${port}`; return `redis://default:${data?.databasePassword}@${getIp}:${port}`;
}; };
setConnectionUrl(buildConnectionUrl()); setConnectionUrl(buildConnectionUrl());
}, [data?.appName, data?.externalPort, data?.databasePassword, form, ip]); }, [data?.appName, data?.externalPort, data?.databasePassword, form, getIp]);
return ( return (
<> <>
<div className="flex w-full flex-col gap-5 "> <div className="flex w-full flex-col gap-5 ">

View File

@@ -17,10 +17,18 @@ import {
FormMessage, FormMessage,
} from "@/components/ui/form"; } from "@/components/ui/form";
import { Input } from "@/components/ui/input"; import { Input } from "@/components/ui/input";
import {
Select,
SelectContent,
SelectGroup,
SelectItem,
SelectLabel,
SelectTrigger,
SelectValue,
} from "@/components/ui/select";
import { api } from "@/utils/api"; import { api } from "@/utils/api";
import { zodResolver } from "@hookform/resolvers/zod"; import { zodResolver } from "@hookform/resolvers/zod";
import { AlertTriangle, Container } from "lucide-react"; import { AlertTriangle, Container } from "lucide-react";
import { useRouter } from "next/router";
import { useEffect, useState } from "react"; import { useEffect, useState } from "react";
import { useForm } from "react-hook-form"; import { useForm } from "react-hook-form";
import { toast } from "sonner"; import { toast } from "sonner";
@@ -36,10 +44,9 @@ const AddRegistrySchema = z.object({
password: z.string().min(1, { password: z.string().min(1, {
message: "Password is required", message: "Password is required",
}), }),
registryUrl: z.string().min(1, { registryUrl: z.string(),
message: "Registry URL is required",
}),
imagePrefix: z.string(), imagePrefix: z.string(),
serverId: z.string().optional(),
}); });
type AddRegistry = z.infer<typeof AddRegistrySchema>; type AddRegistry = z.infer<typeof AddRegistrySchema>;
@@ -48,9 +55,9 @@ export const AddRegistry = () => {
const utils = api.useUtils(); const utils = api.useUtils();
const [isOpen, setIsOpen] = useState(false); const [isOpen, setIsOpen] = useState(false);
const { mutateAsync, error, isError } = api.registry.create.useMutation(); const { mutateAsync, error, isError } = api.registry.create.useMutation();
const { data: servers } = api.server.withSSHKey.useQuery();
const { mutateAsync: testRegistry, isLoading } = const { mutateAsync: testRegistry, isLoading } =
api.registry.testRegistry.useMutation(); api.registry.testRegistry.useMutation();
const router = useRouter();
const form = useForm<AddRegistry>({ const form = useForm<AddRegistry>({
defaultValues: { defaultValues: {
username: "", username: "",
@@ -58,6 +65,7 @@ export const AddRegistry = () => {
registryUrl: "", registryUrl: "",
imagePrefix: "", imagePrefix: "",
registryName: "", registryName: "",
serverId: "",
}, },
resolver: zodResolver(AddRegistrySchema), resolver: zodResolver(AddRegistrySchema),
}); });
@@ -67,6 +75,7 @@ export const AddRegistry = () => {
const registryUrl = form.watch("registryUrl"); const registryUrl = form.watch("registryUrl");
const registryName = form.watch("registryName"); const registryName = form.watch("registryName");
const imagePrefix = form.watch("imagePrefix"); const imagePrefix = form.watch("imagePrefix");
const serverId = form.watch("serverId");
useEffect(() => { useEffect(() => {
form.reset({ form.reset({
@@ -74,6 +83,7 @@ export const AddRegistry = () => {
password: "", password: "",
registryUrl: "", registryUrl: "",
imagePrefix: "", imagePrefix: "",
serverId: "",
}); });
}, [form, form.reset, form.formState.isSubmitSuccessful]); }, [form, form.reset, form.formState.isSubmitSuccessful]);
@@ -85,6 +95,7 @@ export const AddRegistry = () => {
registryUrl: data.registryUrl, registryUrl: data.registryUrl,
registryType: "cloud", registryType: "cloud",
imagePrefix: data.imagePrefix, imagePrefix: data.imagePrefix,
serverId: data.serverId,
}) })
.then(async (data) => { .then(async (data) => {
await utils.registry.all.invalidate(); await utils.registry.all.invalidate();
@@ -211,34 +222,77 @@ export const AddRegistry = () => {
)} )}
/> />
</div> </div>
<DialogFooter className="flex flex-row w-full sm:justify-between gap-4 flex-wrap"> <DialogFooter className="flex flex-col w-full sm:justify-between gap-4 flex-wrap sm:flex-col">
<Button <div className="flex flex-col gap-4 border p-2 rounded-lg">
type="button" <span className="text-sm text-muted-foreground">
variant={"secondary"} Select a server to test the registry. If you don't have a
isLoading={isLoading} server choose the default one.
onClick={async () => { </span>
await testRegistry({ <FormField
username: username, control={form.control}
password: password, name="serverId"
registryUrl: registryUrl, render={({ field }) => (
registryName: registryName, <FormItem>
registryType: "cloud", <FormLabel>Server (Optional)</FormLabel>
imagePrefix: imagePrefix, <FormControl>
}) <Select
.then((data) => { onValueChange={field.onChange}
if (data) { defaultValue={field.value}
toast.success("Registry Tested Successfully"); >
} else { <SelectTrigger className="w-full">
toast.error("Registry Test Failed"); <SelectValue placeholder="Select a server" />
} </SelectTrigger>
<SelectContent>
<SelectGroup>
<SelectLabel>Servers</SelectLabel>
{servers?.map((server) => (
<SelectItem
key={server.serverId}
value={server.serverId}
>
{server.name}
</SelectItem>
))}
<SelectItem value={"none"}>None</SelectItem>
</SelectGroup>
</SelectContent>
</Select>
</FormControl>
<FormMessage />
</FormItem>
)}
/>
<Button
type="button"
variant={"secondary"}
isLoading={isLoading}
onClick={async () => {
await testRegistry({
username: username,
password: password,
registryUrl: registryUrl,
registryName: registryName,
registryType: "cloud",
imagePrefix: imagePrefix,
serverId: serverId,
}) })
.catch(() => { .then((data) => {
toast.error("Error to test the registry"); if (data) {
}); toast.success("Registry Tested Successfully");
}} } else {
> toast.error("Registry Test Failed");
Test Registry }
</Button> })
.catch(() => {
toast.error("Error to test the registry");
});
}}
>
Test Registry
</Button>
</div>
<Button isLoading={form.formState.isSubmitting} type="submit"> <Button isLoading={form.formState.isSubmitting} type="submit">
Create Create
</Button> </Button>

View File

@@ -17,6 +17,15 @@ import {
FormMessage, FormMessage,
} from "@/components/ui/form"; } from "@/components/ui/form";
import { Input } from "@/components/ui/input"; import { Input } from "@/components/ui/input";
import {
Select,
SelectContent,
SelectGroup,
SelectItem,
SelectLabel,
SelectTrigger,
SelectValue,
} from "@/components/ui/select";
import { cn } from "@/lib/utils"; import { cn } from "@/lib/utils";
import { api } from "@/utils/api"; import { api } from "@/utils/api";
import { zodResolver } from "@hookform/resolvers/zod"; import { zodResolver } from "@hookform/resolvers/zod";
@@ -34,10 +43,9 @@ const updateRegistry = z.object({
message: "Username is required", message: "Username is required",
}), }),
password: z.string(), password: z.string(),
registryUrl: z.string().min(1, { registryUrl: z.string(),
message: "Registry URL is required",
}),
imagePrefix: z.string(), imagePrefix: z.string(),
serverId: z.string().optional(),
}); });
type UpdateRegistry = z.infer<typeof updateRegistry>; type UpdateRegistry = z.infer<typeof updateRegistry>;
@@ -48,6 +56,8 @@ interface Props {
export const UpdateDockerRegistry = ({ registryId }: Props) => { export const UpdateDockerRegistry = ({ registryId }: Props) => {
const utils = api.useUtils(); const utils = api.useUtils();
const { data: servers } = api.server.withSSHKey.useQuery();
const { mutateAsync: testRegistry, isLoading } = const { mutateAsync: testRegistry, isLoading } =
api.registry.testRegistry.useMutation(); api.registry.testRegistry.useMutation();
const { data, refetch } = api.registry.one.useQuery( const { data, refetch } = api.registry.one.useQuery(
@@ -69,15 +79,19 @@ export const UpdateDockerRegistry = ({ registryId }: Props) => {
username: "", username: "",
password: "", password: "",
registryUrl: "", registryUrl: "",
serverId: "",
}, },
resolver: zodResolver(updateRegistry), resolver: zodResolver(updateRegistry),
}); });
console.log(form.formState.errors);
const password = form.watch("password"); const password = form.watch("password");
const username = form.watch("username"); const username = form.watch("username");
const registryUrl = form.watch("registryUrl"); const registryUrl = form.watch("registryUrl");
const registryName = form.watch("registryName"); const registryName = form.watch("registryName");
const imagePrefix = form.watch("imagePrefix"); const imagePrefix = form.watch("imagePrefix");
const serverId = form.watch("serverId");
useEffect(() => { useEffect(() => {
if (data) { if (data) {
@@ -87,6 +101,7 @@ export const UpdateDockerRegistry = ({ registryId }: Props) => {
username: data.username || "", username: data.username || "",
password: "", password: "",
registryUrl: data.registryUrl || "", registryUrl: data.registryUrl || "",
serverId: "",
}); });
} }
}, [form, form.reset, data]); }, [form, form.reset, data]);
@@ -99,6 +114,7 @@ export const UpdateDockerRegistry = ({ registryId }: Props) => {
username: data.username, username: data.username,
registryUrl: data.registryUrl, registryUrl: data.registryUrl,
imagePrefix: data.imagePrefix, imagePrefix: data.imagePrefix,
serverId: data.serverId,
}) })
.then(async (data) => { .then(async (data) => {
toast.success("Registry Updated"); toast.success("Registry Updated");
@@ -224,13 +240,47 @@ export const UpdateDockerRegistry = ({ registryId }: Props) => {
</div> </div>
</form> </form>
<DialogFooter <DialogFooter className="flex flex-col w-full sm:justify-between gap-4 flex-wrap sm:flex-col">
className={cn( <div className="flex flex-col gap-4 border p-2 rounded-lg">
isCloud ? "sm:justify-between " : "", <span className="text-sm text-muted-foreground">
"flex flex-row w-full gap-4 flex-wrap", Select a server to test the registry. If you don't have a server
)} choose the default one.
> </span>
{isCloud && ( <FormField
control={form.control}
name="serverId"
render={({ field }) => (
<FormItem>
<FormLabel>Server (Optional)</FormLabel>
<FormControl>
<Select
onValueChange={field.onChange}
defaultValue={field.value}
>
<SelectTrigger className="w-full">
<SelectValue placeholder="Select a server" />
</SelectTrigger>
<SelectContent>
<SelectGroup>
<SelectLabel>Servers</SelectLabel>
{servers?.map((server) => (
<SelectItem
key={server.serverId}
value={server.serverId}
>
{server.name}
</SelectItem>
))}
<SelectItem value={"none"}>None</SelectItem>
</SelectGroup>
</SelectContent>
</Select>
</FormControl>
<FormMessage />
</FormItem>
)}
/>
<Button <Button
type="button" type="button"
variant={"secondary"} variant={"secondary"}
@@ -243,6 +293,7 @@ export const UpdateDockerRegistry = ({ registryId }: Props) => {
registryName: registryName, registryName: registryName,
registryType: "cloud", registryType: "cloud",
imagePrefix: imagePrefix, imagePrefix: imagePrefix,
serverId: serverId,
}) })
.then((data) => { .then((data) => {
if (data) { if (data) {
@@ -258,12 +309,12 @@ export const UpdateDockerRegistry = ({ registryId }: Props) => {
> >
Test Registry Test Registry
</Button> </Button>
)} </div>
<Button <Button
isLoading={form.formState.isSubmitting} isLoading={form.formState.isSubmitting}
form="hook-form"
type="submit" type="submit"
form="hook-form"
> >
Update Update
</Button> </Button>

View File

@@ -31,4 +31,39 @@ const Input = React.forwardRef<HTMLInputElement, InputProps>(
); );
Input.displayName = "Input"; Input.displayName = "Input";
export { Input }; const NumberInput = React.forwardRef<HTMLInputElement, InputProps>(
({ className, errorMessage, ...props }, ref) => {
return (
<Input
type="text"
className={cn("text-left", className)}
ref={ref}
{...props}
value={props.value === undefined ? undefined : String(props.value)}
onChange={(e) => {
const value = e.target.value;
if (value === "") {
props.onChange?.(e);
} else {
const number = Number.parseInt(value, 10);
if (!Number.isNaN(number)) {
const syntheticEvent = {
...e,
target: {
...e.target,
value: number,
},
};
props.onChange?.(
syntheticEvent as unknown as React.ChangeEvent<HTMLInputElement>,
);
}
}
}}
/>
);
},
);
NumberInput.displayName = "NumberInput";
export { Input, NumberInput };

View File

@@ -0,0 +1 @@
ALTER TABLE "registry" ALTER COLUMN "registryUrl" SET DEFAULT '';

File diff suppressed because it is too large Load Diff

View File

@@ -267,6 +267,13 @@
"when": 1726988289562, "when": 1726988289562,
"tag": "0037_legal_namor", "tag": "0037_legal_namor",
"breakpoints": true "breakpoints": true
},
{
"idx": 38,
"version": "6",
"when": 1727942090102,
"tag": "0038_rapid_landau",
"breakpoints": true
} }
] ]
} }

View File

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

View File

@@ -68,7 +68,7 @@ export const destinationRouter = createTRPCRouter({
const destination = await findDestinationById(input.destinationId); const destination = await findDestinationById(input.destinationId);
return destination; return destination;
}), }),
all: adminProcedure.query(async () => { all: protectedProcedure.query(async () => {
return await db.query.destinations.findMany({}); return await db.query.destinations.findMany({});
}), }),
remove: adminProcedure remove: adminProcedure

View File

@@ -7,7 +7,7 @@ import {
apiUpdateRegistry, apiUpdateRegistry,
} from "@/server/db/schema"; } from "@/server/db/schema";
import { initializeRegistry } from "@/server/setup/registry-setup"; import { initializeRegistry } from "@/server/setup/registry-setup";
import { execAsync } from "@/server/utils/process/execAsync"; import { execAsync, execAsyncRemote } from "@/server/utils/process/execAsync";
import { manageRegistry } from "@/server/utils/traefik/registry"; import { manageRegistry } from "@/server/utils/traefik/registry";
import { TRPCError } from "@trpc/server"; import { TRPCError } from "@trpc/server";
import { import {
@@ -58,7 +58,13 @@ export const registryRouter = createTRPCRouter({
.mutation(async ({ input }) => { .mutation(async ({ input }) => {
try { try {
const loginCommand = `echo ${input.password} | docker login ${input.registryUrl} --username ${input.username} --password-stdin`; const loginCommand = `echo ${input.password} | docker login ${input.registryUrl} --username ${input.username} --password-stdin`;
await execAsync(loginCommand);
if (input.serverId && input.serverId !== "none") {
await execAsyncRemote(input.serverId, loginCommand);
} else {
await execAsync(loginCommand);
}
return true; return true;
} catch (error) { } catch (error) {
console.log("Error Registry:", error); console.log("Error Registry:", error);
@@ -78,6 +84,7 @@ export const registryRouter = createTRPCRouter({
? input.registryUrl ? input.registryUrl
: "dokploy-registry.docker.localhost", : "dokploy-registry.docker.localhost",
imagePrefix: null, imagePrefix: null,
serverId: undefined,
}); });
await manageRegistry(selfHostedRegistry); await manageRegistry(selfHostedRegistry);
@@ -86,3 +93,17 @@ export const registryRouter = createTRPCRouter({
return selfHostedRegistry; return selfHostedRegistry;
}), }),
}); });
const shellEscape = (str: string) => {
const ret = [];
let s = str;
if (/[^A-Za-z0-9_\/:=-]/.test(s)) {
s = `'${s.replace(/'/g, "'\\''")}'`;
s = s
.replace(/^(?:'')+/g, "") // unduplicate single-quote at the beginning
.replace(/\\'''/g, "\\'"); // remove non-escaped single-quote if there are enclosed between 2 escaped
}
ret.push(s);
return ret.join(" ");
};

View File

@@ -2,7 +2,7 @@ import { db } from "@/server/db";
import { type apiCreateRegistry, registry } from "@/server/db/schema"; import { type apiCreateRegistry, registry } from "@/server/db/schema";
import { initializeRegistry } from "@/server/setup/registry-setup"; import { initializeRegistry } from "@/server/setup/registry-setup";
import { removeService } from "@/server/utils/docker/utils"; import { removeService } from "@/server/utils/docker/utils";
import { execAsync } from "@/server/utils/process/execAsync"; import { execAsync, execAsyncRemote } from "@/server/utils/process/execAsync";
import { import {
manageRegistry, manageRegistry,
removeSelfHostedRegistry, removeSelfHostedRegistry,
@@ -32,9 +32,10 @@ export const createRegistry = async (input: typeof apiCreateRegistry._type) => {
message: "Error input: Inserting registry", message: "Error input: Inserting registry",
}); });
} }
const loginCommand = `echo ${input.password} | docker login ${input.registryUrl} --username ${input.username} --password-stdin`;
if (newRegistry.registryType === "cloud") { if (input.serverId && input.serverId !== "none") {
const loginCommand = `echo ${input.password} | docker login ${input.registryUrl} --username ${input.username} --password-stdin`; await execAsyncRemote(input.serverId, loginCommand);
} else if (newRegistry.registryType === "cloud") {
await execAsync(loginCommand); await execAsync(loginCommand);
} }
@@ -76,7 +77,7 @@ export const removeRegistry = async (registryId: string) => {
export const updateRegistry = async ( export const updateRegistry = async (
registryId: string, registryId: string,
registryData: Partial<Registry>, registryData: Partial<Registry> & { serverId?: string | null },
) => { ) => {
try { try {
const response = await db const response = await db
@@ -92,6 +93,13 @@ export const updateRegistry = async (
await manageRegistry(response); await manageRegistry(response);
await initializeRegistry(response.username, response.password); await initializeRegistry(response.username, response.password);
} }
const loginCommand = `echo ${response?.password} | docker login ${response?.registryUrl} --username ${response?.username} --password-stdin`;
if (registryData?.serverId && registryData?.serverId !== "none") {
await execAsyncRemote(registryData.serverId, loginCommand);
} else if (response?.registryType === "cloud") {
await execAsync(loginCommand);
}
return response; return response;
} catch (error) { } catch (error) {

View File

@@ -23,7 +23,7 @@ export const registry = pgTable("registry", {
imagePrefix: text("imagePrefix"), imagePrefix: text("imagePrefix"),
username: text("username").notNull(), username: text("username").notNull(),
password: text("password").notNull(), password: text("password").notNull(),
registryUrl: text("registryUrl").notNull(), registryUrl: text("registryUrl").notNull().default(""),
createdAt: text("createdAt") createdAt: text("createdAt")
.notNull() .notNull()
.$defaultFn(() => new Date().toISOString()), .$defaultFn(() => new Date().toISOString()),
@@ -45,7 +45,7 @@ const createSchema = createInsertSchema(registry, {
registryName: z.string().min(1), registryName: z.string().min(1),
username: z.string().min(1), username: z.string().min(1),
password: z.string().min(1), password: z.string().min(1),
registryUrl: z.string().min(1), registryUrl: z.string(),
adminId: z.string().min(1), adminId: z.string().min(1),
registryId: z.string().min(1), registryId: z.string().min(1),
registryType: z.enum(["selfHosted", "cloud"]), registryType: z.enum(["selfHosted", "cloud"]),
@@ -62,7 +62,10 @@ export const apiCreateRegistry = createSchema
registryType: z.enum(["selfHosted", "cloud"]), registryType: z.enum(["selfHosted", "cloud"]),
imagePrefix: z.string().nullable().optional(), imagePrefix: z.string().nullable().optional(),
}) })
.required(); .required()
.extend({
serverId: z.string().optional(),
});
export const apiTestRegistry = createSchema.pick({}).extend({ export const apiTestRegistry = createSchema.pick({}).extend({
registryName: z.string().min(1), registryName: z.string().min(1),
@@ -71,6 +74,7 @@ export const apiTestRegistry = createSchema.pick({}).extend({
registryUrl: z.string(), registryUrl: z.string(),
registryType: z.enum(["selfHosted", "cloud"]), registryType: z.enum(["selfHosted", "cloud"]),
imagePrefix: z.string().nullable().optional(), imagePrefix: z.string().nullable().optional(),
serverId: z.string().optional(),
}); });
export const apiRemoveRegistry = createSchema export const apiRemoveRegistry = createSchema
@@ -87,6 +91,7 @@ export const apiFindOneRegistry = createSchema
export const apiUpdateRegistry = createSchema.partial().extend({ export const apiUpdateRegistry = createSchema.partial().extend({
registryId: z.string().min(1), registryId: z.string().min(1),
serverId: z.string().optional(),
}); });
export const apiEnableSelfHostedRegistry = createSchema export const apiEnableSelfHostedRegistry = createSchema

View File

@@ -147,15 +147,15 @@ export const cloneRawBitbucketRepositoryRemote = async (compose: Compose) => {
const bitbucketProvider = await findBitbucketById(bitbucketId); const bitbucketProvider = await findBitbucketById(bitbucketId);
const basePath = COMPOSE_PATH; const basePath = COMPOSE_PATH;
const outputPath = join(basePath, appName, "code"); const outputPath = join(basePath, appName, "code");
await recreateDirectory(outputPath);
const repoclone = `bitbucket.org/${bitbucketOwner}/${bitbucketRepository}.git`; const repoclone = `bitbucket.org/${bitbucketOwner}/${bitbucketRepository}.git`;
const cloneUrl = `https://${bitbucketProvider?.bitbucketUsername}:${bitbucketProvider?.appPassword}@${repoclone}`; const cloneUrl = `https://${bitbucketProvider?.bitbucketUsername}:${bitbucketProvider?.appPassword}@${repoclone}`;
try { try {
await execAsyncRemote( const command = `
serverId, rm -rf ${outputPath};
`git clone --branch ${bitbucketBranch} --depth 1 ${cloneUrl} ${outputPath}`, git clone --branch ${bitbucketBranch} --depth 1 ${cloneUrl} ${outputPath}
); `;
await execAsyncRemote(serverId, command);
} catch (error) { } catch (error) {
throw error; throw error;
} }

View File

@@ -271,13 +271,13 @@ export const cloneRawGithubRepositoryRemote = async (compose: Compose) => {
const octokit = authGithub(githubProvider); const octokit = authGithub(githubProvider);
const token = await getGithubToken(octokit); const token = await getGithubToken(octokit);
const repoclone = `github.com/${owner}/${repository}.git`; const repoclone = `github.com/${owner}/${repository}.git`;
await recreateDirectory(outputPath);
const cloneUrl = `https://oauth2:${token}@${repoclone}`; const cloneUrl = `https://oauth2:${token}@${repoclone}`;
try { try {
await execAsyncRemote( const command = `
serverId, rm -rf ${outputPath};
`git clone --branch ${branch} --depth 1 ${cloneUrl} ${outputPath}`, git clone --branch ${branch} --depth 1 ${cloneUrl} ${outputPath}
); `;
await execAsyncRemote(serverId, command);
} catch (error) { } catch (error) {
throw error; throw error;
} }

View File

@@ -390,14 +390,14 @@ export const cloneRawGitlabRepositoryRemote = async (compose: Compose) => {
await refreshGitlabToken(gitlabId); await refreshGitlabToken(gitlabId);
const basePath = COMPOSE_PATH; const basePath = COMPOSE_PATH;
const outputPath = join(basePath, appName, "code"); const outputPath = join(basePath, appName, "code");
await recreateDirectory(outputPath);
const repoclone = `gitlab.com/${gitlabPathNamespace}.git`; const repoclone = `gitlab.com/${gitlabPathNamespace}.git`;
const cloneUrl = `https://oauth2:${gitlabProvider?.accessToken}@${repoclone}`; const cloneUrl = `https://oauth2:${gitlabProvider?.accessToken}@${repoclone}`;
try { try {
await execAsyncRemote( const command = `
serverId, rm -rf ${outputPath};
`git clone --branch ${branch} --depth 1 ${cloneUrl} ${outputPath}`, git clone --branch ${branch} --depth 1 ${cloneUrl} ${outputPath}
); `;
await execAsyncRemote(serverId, command);
} catch (error) { } catch (error) {
throw error; throw error;
} }

View File

@@ -70,6 +70,7 @@ export const createComposeFileRawRemote = async (compose: Compose) => {
try { try {
const encodedContent = encodeBase64(composeFile); const encodedContent = encodeBase64(composeFile);
const command = ` const command = `
rm -rf ${outputPath};
mkdir -p ${outputPath}; mkdir -p ${outputPath};
echo "${encodedContent}" | base64 -d > "${filePath}"; echo "${encodedContent}" | base64 -d > "${filePath}";
`; `;

View File

@@ -72,6 +72,8 @@ export const setupTerminalWebSocketServer = (
"StrictHostKeyChecking=no", "StrictHostKeyChecking=no",
"-i", "-i",
privateKey, privateKey,
"-p",
`${server.port}`,
`${server.username}@${server.ipAddress}`, `${server.username}@${server.ipAddress}`,
]; ];
const ptyProcess = spawn("ssh", sshCommand.slice(1), { const ptyProcess = spawn("ssh", sshCommand.slice(1), {

View File

@@ -1,114 +1,133 @@
#!/bin/bash #!/bin/bash
install_dokploy() {
if [ "$(id -u)" != "0" ]; then
echo "This script must be run as root" >&2
exit 1
fi
if [ "$(id -u)" != "0" ]; then # check if is Mac OS
echo "This script must be run as root" >&2 if [ "$(uname)" = "Darwin" ]; then
exit 1 echo "This script must be run on Linux" >&2
fi exit 1
fi
# check if is Mac OS
if [ "$(uname)" = "Darwin" ]; then
echo "This script must be run on Linux" >&2
exit 1
fi
# check if is running inside a container # check if is running inside a container
if [ -f /.dockerenv ]; then if [ -f /.dockerenv ]; then
echo "This script must be run on Linux" >&2 echo "This script must be run on Linux" >&2
exit 1 exit 1
fi fi
# check if something is running on port 80 # check if something is running on port 80
if ss -tulnp | grep ':80 ' >/dev/null; then if ss -tulnp | grep ':80 ' >/dev/null; then
echo "Error: something is already running on port 80" >&2 echo "Error: something is already running on port 80" >&2
exit 1 exit 1
fi fi
# check if something is running on port 443 # check if something is running on port 443
if ss -tulnp | grep ':443 ' >/dev/null; then if ss -tulnp | grep ':443 ' >/dev/null; then
echo "Error: something is already running on port 443" >&2 echo "Error: something is already running on port 443" >&2
exit 1 exit 1
fi fi
command_exists() { command_exists() {
command -v "$@" > /dev/null 2>&1 command -v "$@" > /dev/null 2>&1
} }
if command_exists docker; then if command_exists docker; then
echo "Docker already installed" echo "Docker already installed"
else
curl -sSL https://get.docker.com | sh
fi
docker swarm leave --force 2>/dev/null
get_ip() {
# Try to get IPv4
local ipv4=$(curl -4s https://ifconfig.io 2>/dev/null)
if [ -n "$ipv4" ]; then
echo "$ipv4"
else else
# Try to get IPv6 curl -sSL https://get.docker.com | sh
local ipv6=$(curl -6s https://ifconfig.io 2>/dev/null) fi
if [ -n "$ipv6" ]; then
echo "$ipv6" docker swarm leave --force 2>/dev/null
get_ip() {
# Try to get IPv4
local ipv4=$(curl -4s https://ifconfig.io 2>/dev/null)
if [ -n "$ipv4" ]; then
echo "$ipv4"
else
# Try to get IPv6
local ipv6=$(curl -6s https://ifconfig.io 2>/dev/null)
if [ -n "$ipv6" ]; then
echo "$ipv6"
fi
fi fi
fi }
advertise_addr=$(get_ip)
docker swarm init --advertise-addr $advertise_addr
echo "Swarm initialized"
docker network rm -f dokploy-network 2>/dev/null
docker network create --driver overlay --attachable dokploy-network
echo "Network created"
mkdir -p /etc/dokploy
chmod 777 /etc/dokploy
docker pull dokploy/dokploy:canary
# Installation
docker service create \
--name dokploy \
--replicas 1 \
--network dokploy-network \
--mount type=bind,source=/var/run/docker.sock,target=/var/run/docker.sock \
--mount type=bind,source=/etc/dokploy,target=/etc/dokploy \
--mount type=volume,source=dokploy-docker-config,target=/root/.docker \
--publish published=3000,target=3000,mode=host \
--update-parallelism 1 \
--update-order stop-first \
--constraint 'node.role == manager' \
-e RELEASE_TAG=canary \
dokploy/dokploy:canary
GREEN="\033[0;32m"
YELLOW="\033[1;33m"
BLUE="\033[0;34m"
NC="\033[0m" # No Color
format_ip_for_url() {
local ip="$1"
if echo "$ip" | grep -q ':'; then
# IPv6
echo "[${ip}]"
else
# IPv4
echo "${ip}"
fi
}
formatted_addr=$(format_ip_for_url "$advertise_addr")
echo ""
printf "${GREEN}Congratulations, Dokploy is installed!${NC}\n"
printf "${BLUE}Wait 15 seconds for the server to start${NC}\n"
printf "${YELLOW}Please go to http://${formatted_addr}:3000${NC}\n\n"
echo ""
} }
advertise_addr=$(get_ip) update_dokploy() {
echo "Updating Dokploy..."
# Pull the latest canary image
docker pull dokploy/dokploy:canary
docker swarm init --advertise-addr $advertise_addr # Update the service
docker service update --image dokploy/dokploy:canary dokploy
echo "Swarm initialized" echo "Dokploy has been updated to the latest canary version."
docker network rm -f dokploy-network 2>/dev/null
docker network create --driver overlay --attachable dokploy-network
echo "Network created"
mkdir -p /etc/dokploy
chmod 777 /etc/dokploy
docker pull dokploy/dokploy:canary
# Installation
docker service create \
--name dokploy \
--replicas 1 \
--network dokploy-network \
--mount type=bind,source=/var/run/docker.sock,target=/var/run/docker.sock \
--mount type=bind,source=/etc/dokploy,target=/etc/dokploy \
--mount type=volume,source=dokploy-docker-config,target=/root/.docker \
--publish published=3000,target=3000,mode=host \
--update-parallelism 1 \
--update-order stop-first \
--constraint 'node.role == manager' \
-e RELEASE_TAG=canary \
dokploy/dokploy:canary
GREEN="\033[0;32m"
YELLOW="\033[1;33m"
BLUE="\033[0;34m"
NC="\033[0m" # No Color
format_ip_for_url() {
local ip="$1"
if echo "$ip" | grep -q ':'; then
# IPv6
echo "[${ip}]"
else
# IPv4
echo "${ip}"
fi
} }
formatted_addr=$(format_ip_for_url "$advertise_addr") # Main script execution
echo "" if [ "$1" = "update" ]; then
printf "${GREEN}Congratulations, Dokploy is installed!${NC}\n" update_dokploy
printf "${BLUE}Wait 15 seconds for the server to start${NC}\n" else
printf "${YELLOW}Please go to http://${formatted_addr}:3000${NC}\n\n" install_dokploy
echo "" fi

View File

@@ -1,97 +1,117 @@
#!/bin/bash #!/bin/bash
install_dokploy() {
if [ "$(id -u)" != "0" ]; then if [ "$(id -u)" != "0" ]; then
echo "This script must be run as root" >&2 echo "This script must be run as root" >&2
exit 1 exit 1
fi
# check if is Mac OS
if [ "$(uname)" = "Darwin" ]; then
echo "This script must be run on Linux" >&2
exit 1
fi
# check if is running inside a container
if [ -f /.dockerenv ]; then
echo "This script must be run on Linux" >&2
exit 1
fi
# check if something is running on port 80
if ss -tulnp | grep ':80 ' >/dev/null; then
echo "Error: something is already running on port 80" >&2
exit 1
fi
# check if something is running on port 443
if ss -tulnp | grep ':443 ' >/dev/null; then
echo "Error: something is already running on port 443" >&2
exit 1
fi
command_exists() {
command -v "$@" > /dev/null 2>&1
}
if command_exists docker; then
echo "Docker already installed"
else
curl -sSL https://get.docker.com | sh
fi
docker swarm leave --force 2>/dev/null
advertise_addr=$(curl -s ifconfig.me)
docker swarm init --advertise-addr $advertise_addr
echo "Swarm initialized"
docker network rm -f dokploy-network 2>/dev/null
docker network create --driver overlay --attachable dokploy-network
echo "Network created"
mkdir -p /etc/dokploy
chmod 777 /etc/dokploy
docker pull dokploy/dokploy:feature
# Installation
docker service create \
--name dokploy \
--replicas 1 \
--network dokploy-network \
--mount type=bind,source=/var/run/docker.sock,target=/var/run/docker.sock \
--mount type=bind,source=/etc/dokploy,target=/etc/dokploy \
--mount type=volume,source=dokploy-docker-config,target=/root/.docker \
--publish published=3000,target=3000,mode=host \
--update-parallelism 1 \
--update-order stop-first \
--constraint 'node.role == manager' \
-e RELEASE_TAG=feature \
dokploy/dokploy:feature
GREEN="\033[0;32m"
YELLOW="\033[1;33m"
BLUE="\033[0;34m"
NC="\033[0m" # No Color
format_ip_for_url() {
local ip="$1"
if echo "$ip" | grep -q ':'; then
# IPv6
echo "[${ip}]"
else
# IPv4
echo "${ip}"
fi fi
# check if is Mac OS
if [ "$(uname)" = "Darwin" ]; then
echo "This script must be run on Linux" >&2
exit 1
fi
# check if is running inside a container
if [ -f /.dockerenv ]; then
echo "This script must be run on Linux" >&2
exit 1
fi
# check if something is running on port 80
if ss -tulnp | grep ':80 ' >/dev/null; then
echo "Error: something is already running on port 80" >&2
exit 1
fi
# check if something is running on port 443
if ss -tulnp | grep ':443 ' >/dev/null; then
echo "Error: something is already running on port 443" >&2
exit 1
fi
command_exists() {
command -v "$@" > /dev/null 2>&1
}
if command_exists docker; then
echo "Docker already installed"
else
curl -sSL https://get.docker.com | sh
fi
docker swarm leave --force 2>/dev/null
advertise_addr=$(curl -s ifconfig.me)
docker swarm init --advertise-addr $advertise_addr
echo "Swarm initialized"
docker network rm -f dokploy-network 2>/dev/null
docker network create --driver overlay --attachable dokploy-network
echo "Network created"
mkdir -p /etc/dokploy
chmod 777 /etc/dokploy
docker pull dokploy/dokploy:feature
# Installation
docker service create \
--name dokploy \
--replicas 1 \
--network dokploy-network \
--mount type=bind,source=/var/run/docker.sock,target=/var/run/docker.sock \
--mount type=bind,source=/etc/dokploy,target=/etc/dokploy \
--mount type=volume,source=dokploy-docker-config,target=/root/.docker \
--publish published=3000,target=3000,mode=host \
--update-parallelism 1 \
--update-order stop-first \
--constraint 'node.role == manager' \
-e RELEASE_TAG=feature \
dokploy/dokploy:feature
GREEN="\033[0;32m"
YELLOW="\033[1;33m"
BLUE="\033[0;34m"
NC="\033[0m" # No Color
format_ip_for_url() {
local ip="$1"
if echo "$ip" | grep -q ':'; then
# IPv6
echo "[${ip}]"
else
# IPv4
echo "${ip}"
fi
}
formatted_addr=$(format_ip_for_url "$advertise_addr")
echo ""
printf "${GREEN}Congratulations, Dokploy is installed!${NC}\n"
printf "${BLUE}Wait 15 seconds for the server to start${NC}\n"
printf "${YELLOW}Please go to http://${formatted_addr}:3000${NC}\n\n"
echo ""
} }
formatted_addr=$(format_ip_for_url "$advertise_addr") update_dokploy() {
echo "" echo "Updating Dokploy..."
printf "${GREEN}Congratulations, Dokploy is installed!${NC}\n"
printf "${BLUE}Wait 15 seconds for the server to start${NC}\n" # Pull the latest feature image
printf "${YELLOW}Please go to http://${formatted_addr}:3000${NC}\n\n" docker pull dokploy/dokploy:feature
echo ""
# Update the service
docker service update --image dokploy/dokploy:feature dokploy
echo "Dokploy has been updated to the latest feature version."
}
# Main script execution
if [ "$1" = "update" ]; then
update_dokploy
else
install_dokploy
fi

View File

@@ -1,112 +1,130 @@
#!/bin/bash #!/bin/bash
install_dokploy() {
if [ "$(id -u)" != "0" ]; then
echo "This script must be run as root" >&2
exit 1
fi
if [ "$(id -u)" != "0" ]; then # check if is Mac OS
echo "This script must be run as root" >&2 if [ "$(uname)" = "Darwin" ]; then
exit 1 echo "This script must be run on Linux" >&2
fi exit 1
fi
# check if is Mac OS # check if is running inside a container
if [ "$(uname)" = "Darwin" ]; then if [ -f /.dockerenv ]; then
echo "This script must be run on Linux" >&2 echo "This script must be run on Linux" >&2
exit 1 exit 1
fi fi
# check if something is running on port 80
if ss -tulnp | grep ':80 ' >/dev/null; then
echo "Error: something is already running on port 80" >&2
exit 1
fi
# check if is running inside a container # check if something is running on port 443
if [ -f /.dockerenv ]; then if ss -tulnp | grep ':443 ' >/dev/null; then
echo "This script must be run on Linux" >&2 echo "Error: something is already running on port 443" >&2
exit 1 exit 1
fi fi
# check if something is running on port 80 command_exists() {
if ss -tulnp | grep ':80 ' >/dev/null; then command -v "$@" > /dev/null 2>&1
echo "Error: something is already running on port 80" >&2 }
exit 1
fi
# check if something is running on port 443 if command_exists docker; then
if ss -tulnp | grep ':443 ' >/dev/null; then echo "Docker already installed"
echo "Error: something is already running on port 443" >&2
exit 1
fi
command_exists() {
command -v "$@" > /dev/null 2>&1
}
if command_exists docker; then
echo "Docker already installed"
else
curl -sSL https://get.docker.com | sh
fi
docker swarm leave --force 2>/dev/null
get_ip() {
# Try to get IPv4
local ipv4=$(curl -4s https://ifconfig.io 2>/dev/null)
if [ -n "$ipv4" ]; then
echo "$ipv4"
else else
# Try to get IPv6 curl -sSL https://get.docker.com | sh
local ipv6=$(curl -6s https://ifconfig.io 2>/dev/null) fi
if [ -n "$ipv6" ]; then
echo "$ipv6" docker swarm leave --force 2>/dev/null
get_ip() {
# Try to get IPv4
local ipv4=$(curl -4s https://ifconfig.io 2>/dev/null)
if [ -n "$ipv4" ]; then
echo "$ipv4"
else
# Try to get IPv6
local ipv6=$(curl -6s https://ifconfig.io 2>/dev/null)
if [ -n "$ipv6" ]; then
echo "$ipv6"
fi
fi fi
fi }
advertise_addr=$(get_ip)
docker swarm init --advertise-addr $advertise_addr
echo "Swarm initialized"
docker network rm -f dokploy-network 2>/dev/null
docker network create --driver overlay --attachable dokploy-network
echo "Network created"
mkdir -p /etc/dokploy
chmod 777 /etc/dokploy
docker pull dokploy/dokploy:latest
# Installation
docker service create \
--name dokploy \
--replicas 1 \
--network dokploy-network \
--mount type=bind,source=/var/run/docker.sock,target=/var/run/docker.sock \
--mount type=bind,source=/etc/dokploy,target=/etc/dokploy \
--mount type=volume,source=dokploy-docker-config,target=/root/.docker \
--publish published=3000,target=3000,mode=host \
--update-parallelism 1 \
--update-order stop-first \
--constraint 'node.role == manager' \
dokploy/dokploy:latest
GREEN="\033[0;32m"
YELLOW="\033[1;33m"
BLUE="\033[0;34m"
NC="\033[0m" # No Color
format_ip_for_url() {
local ip="$1"
if echo "$ip" | grep -q ':'; then
# IPv6
echo "[${ip}]"
else
# IPv4
echo "${ip}"
fi
}
formatted_addr=$(format_ip_for_url "$advertise_addr")
echo ""
printf "${GREEN}Congratulations, Dokploy is installed!${NC}\n"
printf "${BLUE}Wait 15 seconds for the server to start${NC}\n"
printf "${YELLOW}Please go to http://${formatted_addr}:3000${NC}\n\n"
} }
advertise_addr=$(get_ip) update_dokploy() {
echo "Updating Dokploy..."
# Pull the latest image
docker pull dokploy/dokploy:latest
docker swarm init --advertise-addr $advertise_addr # Update the service
docker service update --image dokploy/dokploy:latest dokploy
echo "Swarm initialized" echo "Dokploy has been updated to the latest version."
docker network rm -f dokploy-network 2>/dev/null
docker network create --driver overlay --attachable dokploy-network
echo "Network created"
mkdir -p /etc/dokploy
chmod 777 /etc/dokploy
docker pull dokploy/dokploy:latest
# Installation
docker service create \
--name dokploy \
--replicas 1 \
--network dokploy-network \
--mount type=bind,source=/var/run/docker.sock,target=/var/run/docker.sock \
--mount type=bind,source=/etc/dokploy,target=/etc/dokploy \
--mount type=volume,source=dokploy-docker-config,target=/root/.docker \
--publish published=3000,target=3000,mode=host \
--update-parallelism 1 \
--update-order stop-first \
--constraint 'node.role == manager' \
dokploy/dokploy:latest
GREEN="\033[0;32m"
YELLOW="\033[1;33m"
BLUE="\033[0;34m"
NC="\033[0m" # No Color
format_ip_for_url() {
local ip="$1"
if echo "$ip" | grep -q ':'; then
# IPv6
echo "[${ip}]"
else
# IPv4
echo "${ip}"
fi
} }
formatted_addr=$(format_ip_for_url "$advertise_addr") # Main script execution
echo "" if [ "$1" = "update" ]; then
printf "${GREEN}Congratulations, Dokploy is installed!${NC}\n" update_dokploy
printf "${BLUE}Wait 15 seconds for the server to start${NC}\n" else
printf "${YELLOW}Please go to http://${formatted_addr}:3000${NC}\n\n" install_dokploy
echo "" fi