From d75de778384e64cfdb5363981ec3d4d38e209863 Mon Sep 17 00:00:00 2001 From: NoelisTired Date: Wed, 3 Jun 2026 11:23:44 +0200 Subject: [PATCH] fix: refresh server IP after web server restore When restoring a Dokploy web server backup, the database is restored as-is, which means `webServerSettings.serverIp` keeps the IP of the machine the backup was taken on. After restoring onto a different host the dashboard kept showing the old "Server IP". Re-detect the current host's public IP with `getPublicIpWithFallback` once the database restore finishes and persist it via `updateWebServerSettings`, mirroring how the IP is detected during initial setup. If detection fails the stored value is left untouched and a warning is emitted so it can be updated manually. Adds tests covering the success, detection-failure, and ordering cases. --- .../__test__/restore/web-server.test.ts | 91 +++++++++++++++++++ .../server/src/utils/restore/web-server.ts | 16 ++++ 2 files changed, 107 insertions(+) create mode 100644 apps/dokploy/__test__/restore/web-server.test.ts diff --git a/apps/dokploy/__test__/restore/web-server.test.ts b/apps/dokploy/__test__/restore/web-server.test.ts new file mode 100644 index 0000000000..5df2f9ad8f --- /dev/null +++ b/apps/dokploy/__test__/restore/web-server.test.ts @@ -0,0 +1,91 @@ +import { restoreWebServerBackup } from "@dokploy/server/utils/restore/web-server"; +import { beforeEach, describe, expect, it, vi } from "vitest"; + +const execAsyncMock = vi.fn(); +const updateWebServerSettingsMock = vi.fn(); +const getPublicIpWithFallbackMock = vi.fn(); + +vi.mock("@dokploy/server/utils/process/execAsync", () => ({ + execAsync: (...args: unknown[]) => execAsyncMock(...args), +})); + +vi.mock("@dokploy/server/services/web-server-settings", () => ({ + updateWebServerSettings: (...args: unknown[]) => + updateWebServerSettingsMock(...args), +})); + +vi.mock("@dokploy/server/wss/utils", () => ({ + getPublicIpWithFallback: (...args: unknown[]) => + getPublicIpWithFallbackMock(...args), +})); + +vi.mock("@dokploy/server/utils/backups/utils", () => ({ + getS3Credentials: () => ["--s3-flag"], +})); + +const destination = { + bucket: "my-bucket", +} as any; + +describe("restoreWebServerBackup", () => { + beforeEach(() => { + vi.clearAllMocks(); + + // Default execAsync behaviour: the two `ls ... || true` probes must report + // that the database files exist, every other command resolves with no output. + execAsyncMock.mockImplementation((command: string) => { + if (command.includes("database.sql.gz")) { + return Promise.resolve({ stdout: "database.sql.gz", stderr: "" }); + } + if (command.includes("ls") && command.includes("database.sql")) { + return Promise.resolve({ stdout: "database.sql", stderr: "" }); + } + if (command.includes("docker ps")) { + return Promise.resolve({ stdout: "container-id", stderr: "" }); + } + return Promise.resolve({ stdout: "", stderr: "" }); + }); + }); + + it("re-detects and persists the current public IP after a restore", async () => { + getPublicIpWithFallbackMock.mockResolvedValue("203.0.113.10"); + const emit = vi.fn(); + + await restoreWebServerBackup(destination, "backup.zip", emit); + + expect(getPublicIpWithFallbackMock).toHaveBeenCalledTimes(1); + expect(updateWebServerSettingsMock).toHaveBeenCalledWith({ + serverIp: "203.0.113.10", + }); + expect(emit).toHaveBeenCalledWith("Server IP updated to: 203.0.113.10"); + expect(emit).toHaveBeenCalledWith("Restore completed successfully!"); + }); + + it("does not overwrite the server IP when detection fails", async () => { + getPublicIpWithFallbackMock.mockResolvedValue(null); + const emit = vi.fn(); + + await restoreWebServerBackup(destination, "backup.zip", emit); + + expect(getPublicIpWithFallbackMock).toHaveBeenCalledTimes(1); + expect(updateWebServerSettingsMock).not.toHaveBeenCalled(); + expect(emit).toHaveBeenCalledWith( + "Warning: could not detect the public IP, the server IP was left unchanged. Update it manually in Web Server settings if needed.", + ); + expect(emit).toHaveBeenCalledWith("Restore completed successfully!"); + }); + + it("refreshes the IP only after the database restore has run", async () => { + getPublicIpWithFallbackMock.mockResolvedValue("203.0.113.10"); + const emit = vi.fn(); + + await restoreWebServerBackup(destination, "backup.zip", emit); + + // pg_restore must have been invoked before we touch the server IP. + const restoreCalled = execAsyncMock.mock.calls.some(([command]) => + String(command).includes("pg_restore"), + ); + expect(restoreCalled).toBe(true); + expect(updateWebServerSettingsMock).toHaveBeenCalledTimes(1); + }); +}); diff --git a/packages/server/src/utils/restore/web-server.ts b/packages/server/src/utils/restore/web-server.ts index 683a1898ae..d42c0bd52f 100644 --- a/packages/server/src/utils/restore/web-server.ts +++ b/packages/server/src/utils/restore/web-server.ts @@ -3,6 +3,8 @@ import { tmpdir } from "node:os"; import { join } from "node:path"; import { IS_CLOUD, paths } from "@dokploy/server/constants"; import type { Destination } from "@dokploy/server/services/destination"; +import { updateWebServerSettings } from "@dokploy/server/services/web-server-settings"; +import { getPublicIpWithFallback } from "@dokploy/server/wss/utils"; import { getS3Credentials } from "../backups/utils"; import { execAsync } from "../process/execAsync"; @@ -133,6 +135,20 @@ export const restoreWebServerBackup = async ( `docker exec ${postgresContainerId} rm /tmp/database.sql`, ); + // The restored database contains the server IP from the machine the + // backup was taken on. Re-detect the current host's public IP so the + // dashboard reflects this server instead of the old one. + emit("Refreshing server IP..."); + const serverIp = await getPublicIpWithFallback(); + if (serverIp) { + await updateWebServerSettings({ serverIp }); + emit(`Server IP updated to: ${serverIp}`); + } else { + emit( + "Warning: could not detect the public IP, the server IP was left unchanged. Update it manually in Web Server settings if needed.", + ); + } + emit("Restore completed successfully!"); } finally { // Cleanup