@@ -59,4 +73,68 @@ export const ShowInternalPostgresCredentials = ({ postgresId }: Props) => {
>
);
};
-// ReplyError: MISCONF Redis is configured to save RDB snapshots, but it's currently unable to persist to disk. Commands that may modify the data set are disabled, because this instance is configured to report errors during writes if RDB snapshotting fails (stop-w
+
+const UpdatePassword = ({ postgresId }: { postgresId: string }) => {
+ const [open, setOpen] = useState(false);
+ const [password, setPassword] = useState("");
+ const utils = api.useUtils();
+
+ const { mutateAsync, isLoading } = api.postgres.changePassword.useMutation();
+
+ const onSave = async () => {
+ if (!password) {
+ toast.error("Password is required");
+ return;
+ }
+ try {
+ await mutateAsync({
+ postgresId,
+ databasePassword: password,
+ });
+ await utils.postgres.one.invalidate({ postgresId });
+ toast.success("Password updated successfully");
+ setOpen(false);
+ setPassword("");
+ } catch (error) {
+ toast.error("Error updating password");
+ }
+ };
+
+ return (
+
+ );
+};
diff --git a/apps/dokploy/server/api/routers/postgres.ts b/apps/dokploy/server/api/routers/postgres.ts
index e1718bff12..219cf974cd 100644
--- a/apps/dokploy/server/api/routers/postgres.ts
+++ b/apps/dokploy/server/api/routers/postgres.ts
@@ -1,5 +1,6 @@
import {
addNewService,
+ changePostgresPassword,
checkServiceAccess,
createMount,
createPostgres,
@@ -26,6 +27,7 @@ import { z } from "zod";
import { createTRPCRouter, protectedProcedure } from "@/server/api/trpc";
import { db } from "@/server/db";
import {
+ apiChangePostgresPassword,
apiChangePostgresStatus,
apiCreatePostgres,
apiDeployPostgres,
@@ -454,4 +456,22 @@ export const postgresRouter = createTRPCRouter({
return true;
}),
+ changePassword: protectedProcedure
+ .input(apiChangePostgresPassword)
+ .mutation(async ({ input, ctx }) => {
+ const postgres = await findPostgresById(input.postgresId);
+ if (
+ postgres.environment.project.organizationId !==
+ ctx.session.activeOrganizationId
+ ) {
+ throw new TRPCError({
+ code: "UNAUTHORIZED",
+ message: "You are not authorized to change this Postgres password",
+ });
+ }
+ return await changePostgresPassword(
+ input.postgresId,
+ input.databasePassword,
+ );
+ }),
});
diff --git a/packages/server/src/db/schema/postgres.ts b/packages/server/src/db/schema/postgres.ts
index d4ad7ad83d..3d5e0515ca 100644
--- a/packages/server/src/db/schema/postgres.ts
+++ b/packages/server/src/db/schema/postgres.ts
@@ -194,3 +194,10 @@ export const apiRebuildPostgres = createSchema
postgresId: true,
})
.required();
+
+export const apiChangePostgresPassword = createSchema
+ .pick({
+ postgresId: true,
+ databasePassword: true,
+ })
+ .required();
diff --git a/packages/server/src/services/postgres.ts b/packages/server/src/services/postgres.ts
index c926a55263..dcac07c51c 100644
--- a/packages/server/src/services/postgres.ts
+++ b/packages/server/src/services/postgres.ts
@@ -1,16 +1,23 @@
import { db } from "@dokploy/server/db";
import {
type apiCreatePostgres,
+ applications,
backups,
buildAppName,
+ environments,
postgres,
+ projects,
} from "@dokploy/server/db/schema";
import { generatePassword } from "@dokploy/server/templates";
import { buildPostgres } from "@dokploy/server/utils/databases/postgres";
import { pullImage } from "@dokploy/server/utils/docker/utils";
-import { execAsyncRemote } from "@dokploy/server/utils/process/execAsync";
+import {
+ execAsync,
+ execAsyncRemote,
+} from "@dokploy/server/utils/process/execAsync";
import { TRPCError } from "@trpc/server";
import { eq, getTableColumns } from "drizzle-orm";
+import { updateApplication } from "./application";
import { validUniqueServerAppName } from "./project";
export function getMountPath(dockerImage: string): string {
@@ -173,3 +180,82 @@ export const deployPostgres = async (
}
return postgres;
};
+
+export const changePostgresPassword = async (
+ postgresId: string,
+ newPassword: string,
+) => {
+ const postgres = await findPostgresById(postgresId);
+
+ // 1. Update in Docker Container
+ let command: string;
+ if (postgres.serverId) {
+ const { stdout: containerId } = await execAsyncRemote(
+ postgres.serverId,
+ `docker ps -q -f name=${postgres.appName}`,
+ );
+ if (!containerId) {
+ throw new TRPCError({
+ code: "INTERNAL_SERVER_ERROR",
+ message: `Container not found for service: ${postgres.appName}`,
+ });
+ }
+ command = `docker exec ${containerId.trim()} psql -U ${postgres.databaseUser} -c "ALTER USER ${postgres.databaseUser} WITH PASSWORD '${newPassword}';"`;
+ await execAsyncRemote(postgres.serverId, command);
+ } else {
+ const { stdout: containerId } = await execAsync(
+ `docker ps -q -f name=${postgres.appName}`,
+ );
+ if (!containerId) {
+ throw new TRPCError({
+ code: "INTERNAL_SERVER_ERROR",
+ message: `Container not found for service: ${postgres.appName}`,
+ });
+ }
+ command = `docker exec ${containerId.trim()} psql -U ${postgres.databaseUser} -c "ALTER USER ${postgres.databaseUser} WITH PASSWORD '${newPassword}';"`;
+ await execAsync(command);
+ }
+
+ // 2. Update in Dokploy Database
+ await updatePostgresById(postgresId, {
+ databasePassword: newPassword,
+ });
+
+ // 3. Update Dependent Applications
+ const project = postgres.environment.project;
+
+ // Find all applications in the same organization
+ const allApplications = await db
+ .select({
+ applicationId: applications.applicationId,
+ env: applications.env,
+ name: applications.name,
+ })
+ .from(applications)
+ .innerJoin(
+ environments,
+ eq(applications.environmentId, environments.environmentId),
+ )
+ .innerJoin(projects, eq(environments.projectId, projects.projectId))
+ .where(eq(projects.organizationId, project.organizationId));
+
+ // Update applications that have the old connection string
+ const oldPassword = postgres.databasePassword;
+ let updatedCount = 0;
+
+ for (const app of allApplications) {
+ if (app.env?.includes(oldPassword)) {
+ // Replace all occurrences of the old password in the env string if it's part of a connection string context ideally,
+ // but for now simple replacement as per requirement.
+ // To be safer we could look for the specific connection string format, but 'includes' check + replace is standard for this scope.
+ const newEnv = app.env.replace(new RegExp(oldPassword, "g"), newPassword);
+
+ await updateApplication(app.applicationId, {
+ env: newEnv,
+ });
+ updatedCount++;
+ }
+ }
+
+ return { success: true, updatedApplications: updatedCount };
+};