diff --git a/.github/pull_request_template.md b/.github/pull_request_template.md index 58825f900..0b849afc0 100644 --- a/.github/pull_request_template.md +++ b/.github/pull_request_template.md @@ -6,16 +6,13 @@ Please describe in a short paragraph what this PR is about. Before submitting this PR, please make sure that: -- [ ] You created a dedicated branch based on the `canary` branch. -- [ ] You have read the suggestions in the CONTRIBUTING.md file https://github.com/Dokploy/dokploy/blob/canary/CONTRIBUTING.md#pull-request -- [ ] You have tested this PR in your local instance. +- [] You created a dedicated branch based on the `canary` branch. +- [] You have read the suggestions in the CONTRIBUTING.md file https://github.com/Dokploy/dokploy/blob/canary/CONTRIBUTING.md#pull-request +- [] You have tested this PR in your local instance. ## Issues related (if applicable) -Close automatically the related issues using the keywords: `closes #ISSUE_NUMBER`, `fixes #ISSUE_NUMBER`, `resolves #ISSUE_NUMBER` - -Example: `closes #123` +closes #123 ## Screenshots (if applicable) -If you include a video or screenshot, would be awesome so we can see the changes in action. \ No newline at end of file diff --git a/.github/sponsors/tuple.png b/.github/sponsors/tuple.png new file mode 100644 index 000000000..1d7be47ca Binary files /dev/null and b/.github/sponsors/tuple.png differ diff --git a/.github/workflows/dokploy.yml b/.github/workflows/dokploy.yml index afb4ba4d2..529cd8f7f 100644 --- a/.github/workflows/dokploy.yml +++ b/.github/workflows/dokploy.yml @@ -2,7 +2,7 @@ name: Dokploy Docker Build on: push: - branches: [main, canary] + branches: [main, canary, "fix/re-apply-database-migration-fix"] workflow_dispatch: env: diff --git a/README.md b/README.md index fb2ee82a5..8faf22a35 100644 --- a/README.md +++ b/README.md @@ -11,8 +11,25 @@
+ + +
+ Special thanks to: +
+
+ + Tuple's sponsorship image + + +### [Tuple, the premier screen sharing app for developers](https://tuple.app/dokploy) +[Available for MacOS & Windows](https://tuple.app/dokploy)
+ +
+ + Dokploy is a free, self-hostable Platform as a Service (PaaS) that simplifies the deployment and management of applications and databases. + ## ✨ Features Dokploy includes multiple features to make your life easier. diff --git a/apps/api/src/schema.ts b/apps/api/src/schema.ts index 609289bf7..f87d0ee32 100644 --- a/apps/api/src/schema.ts +++ b/apps/api/src/schema.ts @@ -3,8 +3,8 @@ import { z } from "zod"; export const deployJobSchema = z.discriminatedUnion("applicationType", [ z.object({ applicationId: z.string(), - titleLog: z.string(), - descriptionLog: z.string(), + titleLog: z.string().optional(), + descriptionLog: z.string().optional(), server: z.boolean().optional(), type: z.enum(["deploy", "redeploy"]), applicationType: z.literal("application"), @@ -12,8 +12,8 @@ export const deployJobSchema = z.discriminatedUnion("applicationType", [ }), z.object({ composeId: z.string(), - titleLog: z.string(), - descriptionLog: z.string(), + titleLog: z.string().optional(), + descriptionLog: z.string().optional(), server: z.boolean().optional(), type: z.enum(["deploy", "redeploy"]), applicationType: z.literal("compose"), @@ -22,8 +22,8 @@ export const deployJobSchema = z.discriminatedUnion("applicationType", [ z.object({ applicationId: z.string(), previewDeploymentId: z.string(), - titleLog: z.string(), - descriptionLog: z.string(), + titleLog: z.string().optional(), + descriptionLog: z.string().optional(), server: z.boolean().optional(), type: z.enum(["deploy"]), applicationType: z.literal("application-preview"), diff --git a/apps/api/src/utils.ts b/apps/api/src/utils.ts index ee3943d34..ee2ac3e50 100644 --- a/apps/api/src/utils.ts +++ b/apps/api/src/utils.ts @@ -18,14 +18,14 @@ export const deploy = async (job: DeployJob) => { if (job.type === "redeploy") { await rebuildRemoteApplication({ applicationId: job.applicationId, - titleLog: job.titleLog, - descriptionLog: job.descriptionLog, + titleLog: job.titleLog || "Rebuild deployment", + descriptionLog: job.descriptionLog || "", }); } else if (job.type === "deploy") { await deployRemoteApplication({ applicationId: job.applicationId, - titleLog: job.titleLog, - descriptionLog: job.descriptionLog, + titleLog: job.titleLog || "Manual deployment", + descriptionLog: job.descriptionLog || "", }); } } @@ -38,14 +38,14 @@ export const deploy = async (job: DeployJob) => { if (job.type === "redeploy") { await rebuildRemoteCompose({ composeId: job.composeId, - titleLog: job.titleLog, - descriptionLog: job.descriptionLog, + titleLog: job.titleLog || "Rebuild deployment", + descriptionLog: job.descriptionLog || "", }); } else if (job.type === "deploy") { await deployRemoteCompose({ composeId: job.composeId, - titleLog: job.titleLog, - descriptionLog: job.descriptionLog, + titleLog: job.titleLog || "Manual deployment", + descriptionLog: job.descriptionLog || "", }); } } @@ -57,8 +57,8 @@ export const deploy = async (job: DeployJob) => { if (job.type === "deploy") { await deployRemotePreviewApplication({ applicationId: job.applicationId, - titleLog: job.titleLog, - descriptionLog: job.descriptionLog, + titleLog: job.titleLog || "Preview Deployment", + descriptionLog: job.descriptionLog || "", previewDeploymentId: job.previewDeploymentId, }); } diff --git a/apps/dokploy/__test__/drop/drop.test.ts b/apps/dokploy/__test__/drop/drop.test.ts index a9bc178a2..b597b3aa4 100644 --- a/apps/dokploy/__test__/drop/drop.test.ts +++ b/apps/dokploy/__test__/drop/drop.test.ts @@ -56,13 +56,21 @@ const baseApp: ApplicationNested = { previewPort: 3000, previewLimit: 0, previewWildcard: "", - project: { + environment: { env: "", - organizationId: "", + environmentId: "", name: "", - description: "", createdAt: "", + description: "", projectId: "", + project: { + env: "", + organizationId: "", + name: "", + description: "", + createdAt: "", + projectId: "", + }, }, buildArgs: null, buildPath: "/", @@ -92,6 +100,7 @@ const baseApp: ApplicationNested = { dockerfile: null, dockerImage: null, dropBuildPath: null, + environmentId: "", enabled: null, env: null, healthCheckSwarm: null, @@ -106,7 +115,6 @@ const baseApp: ApplicationNested = { password: null, placementSwarm: null, ports: [], - projectId: "", publishDirectory: null, isStaticSpa: null, redirects: [], diff --git a/apps/dokploy/__test__/env/environment.test.ts b/apps/dokploy/__test__/env/environment.test.ts new file mode 100644 index 000000000..95d46dcc0 --- /dev/null +++ b/apps/dokploy/__test__/env/environment.test.ts @@ -0,0 +1,335 @@ +import { prepareEnvironmentVariables } from "@dokploy/server/index"; +import { describe, expect, it } from "vitest"; + +const projectEnv = ` +ENVIRONMENT=staging +DATABASE_URL=postgres://postgres:postgres@localhost:5432/project_db +PORT=3000 +`; + +const environmentEnv = ` +NODE_ENV=development +API_URL=https://api.dev.example.com +REDIS_URL=redis://localhost:6379 +DATABASE_NAME=dev_database +SECRET_KEY=env-secret-123 +`; + +describe("prepareEnvironmentVariables (environment variables)", () => { + it("resolves environment variables correctly", () => { + const serviceWithEnvVars = ` +NODE_ENV=\${{environment.NODE_ENV}} +API_URL=\${{environment.API_URL}} +SERVICE_PORT=4000 +`; + + const resolved = prepareEnvironmentVariables( + serviceWithEnvVars, + "", + environmentEnv, + ); + + expect(resolved).toEqual([ + "NODE_ENV=development", + "API_URL=https://api.dev.example.com", + "SERVICE_PORT=4000", + ]); + }); + + it("resolves both project and environment variables", () => { + const serviceWithBoth = ` +ENVIRONMENT=\${{project.ENVIRONMENT}} +NODE_ENV=\${{environment.NODE_ENV}} +API_URL=\${{environment.API_URL}} +DATABASE_URL=\${{project.DATABASE_URL}} +SERVICE_PORT=4000 +`; + + const resolved = prepareEnvironmentVariables( + serviceWithBoth, + projectEnv, + environmentEnv, + ); + + expect(resolved).toEqual([ + "ENVIRONMENT=staging", + "NODE_ENV=development", + "API_URL=https://api.dev.example.com", + "DATABASE_URL=postgres://postgres:postgres@localhost:5432/project_db", + "SERVICE_PORT=4000", + ]); + }); + + it("handles undefined environment variables", () => { + const serviceWithUndefined = ` +UNDEFINED_VAR=\${{environment.UNDEFINED_VAR}} +`; + + expect(() => + prepareEnvironmentVariables(serviceWithUndefined, "", environmentEnv), + ).toThrow("Invalid environment variable: environment.UNDEFINED_VAR"); + }); + + it("allows service variables to override environment variables", () => { + const serviceOverrideEnv = ` +NODE_ENV=production +API_URL=\${{environment.API_URL}} +`; + + const resolved = prepareEnvironmentVariables( + serviceOverrideEnv, + "", + environmentEnv, + ); + + expect(resolved).toEqual([ + "NODE_ENV=production", // Overrides environment variable + "API_URL=https://api.dev.example.com", + ]); + }); + + it("resolves complex references with project, environment, and service variables", () => { + const complexServiceEnv = ` +FULL_DATABASE_URL=\${{project.DATABASE_URL}}/\${{environment.DATABASE_NAME}} +API_ENDPOINT=\${{environment.API_URL}}/\${{project.ENVIRONMENT}}/api +SERVICE_NAME=my-service +COMPLEX_VAR=\${{SERVICE_NAME}}-\${{environment.NODE_ENV}}-\${{project.ENVIRONMENT}} +`; + + const resolved = prepareEnvironmentVariables( + complexServiceEnv, + projectEnv, + environmentEnv, + ); + + expect(resolved).toEqual([ + "FULL_DATABASE_URL=postgres://postgres:postgres@localhost:5432/project_db/dev_database", + "API_ENDPOINT=https://api.dev.example.com/staging/api", + "SERVICE_NAME=my-service", + "COMPLEX_VAR=my-service-development-staging", + ]); + }); + + it("handles environment variables with special characters", () => { + const specialEnvVars = ` +SPECIAL_URL=https://special.com +COMPLEX_KEY="key-with-@#$%^&*()" +JWT_SECRET="secret-with-spaces and symbols!@#" +`; + + const serviceWithSpecial = ` +FULL_URL=\${{environment.SPECIAL_URL}}/path?key=\${{environment.COMPLEX_KEY}} +AUTH_SECRET=\${{environment.JWT_SECRET}} +`; + + const resolved = prepareEnvironmentVariables( + serviceWithSpecial, + "", + specialEnvVars, + ); + + expect(resolved).toEqual([ + "FULL_URL=https://special.com/path?key=key-with-@#$%^&*()", + "AUTH_SECRET=secret-with-spaces and symbols!@#", + ]); + }); + + it("maintains precedence: service > environment > project", () => { + const conflictingProjectEnv = ` +NODE_ENV=production-project +API_URL=https://project.api.com +DATABASE_NAME=project_db +`; + + const conflictingEnvironmentEnv = ` +NODE_ENV=development-environment +API_URL=https://environment.api.com +DATABASE_NAME=env_db +`; + + const serviceWithConflicts = ` +NODE_ENV=service-override +PROJECT_ENV=\${{project.NODE_ENV}} +ENV_VAR=\${{environment.API_URL}} +DB_NAME=\${{environment.DATABASE_NAME}} +`; + + const resolved = prepareEnvironmentVariables( + serviceWithConflicts, + conflictingProjectEnv, + conflictingEnvironmentEnv, + ); + + expect(resolved).toEqual([ + "NODE_ENV=service-override", // Service wins + "PROJECT_ENV=production-project", // Project reference + "ENV_VAR=https://environment.api.com", // Environment reference + "DB_NAME=env_db", // Environment reference + ]); + }); + + it("handles empty environment variables", () => { + const serviceWithEmpty = ` +SERVICE_VAR=test +PROJECT_VAR=\${{project.ENVIRONMENT}} +`; + + const resolved = prepareEnvironmentVariables( + serviceWithEmpty, + projectEnv, + "", + ); + + expect(resolved).toEqual(["SERVICE_VAR=test", "PROJECT_VAR=staging"]); + }); + + it("handles mixed quotes and environment variables", () => { + const envWithQuotes = ` +QUOTED_VAR="development" +SINGLE_QUOTED='https://api.dev.example.com' +MIXED_VAR="value with 'single' quotes" +`; + + const serviceWithQuotes = ` +NODE_ENV=\${{environment.QUOTED_VAR}} +API_URL=\${{environment.SINGLE_QUOTED}} +COMPLEX="Prefix-\${{environment.MIXED_VAR}}-Suffix" +`; + + const resolved = prepareEnvironmentVariables( + serviceWithQuotes, + "", + envWithQuotes, + ); + + expect(resolved).toEqual([ + "NODE_ENV=development", + "API_URL=https://api.dev.example.com", + "COMPLEX=Prefix-value with 'single' quotes-Suffix", + ]); + }); + + it("resolves multiple environment references in single value", () => { + const multiRefEnv = ` +HOST=localhost +PORT=5432 +USERNAME=postgres +PASSWORD=secret123 +`; + + const serviceWithMultiRefs = ` +DATABASE_URL=postgresql://\${{environment.USERNAME}}:\${{environment.PASSWORD}}@\${{environment.HOST}}:\${{environment.PORT}}/mydb +CONNECTION_STRING=\${{environment.HOST}}:\${{environment.PORT}} +`; + + const resolved = prepareEnvironmentVariables( + serviceWithMultiRefs, + "", + multiRefEnv, + ); + + expect(resolved).toEqual([ + "DATABASE_URL=postgresql://postgres:secret123@localhost:5432/mydb", + "CONNECTION_STRING=localhost:5432", + ]); + }); + + it("handles nested references with environment and project variables", () => { + const nestedProjectEnv = ` +BASE_DOMAIN=example.com +PROTOCOL=https +`; + + const nestedEnvironmentEnv = ` +SUBDOMAIN=api.dev +PATH_PREFIX=/v1 +`; + + const serviceWithNested = ` +FULL_URL=\${{project.PROTOCOL}}://\${{environment.SUBDOMAIN}}.\${{project.BASE_DOMAIN}}\${{environment.PATH_PREFIX}}/endpoint +API_BASE=\${{project.PROTOCOL}}://\${{environment.SUBDOMAIN}}.\${{project.BASE_DOMAIN}} +`; + + const resolved = prepareEnvironmentVariables( + serviceWithNested, + nestedProjectEnv, + nestedEnvironmentEnv, + ); + + expect(resolved).toEqual([ + "FULL_URL=https://api.dev.example.com/v1/endpoint", + "API_BASE=https://api.dev.example.com", + ]); + }); + + it("throws error for malformed environment variable references", () => { + const serviceWithMalformed = ` +MALFORMED1=\${{environment.}} +MALFORMED2=\${{environment}} +VALID=\${{environment.NODE_ENV}} +`; + + // Should throw error for empty variable name after environment. + expect(() => + prepareEnvironmentVariables(serviceWithMalformed, "", environmentEnv), + ).toThrow("Invalid environment variable: environment."); + }); + + it("handles environment variables with numeric values", () => { + const numericEnv = ` +PORT=8080 +TIMEOUT=30 +RETRY_COUNT=3 +PERCENTAGE=99.5 +`; + + const serviceWithNumeric = ` +SERVER_PORT=\${{environment.PORT}} +REQUEST_TIMEOUT=\${{environment.TIMEOUT}} +MAX_RETRIES=\${{environment.RETRY_COUNT}} +SUCCESS_RATE=\${{environment.PERCENTAGE}} +`; + + const resolved = prepareEnvironmentVariables( + serviceWithNumeric, + "", + numericEnv, + ); + + expect(resolved).toEqual([ + "SERVER_PORT=8080", + "REQUEST_TIMEOUT=30", + "MAX_RETRIES=3", + "SUCCESS_RATE=99.5", + ]); + }); + + it("handles boolean-like environment variables", () => { + const booleanEnv = ` +DEBUG=true +ENABLED=false +PRODUCTION=1 +DEVELOPMENT=0 +`; + + const serviceWithBoolean = ` +DEBUG_MODE=\${{environment.DEBUG}} +FEATURE_ENABLED=\${{environment.ENABLED}} +IS_PROD=\${{environment.PRODUCTION}} +IS_DEV=\${{environment.DEVELOPMENT}} +`; + + const resolved = prepareEnvironmentVariables( + serviceWithBoolean, + "", + booleanEnv, + ); + + expect(resolved).toEqual([ + "DEBUG_MODE=true", + "FEATURE_ENABLED=false", + "IS_PROD=1", + "IS_DEV=0", + ]); + }); +}); diff --git a/apps/dokploy/__test__/traefik/traefik.test.ts b/apps/dokploy/__test__/traefik/traefik.test.ts index ff8a99620..5be96e473 100644 --- a/apps/dokploy/__test__/traefik/traefik.test.ts +++ b/apps/dokploy/__test__/traefik/traefik.test.ts @@ -36,13 +36,22 @@ const baseApp: ApplicationNested = { previewLimit: 0, previewCustomCertResolver: null, previewWildcard: "", - project: { + environmentId: "", + environment: { env: "", - organizationId: "", + environmentId: "", name: "", - description: "", createdAt: "", + description: "", projectId: "", + project: { + env: "", + organizationId: "", + name: "", + description: "", + createdAt: "", + projectId: "", + }, }, buildPath: "/", gitlabPathNamespace: "", @@ -85,7 +94,6 @@ const baseApp: ApplicationNested = { password: null, placementSwarm: null, ports: [], - projectId: "", publishDirectory: null, isStaticSpa: null, redirects: [], diff --git a/apps/dokploy/components/dashboard/application/deployments/show-deployments.tsx b/apps/dokploy/components/dashboard/application/deployments/show-deployments.tsx index 5efa9e5f2..13694a283 100644 --- a/apps/dokploy/components/dashboard/application/deployments/show-deployments.tsx +++ b/apps/dokploy/components/dashboard/application/deployments/show-deployments.tsx @@ -77,7 +77,7 @@ export const ShowDeployments = ({
Deployments - See all the 10 last deployments for this {type} + See the last 10 deployments for this {type}
@@ -104,7 +104,9 @@ export const ShowDeployments = ({ Webhook URL:
- {`${url}/api/deploy${type === "compose" ? "/compose" : ""}/${refreshToken}`} + {`${url}/api/deploy${ + type === "compose" ? "/compose" : "" + }/${refreshToken}`} {(type === "application" || type === "compose") && ( diff --git a/apps/dokploy/components/dashboard/application/general/show.tsx b/apps/dokploy/components/dashboard/application/general/show.tsx index a8fef349b..5387659ad 100644 --- a/apps/dokploy/components/dashboard/application/general/show.tsx +++ b/apps/dokploy/components/dashboard/application/general/show.tsx @@ -69,7 +69,7 @@ export const ShowGeneralApplication = ({ applicationId }: Props) => { toast.success("Application deployed successfully"); refetch(); router.push( - `/dashboard/project/${data?.projectId}/services/application/${applicationId}?tab=deployments`, + `/dashboard/project/${data?.environment.projectId}/environment/${data?.environmentId}/services/application/${applicationId}?tab=deployments`, ); }) .catch(() => { diff --git a/apps/dokploy/components/dashboard/application/schedules/show-schedules.tsx b/apps/dokploy/components/dashboard/application/schedules/show-schedules.tsx index 3ebc76def..3209b6e03 100644 --- a/apps/dokploy/components/dashboard/application/schedules/show-schedules.tsx +++ b/apps/dokploy/components/dashboard/application/schedules/show-schedules.tsx @@ -58,7 +58,7 @@ export const ShowSchedules = ({ id, scheduleType = "application" }: Props) => { return ( -
+
Scheduled Tasks @@ -91,15 +91,15 @@ export const ShowSchedules = ({ id, scheduleType = "application" }: Props) => { return (
-
+
-
-

+
+

{schedule.name}

{ {schedule.enabled ? "Enabled" : "Disabled"}
-
+
{
-
+
{ })}
) : ( -
+

No scheduled tasks diff --git a/apps/dokploy/components/dashboard/compose/delete-service.tsx b/apps/dokploy/components/dashboard/compose/delete-service.tsx index 438af954a..e75aad5e5 100644 --- a/apps/dokploy/components/dashboard/compose/delete-service.tsx +++ b/apps/dokploy/components/dashboard/compose/delete-service.tsx @@ -101,7 +101,9 @@ export const DeleteService = ({ id, type }: Props) => { deleteVolumes, }) .then((result) => { - push(`/dashboard/project/${result?.projectId}`); + push( + `/dashboard/project/${result?.environment?.projectId}/environment/${result?.environment?.environmentId}`, + ); toast.success("deleted successfully"); setIsOpen(false); }) diff --git a/apps/dokploy/components/dashboard/compose/general/actions.tsx b/apps/dokploy/components/dashboard/compose/general/actions.tsx index 29a9f9be3..870444be7 100644 --- a/apps/dokploy/components/dashboard/compose/general/actions.tsx +++ b/apps/dokploy/components/dashboard/compose/general/actions.tsx @@ -47,7 +47,7 @@ export const ComposeActions = ({ composeId }: Props) => { toast.success("Compose deployed successfully"); refetch(); router.push( - `/dashboard/project/${data?.project.projectId}/services/compose/${composeId}?tab=deployments`, + `/dashboard/project/${data?.environment.projectId}/environment/${data?.environmentId}/services/compose/${composeId}?tab=deployments`, ); }) .catch(() => { diff --git a/apps/dokploy/components/dashboard/project/add-ai-assistant.tsx b/apps/dokploy/components/dashboard/project/add-ai-assistant.tsx index 2bb47618e..88fd1d111 100644 --- a/apps/dokploy/components/dashboard/project/add-ai-assistant.tsx +++ b/apps/dokploy/components/dashboard/project/add-ai-assistant.tsx @@ -1,10 +1,10 @@ import { TemplateGenerator } from "@/components/dashboard/project/ai/template-generator"; interface Props { - projectId: string; + environmentId: string; projectName?: string; } -export const AddAiAssistant = ({ projectId }: Props) => { - return ; +export const AddAiAssistant = ({ environmentId }: Props) => { + return ; }; diff --git a/apps/dokploy/components/dashboard/project/add-application.tsx b/apps/dokploy/components/dashboard/project/add-application.tsx index 137f75a51..079701eb8 100644 --- a/apps/dokploy/components/dashboard/project/add-application.tsx +++ b/apps/dokploy/components/dashboard/project/add-application.tsx @@ -64,11 +64,11 @@ const AddTemplateSchema = z.object({ type AddTemplate = z.infer; interface Props { - projectId: string; + environmentId: string; projectName?: string; } -export const AddApplication = ({ projectId, projectName }: Props) => { +export const AddApplication = ({ environmentId, projectName }: Props) => { const utils = api.useUtils(); const { data: isCloud } = api.settings.isCloud.useQuery(); const [visible, setVisible] = useState(false); @@ -76,6 +76,10 @@ export const AddApplication = ({ projectId, projectName }: Props) => { const { data: servers } = api.server.withSSHKey.useQuery(); const hasServers = servers && servers.length > 0; + // Show dropdown logic based on cloud environment + // Cloud: show only if there are remote servers (no Dokploy option) + // Self-hosted: show only if there are remote servers (Dokploy is default, hide if no remote servers) + const shouldShowServerDropdown = hasServers; const { mutateAsync, isLoading, error, isError } = api.application.create.useMutation(); @@ -94,15 +98,15 @@ export const AddApplication = ({ projectId, projectName }: Props) => { name: data.name, appName: data.appName, description: data.description, - projectId, - serverId: data.serverId, + serverId: data.serverId === "dokploy" ? undefined : data.serverId, + environmentId, }) .then(async () => { toast.success("Service Created"); form.reset(); setVisible(false); - await utils.project.one.invalidate({ - projectId, + await utils.environment.one.invalidate({ + environmentId, }); }) .catch(() => { @@ -157,7 +161,7 @@ export const AddApplication = ({ projectId, projectName }: Props) => { )} /> - {hasServers && ( + {shouldShowServerDropdown && ( { diff --git a/apps/dokploy/components/dashboard/project/add-compose.tsx b/apps/dokploy/components/dashboard/project/add-compose.tsx index c32e55c16..a187104ec 100644 --- a/apps/dokploy/components/dashboard/project/add-compose.tsx +++ b/apps/dokploy/components/dashboard/project/add-compose.tsx @@ -65,11 +65,11 @@ const AddComposeSchema = z.object({ type AddCompose = z.infer; interface Props { - projectId: string; + environmentId: string; projectName?: string; } -export const AddCompose = ({ projectId, projectName }: Props) => { +export const AddCompose = ({ environmentId, projectName }: Props) => { const utils = api.useUtils(); const [visible, setVisible] = useState(false); const slug = slugify(projectName); @@ -78,7 +78,14 @@ export const AddCompose = ({ projectId, projectName }: Props) => { const { mutateAsync, isLoading, error, isError } = api.compose.create.useMutation(); + // Get environment data to extract projectId + const { data: environment } = api.environment.one.useQuery({ environmentId }); + const hasServers = servers && servers.length > 0; + // Show dropdown logic based on cloud environment + // Cloud: show only if there are remote servers (no Dokploy option) + // Self-hosted: show only if there are remote servers (Dokploy is default, hide if no remote servers) + const shouldShowServerDropdown = hasServers; const form = useForm({ defaultValues: { @@ -98,16 +105,17 @@ export const AddCompose = ({ projectId, projectName }: Props) => { await mutateAsync({ name: data.name, description: data.description, - projectId, + environmentId, composeType: data.composeType, appName: data.appName, - serverId: data.serverId, + serverId: data.serverId === "dokploy" ? undefined : data.serverId, }) .then(async () => { toast.success("Compose Created"); setVisible(false); - await utils.project.one.invalidate({ - projectId, + // Invalidate the project query to refresh the environment data + await utils.environment.one.invalidate({ + environmentId, }); }) .catch(() => { @@ -165,7 +173,7 @@ export const AddCompose = ({ projectId, projectName }: Props) => { )} />

- {hasServers && ( + {shouldShowServerDropdown && ( { diff --git a/apps/dokploy/components/dashboard/project/add-database.tsx b/apps/dokploy/components/dashboard/project/add-database.tsx index 6b07baa81..064e93544 100644 --- a/apps/dokploy/components/dashboard/project/add-database.tsx +++ b/apps/dokploy/components/dashboard/project/add-database.tsx @@ -83,7 +83,12 @@ const baseDatabaseSchema = z.object({ message: "App name supports lowercase letters, numbers, '-' and can only start and end letters, and does not support continuous '-'", }), - databasePassword: z.string(), + databasePassword: z + .string() + .regex(/^[a-zA-Z0-9@#%^&*()_+\-=[\]{}|;:,.<>?~`]*$/, { + message: + "Password contains invalid characters. Please avoid: $ ! ' \" \\ / and space characters for database compatibility", + }), dockerImage: z.string(), description: z.string().nullable(), serverId: z.string().nullable(), @@ -112,7 +117,13 @@ const mySchema = z.discriminatedUnion("type", [ z .object({ type: z.literal("mysql"), - databaseRootPassword: z.string().default(""), + databaseRootPassword: z + .string() + .regex(/^[a-zA-Z0-9@#%^&*()_+\-=[\]{}|;:,.<>?~`]*$/, { + message: + "Password contains invalid characters. Please avoid: $ ! ' \" \\ / and space characters for database compatibility", + }) + .optional(), databaseUser: z.string().default("mysql"), databaseName: z.string().default("mysql"), }) @@ -121,7 +132,13 @@ const mySchema = z.discriminatedUnion("type", [ .object({ type: z.literal("mariadb"), dockerImage: z.string().default("mariadb:4"), - databaseRootPassword: z.string().default(""), + databaseRootPassword: z + .string() + .regex(/^[a-zA-Z0-9@#%^&*()_+\-=[\]{}|;:,.<>?~`]*$/, { + message: + "Password contains invalid characters. Please avoid: $ ! ' \" \\ / and space characters for database compatibility", + }) + .optional(), databaseUser: z.string().default("mariadb"), databaseName: z.string().default("mariadb"), }) @@ -154,14 +171,15 @@ const databasesMap = { type AddDatabase = z.infer; interface Props { - projectId: string; + environmentId: string; projectName?: string; } -export const AddDatabase = ({ projectId, projectName }: Props) => { +export const AddDatabase = ({ environmentId, projectName }: Props) => { const utils = api.useUtils(); const [visible, setVisible] = useState(false); const slug = slugify(projectName); + const { data: isCloud } = api.settings.isCloud.useQuery(); const { data: servers } = api.server.withSSHKey.useQuery(); const postgresMutation = api.postgres.create.useMutation(); const mongoMutation = api.mongo.create.useMutation(); @@ -169,7 +187,14 @@ export const AddDatabase = ({ projectId, projectName }: Props) => { const mariadbMutation = api.mariadb.create.useMutation(); const mysqlMutation = api.mysql.create.useMutation(); + // Get environment data to extract projectId + const { data: environment } = api.environment.one.useQuery({ environmentId }); + const hasServers = servers && servers.length > 0; + // Show dropdown logic based on cloud environment + // Cloud: show only if there are remote servers (no Dokploy option) + // Self-hosted: show only if there are remote servers (Dokploy is default, hide if no remote servers) + const shouldShowServerDropdown = hasServers; const form = useForm({ defaultValues: { @@ -203,8 +228,8 @@ export const AddDatabase = ({ projectId, projectName }: Props) => { name: data.name, appName: data.appName, dockerImage: defaultDockerImage, - projectId, - serverId: data.serverId, + serverId: data.serverId === "dokploy" ? undefined : data.serverId, + environmentId, description: data.description, }; @@ -216,7 +241,7 @@ export const AddDatabase = ({ projectId, projectName }: Props) => { databaseUser: data.databaseUser || databasesUserDefaultPlaceholder[data.type], - serverId: data.serverId, + serverId: data.serverId === "dokploy" ? null : data.serverId, }); } else if (data.type === "mongo") { promise = mongoMutation.mutateAsync({ @@ -224,25 +249,24 @@ export const AddDatabase = ({ projectId, projectName }: Props) => { databasePassword: data.databasePassword, databaseUser: data.databaseUser || databasesUserDefaultPlaceholder[data.type], - serverId: data.serverId, + serverId: data.serverId === "dokploy" ? null : data.serverId, replicaSets: data.replicaSets, }); } else if (data.type === "redis") { promise = redisMutation.mutateAsync({ ...commonParams, databasePassword: data.databasePassword, - serverId: data.serverId, - projectId, + serverId: data.serverId === "dokploy" ? null : data.serverId, }); } else if (data.type === "mariadb") { promise = mariadbMutation.mutateAsync({ ...commonParams, databasePassword: data.databasePassword, - databaseRootPassword: data.databaseRootPassword, + databaseRootPassword: data.databaseRootPassword || "", databaseName: data.databaseName || "mariadb", databaseUser: data.databaseUser || databasesUserDefaultPlaceholder[data.type], - serverId: data.serverId, + serverId: data.serverId === "dokploy" ? null : data.serverId, }); } else if (data.type === "mysql") { promise = mysqlMutation.mutateAsync({ @@ -251,8 +275,8 @@ export const AddDatabase = ({ projectId, projectName }: Props) => { databaseName: data.databaseName || "mysql", databaseUser: data.databaseUser || databasesUserDefaultPlaceholder[data.type], - databaseRootPassword: data.databaseRootPassword, - serverId: data.serverId, + serverId: data.serverId === "dokploy" ? null : data.serverId, + databaseRootPassword: data.databaseRootPassword || "", }); } @@ -271,8 +295,9 @@ export const AddDatabase = ({ projectId, projectName }: Props) => { databaseUser: "", }); setVisible(false); - await utils.project.one.invalidate({ - projectId, + // Invalidate the project query to refresh the environment data + await utils.environment.one.invalidate({ + environmentId, }); }) .catch(() => { @@ -382,7 +407,7 @@ export const AddDatabase = ({ projectId, projectName }: Props) => { )} /> - {hasServers && ( + {shouldShowServerDropdown && ( { Select a Server setName(e.target.value)} + placeholder="Environment name" + /> +
+
+ +