{
+ const ip = data?.server?.ipAddress || serverIp;
+ if (ip) {
+ copy(ip);
+ toast.success("IP Address Copied!");
+ }
+ }}
variant={
!data?.serverId
? "default"
diff --git a/apps/dokploy/public/llms.txt b/apps/dokploy/public/llms.txt
new file mode 100644
index 0000000000..64c18a02ba
--- /dev/null
+++ b/apps/dokploy/public/llms.txt
@@ -0,0 +1,41 @@
+# Dokploy
+
+> Dokploy is a free, open-source, self-hostable Platform as a Service (PaaS) that simplifies deploying and managing applications, databases, and services on your own servers. It is an open-source alternative to Heroku, Vercel, and Netlify, built on Docker and Traefik, with support for applications, managed databases, Docker Compose, Docker Swarm clusters, automated backups, real-time monitoring, templates, and a full CLI and REST API.
+
+This is a self-hosted Dokploy instance. The complete, always up-to-date documentation index for AI tools is auto-generated and maintained at docs.dokploy.com/llms.txt — prefer that file as the authoritative source for documentation links.
+
+## Documentation
+
+- [Documentation index (llms.txt)](https://docs.dokploy.com/llms.txt): Authoritative, auto-generated machine-readable index of all documentation pages.
+- [Full documentation (llms-full.txt)](https://docs.dokploy.com/llms-full.txt): All documentation pages as plain text.
+- [OpenAPI specification](https://docs.dokploy.com/openapi.json): Complete REST API reference in OpenAPI format.
+- [Introduction](https://docs.dokploy.com/docs/core): Welcome to Dokploy — overview and getting started.
+- [Features](https://docs.dokploy.com/docs/core/features): The full suite of features available in Dokploy.
+- [Architecture](https://docs.dokploy.com/docs/core/architecture): Core architecture components of Dokploy.
+- [Installation](https://docs.dokploy.com/docs/core/installation): Install Dokploy on a server in minutes.
+- [Applications](https://docs.dokploy.com/docs/core/applications): Deploy applications from Git, Docker, Nixpacks, and Buildpacks.
+- [Databases](https://docs.dokploy.com/docs/core/databases): Create and manage MySQL, PostgreSQL, MongoDB, MariaDB, and Redis.
+- [Docker Compose](https://docs.dokploy.com/docs/core/docker-compose): Deploy multi-service applications with Docker Compose.
+- [Backups](https://docs.dokploy.com/docs/core/backups): Schedule and manage backups to S3-compatible storage.
+- [Monitoring](https://docs.dokploy.com/docs/core/monitoring): Real-time monitoring of applications, databases, and servers.
+- [Cluster (Docker Swarm)](https://docs.dokploy.com/docs/core/cluster): Scale across multiple nodes with Docker Swarm.
+- [Schedule Jobs](https://docs.dokploy.com/docs/core/schedule-jobs): Run cron-style scheduled tasks.
+- [CLI](https://docs.dokploy.com/docs/cli): Manage Dokploy from the command line.
+- [API Reference](https://docs.dokploy.com/docs/api): Interact with the Dokploy REST API.
+
+## Product
+
+- [Homepage](https://dokploy.com): Product overview.
+- [Pricing](https://dokploy.com/pricing): Plans and pricing.
+- [Enterprise](https://dokploy.com/enterprise): Enterprise features and offering.
+- [Self-Hosted PaaS](https://dokploy.com/self-hosted-paas): Self-hosted Platform as a Service overview.
+- [Dokploy Cloud](https://docs.dokploy.com/docs/core/cloud): Managed, hosted Dokploy.
+- [Comparison](https://dokploy.com/comparison): How Dokploy compares to CapRover, Coolify, Dokku, and Portainer.
+- [Blog](https://dokploy.com/blog): Articles, guides, and announcements.
+- [Changelog](https://dokploy.com/changelog): Product updates and release notes.
+
+## Optional
+
+- [Contact](https://dokploy.com/contact): Get in touch with the Dokploy team.
+- [GitHub Repository](https://github.com/Dokploy/dokploy): Source code, issues, and contribution guidelines.
+- [Discord Community](https://discord.gg/2tBnJ3jDJc): Community help, feedback, and discussions.
diff --git a/apps/dokploy/scripts/migrate-auth-secret.ts b/apps/dokploy/scripts/migrate-auth-secret.ts
index 5a71678d9a..302612c6d8 100644
--- a/apps/dokploy/scripts/migrate-auth-secret.ts
+++ b/apps/dokploy/scripts/migrate-auth-secret.ts
@@ -46,7 +46,7 @@ async function main() {
if (records.length === 0) {
console.log("✅ No 2FA records found, nothing to migrate.");
- return;
+ process.exit(0);
}
console.log(`📦 Found ${records.length} 2FA record(s) to migrate.`);
diff --git a/apps/dokploy/server/api/routers/compose.ts b/apps/dokploy/server/api/routers/compose.ts
index 2548184781..d1fbb14d3a 100644
--- a/apps/dokploy/server/api/routers/compose.ts
+++ b/apps/dokploy/server/api/routers/compose.ts
@@ -862,6 +862,76 @@ export const composeRouter = createTRPCRouter({
}
}),
+ previewTemplate: protectedProcedure
+ .input(
+ z.object({
+ base64: z.string(),
+ appName: z.string(),
+ serverId: z.string().optional(),
+ }),
+ )
+ .mutation(async ({ input, ctx }) => {
+ try {
+ if (input.serverId) {
+ const accessibleIds = await getAccessibleServerIds(ctx.session);
+ if (!accessibleIds.has(input.serverId)) {
+ throw new TRPCError({
+ code: "UNAUTHORIZED",
+ message: "You are not authorized to access this server",
+ });
+ }
+ }
+
+ const decodedData = Buffer.from(input.base64, "base64").toString(
+ "utf-8",
+ );
+
+ let serverIp = "127.0.0.1";
+
+ if (input.serverId) {
+ const server = await findServerById(input.serverId);
+ serverIp = server.ipAddress;
+ } else if (process.env.NODE_ENV !== "development") {
+ const settings = await getWebServerSettings();
+ serverIp = settings?.serverIp || "127.0.0.1";
+ }
+
+ const templateData = JSON.parse(decodedData);
+ const config = parse(templateData.config) as CompleteTemplate;
+
+ if (!templateData.compose || !config) {
+ throw new TRPCError({
+ code: "BAD_REQUEST",
+ message:
+ "Invalid template format. Must contain compose and config fields",
+ });
+ }
+
+ const configModified = {
+ ...config,
+ variables: {
+ APP_NAME: input.appName,
+ ...config.variables,
+ },
+ };
+
+ const processedTemplate = processTemplate(configModified, {
+ serverIp,
+ projectName: input.appName,
+ });
+
+ return {
+ compose: templateData.compose,
+ template: processedTemplate,
+ };
+ } catch (error) {
+ throw new TRPCError({
+ code: "BAD_REQUEST",
+ message: `Error processing template: ${error instanceof Error ? error.message : error}`,
+ });
+ }
+ }),
+
import: protectedProcedure
.input(
z.object({
diff --git a/apps/dokploy/server/api/routers/deployment.ts b/apps/dokploy/server/api/routers/deployment.ts
index 6f3b1d1ae7..d17a04dfb6 100644
--- a/apps/dokploy/server/api/routers/deployment.ts
+++ b/apps/dokploy/server/api/routers/deployment.ts
@@ -151,6 +151,14 @@ export const deploymentRouter = createTRPCRouter({
await checkServicePermissionAndAccess(ctx, serviceId, {
deployment: ["cancel"],
});
+ } else if (deployment.schedule?.serverId) {
+ const targetServer = await findServerById(deployment.schedule.serverId);
+ if (targetServer.organizationId !== ctx.session.activeOrganizationId) {
+ throw new TRPCError({
+ code: "UNAUTHORIZED",
+ message: "You don't have access to this deployment.",
+ });
+ }
}
if (!deployment.pid) {
@@ -188,6 +196,14 @@ export const deploymentRouter = createTRPCRouter({
await checkServicePermissionAndAccess(ctx, serviceId, {
deployment: ["cancel"],
});
+ } else if (deployment.schedule?.serverId) {
+ const targetServer = await findServerById(deployment.schedule.serverId);
+ if (targetServer.organizationId !== ctx.session.activeOrganizationId) {
+ throw new TRPCError({
+ code: "UNAUTHORIZED",
+ message: "You don't have access to this deployment.",
+ });
+ }
}
const result = await removeDeployment(input.deploymentId);
await audit(ctx, {
@@ -197,4 +213,47 @@ export const deploymentRouter = createTRPCRouter({
});
return result;
}),
+
+ readLogs: protectedProcedure
+ .input(
+ z.object({
+ deploymentId: z.string().min(1),
+ tail: z.number().int().min(1).max(10000).default(100),
+ }),
+ )
+ .query(async ({ input, ctx }) => {
+ const deployment = await findDeploymentById(input.deploymentId);
+ const serviceId = deployment.applicationId || deployment.composeId;
+ if (serviceId) {
+ await checkServicePermissionAndAccess(ctx, serviceId, {
+ deployment: ["read"],
+ });
+ } else if (deployment.schedule?.serverId) {
+ const targetServer = await findServerById(deployment.schedule.serverId);
+ if (targetServer.organizationId !== ctx.session.activeOrganizationId) {
+ throw new TRPCError({
+ code: "UNAUTHORIZED",
+ message: "You don't have access to this deployment.",
+ });
+ }
+ }
+
+ if (!deployment.logPath) {
+ return "";
+ }
+
+ const command = `tail -n ${input.tail} "${deployment.logPath}" 2>/dev/null || echo ""`;
+ const serverId = deployment.serverId || deployment.schedule?.serverId;
+ if (serverId) {
+ const { stdout } = await execAsyncRemote(serverId, command);
+ return stdout;
+ }
+
+ if (IS_CLOUD) {
+ return "";
+ }
+
+ const { stdout } = await execAsync(command);
+ return stdout;
+ }),
});
diff --git a/apps/dokploy/server/api/routers/organization.ts b/apps/dokploy/server/api/routers/organization.ts
index 51c1fec5d9..6af018ed81 100644
--- a/apps/dokploy/server/api/routers/organization.ts
+++ b/apps/dokploy/server/api/routers/organization.ts
@@ -295,6 +295,14 @@ export const organizationRouter = createTRPCRouter({
});
}
+ // Owner role is non-delegable — no one can invite as owner
+ if (input.role === "owner") {
+ throw new TRPCError({
+ code: "FORBIDDEN",
+ message: "Cannot invite a user with the owner role",
+ });
+ }
+
// If assigning a custom role, verify it exists
if (!["owner", "admin", "member"].includes(input.role)) {
const customRole = await db.query.organizationRole.findFirst({
diff --git a/apps/dokploy/server/api/routers/user.ts b/apps/dokploy/server/api/routers/user.ts
index 93b7e6cf62..fc3b29d6e4 100644
--- a/apps/dokploy/server/api/routers/user.ts
+++ b/apps/dokploy/server/api/routers/user.ts
@@ -23,6 +23,7 @@ import {
apiUpdateUser,
invitation,
member,
+ session,
user,
} from "@dokploy/server/db/schema";
import {
@@ -32,7 +33,7 @@ import {
import { hasValidLicense } from "@dokploy/server/services/proprietary/license-key";
import { TRPCError } from "@trpc/server";
import * as bcrypt from "bcrypt";
-import { and, asc, eq, gt } from "drizzle-orm";
+import { and, asc, eq, gt, ne } from "drizzle-orm";
import { z } from "zod";
import { audit } from "@/server/api/utils/audit";
import {
@@ -229,6 +230,15 @@ export const userRouter = createTRPCRouter({
password: bcrypt.hashSync(input.password, 10),
})
.where(eq(account.userId, ctx.user.id));
+
+ await db
+ .delete(session)
+ .where(
+ and(
+ eq(session.userId, ctx.user.id),
+ ne(session.id, ctx.session.id),
+ ),
+ );
}
try {
@@ -594,6 +604,13 @@ export const userRouter = createTRPCRouter({
});
}
+ if (input.role === "owner") {
+ throw new TRPCError({
+ code: "FORBIDDEN",
+ message: "Cannot create a user with the owner role",
+ });
+ }
+
return await createOrganizationUserWithCredentials({
organizationId: ctx.session.activeOrganizationId,
email: input.email,
diff --git a/packages/server/src/db/schema/registry.ts b/packages/server/src/db/schema/registry.ts
index 68db88f802..02b2f4c558 100644
--- a/packages/server/src/db/schema/registry.ts
+++ b/packages/server/src/db/schema/registry.ts
@@ -44,6 +44,13 @@ export const registryRelations = relations(registry, ({ many }) => ({
}),
}));
+// Image references require a lowercase namespace (e.g. Docker Hub username).
+const registryUsernameSchema = z
+ .string()
+ .trim()
+ .min(1)
+ .transform((s) => s.toLowerCase());
+
// Registry URLs must be hostname[:port] only — no shell metacharacters
// Empty string is allowed (means default/Docker Hub registry)
const registryUrlSchema = z
@@ -57,7 +64,7 @@ const registryUrlSchema = z
const createSchema = createInsertSchema(registry, {
registryName: z.string().min(1),
- username: z.string().min(1),
+ username: registryUsernameSchema,
password: z.string().min(1),
registryUrl: registryUrlSchema,
organizationId: z.string().min(1),
@@ -70,7 +77,7 @@ export const apiCreateRegistry = createSchema
.pick({})
.extend({
registryName: z.string().min(1),
- username: z.string().min(1),
+ username: registryUsernameSchema,
password: z.string().min(1),
registryUrl: registryUrlSchema,
registryType: z.enum(["cloud"]),
@@ -83,7 +90,7 @@ export const apiCreateRegistry = createSchema
export const apiTestRegistry = createSchema.pick({}).extend({
registryName: z.string().optional(),
- username: z.string().min(1),
+ username: registryUsernameSchema,
password: z.string().min(1),
registryUrl: registryUrlSchema,
registryType: z.enum(["cloud"]),
diff --git a/packages/server/src/utils/cluster/upload.ts b/packages/server/src/utils/cluster/upload.ts
index aa014a05c8..6bf02547c9 100644
--- a/packages/server/src/utils/cluster/upload.ts
+++ b/packages/server/src/utils/cluster/upload.ts
@@ -101,8 +101,8 @@ export const getRegistryTag = (registry: Registry, imageName: string) => {
// Extract the repository name (last part after '/')
const repositoryName = extractRepositoryName(imageName);
- // Build the final tag using registry's username/prefix
- const targetPrefix = imagePrefix || username;
+ // Build the final tag using registry's username/prefix (must be lowercase for valid image refs)
+ const targetPrefix = (imagePrefix || username).toLowerCase();
const finalRegistry = registryUrl || "";
return finalRegistry
diff --git a/packages/server/src/utils/docker/domain.ts b/packages/server/src/utils/docker/domain.ts
index 1a6e77a287..8094f1df2a 100644
--- a/packages/server/src/utils/docker/domain.ts
+++ b/packages/server/src/utils/docker/domain.ts
@@ -337,6 +337,10 @@ export const createDomainLabels = (
labels.push(
`traefik.http.routers.${routerName}.tls.certresolver=${customCertResolver}`,
);
+ } else if (certificateType === "none" && https) {
+ // No cert resolver, but HTTPS is enabled (default/custom certificate):
+ // explicitly enable TLS so Traefik serves the router over HTTPS.
+ labels.push(`traefik.http.routers.${routerName}.tls=true`);
}
}
diff --git a/packages/server/src/utils/watch-paths/should-deploy.ts b/packages/server/src/utils/watch-paths/should-deploy.ts
index 4bc1de1d16..c3e7677fed 100644
--- a/packages/server/src/utils/watch-paths/should-deploy.ts
+++ b/packages/server/src/utils/watch-paths/should-deploy.ts
@@ -2,8 +2,11 @@ import micromatch from "micromatch";
export const shouldDeploy = (
watchPaths: string[] | null,
- modifiedFiles: string[],
+ modifiedFiles: (string | null | undefined)[] | null | undefined,
): boolean => {
if (!watchPaths || watchPaths?.length === 0) return true;
- return micromatch.some(modifiedFiles, watchPaths);
+ const files = (modifiedFiles ?? []).filter(
+ (file): file is string => typeof file === "string",
+ );
+ return micromatch.some(files, watchPaths);
};
diff --git a/packages/server/src/wss/utils.ts b/packages/server/src/wss/utils.ts
index bce5aa245a..ec590399d3 100644
--- a/packages/server/src/wss/utils.ts
+++ b/packages/server/src/wss/utils.ts
@@ -40,7 +40,7 @@ export const readValidDirectory = (
directory: string,
serverId?: string | null,
) => {
- if (!/^[\w/. :-]{1,500}$/.test(directory)) {
+ if (!/^[\w/. :[\]-]{1,500}$/.test(directory)) {
return false;
}