Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
16 commits
Select commit Hold shift + click to select a range
739e6e1
refactor(servers): split the component update script into steps
agentHits Oct 8, 2026
49223ad
feat(servers): compare panel host components with their pinned targets
agentHits Oct 8, 2026
663c94e
feat(servers): update Traefik and Redis on the panel host locally
agentHits Oct 8, 2026
12a557d
feat(settings): expose the panel host components to the Remote Server…
agentHits Oct 8, 2026
f54485f
feat(servers): show the panel host components on the Remote Servers page
agentHits Oct 8, 2026
5d5a0a5
fix(servers): keep the maintenance reason and command on Docker and P…
agentHits Oct 8, 2026
6270ca6
fix(servers): show every Traefik and Redis row with a checkbox
agentHits Oct 8, 2026
56a09c6
fix(settings): refuse a panel update while host components are updating
agentHits Oct 8, 2026
1227e8b
fix(traefik): mount the Docker socket read-only in the generated cont…
agentHits Oct 8, 2026
eb8c5d4
fix(traefik): publish port 80 only when the running container does
agentHits Oct 8, 2026
a414e1c
test(settings): mock the host stack exports used by the settings router
agentHits Oct 8, 2026
90ea2c3
fix(traefik): leave the default bridge network off when the running c…
agentHits Oct 8, 2026
49a4cba
fix(installer): expand the Traefik publish array safely on bash 3.2
agentHits Oct 8, 2026
a0916a6
fix(installer): keep an installed Postgres whose major differs instea…
agentHits Oct 8, 2026
a3863d0
docs: state that port 80 is never opened and certificates renew over 443
agentHits Oct 8, 2026
63c1135
fix(servers): make the host stack update safe to repeat and to race
agentHits Oct 9, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
31 changes: 31 additions & 0 deletions apps/dokploy/__test__/server/host-stack-lock.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
import {
acquireHostStackUpdateLock,
isHostStackUpdateRunning,
} from "@dokploy/server/setup/host-stack-lock";
import { describe, expect, it } from "vitest";

describe("acquireHostStackUpdateLock", () => {
it("admits one holder at a time and frees the lock on release", () => {
const release = acquireHostStackUpdateLock();

expect(release).not.toBeNull();
expect(isHostStackUpdateRunning()).toBe(true);
expect(acquireHostStackUpdateLock()).toBeNull();

release?.();
expect(isHostStackUpdateRunning()).toBe(false);
});

it("ignores a repeated release, so it cannot free a lock taken after it", () => {
const first = acquireHostStackUpdateLock();
first?.();
const second = acquireHostStackUpdateLock();
first?.();

expect(isHostStackUpdateRunning()).toBe(true);
expect(acquireHostStackUpdateLock()).toBeNull();

second?.();
expect(isHostStackUpdateRunning()).toBe(false);
});
});
305 changes: 305 additions & 0 deletions apps/dokploy/__test__/server/host-stack-rows.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,305 @@
import { PINNED_VERSIONS } from "@dokploy/server/setup/component-versions";
import {
buildHostStackRows,
HOST_STACK_COMPONENTS,
type HostStackReadings,
type HostStackRow,
hostStackRefusals,
isHostStackUiComponent,
} from "@dokploy/server/setup/host-stack-rows";
import { TRAEFIK_VERSION } from "@dokploy/server/setup/traefik-setup";
import { describe, expect, it } from "vitest";

const pinnedPostgresMajor = PINNED_VERSIONS.postgres.split(".")[0];

const upToDate: HostStackReadings = {
docker: PINNED_VERSIONS.docker,
traefikImage: `traefik:v${TRAEFIK_VERSION}`,
postgresImage: `postgres:${PINNED_VERSIONS.postgres}`,
redisImage: `redis:${PINNED_VERSIONS.redis}`,
panel: {
installed: "agenthits-dev",
latest: "agenthits-dev",
updateAvailable: false,
},
};

const rowsFor = (overrides: Partial<HostStackReadings>) =>
buildHostStackRows({ ...upToDate, ...overrides });

const rowNamed = (rows: HostStackRow[], name: HostStackRow["name"]) => {
const row = rows.find((candidate) => candidate.name === name);
if (!row) {
throw new Error(`no row named ${name}`);
}
return row;
};

describe("buildHostStackRows", () => {
it("lists the panel host components in stack order", () => {
expect(rowsFor({}).map((row) => row.name)).toEqual([
"panel",
"docker",
"traefik",
"postgres",
"redis",
]);
});

it("reports every component as current when the installed versions match the pins", () => {
for (const row of rowsFor({})) {
expect(row.status, row.name).toBe("current");
expect(row.outdated, row.name).toBe(false);
}
});

it("keeps the reason of the maintenance-window steps even when they are current", () => {
const rows = rowsFor({});

expect(rowNamed(rows, "docker")).toMatchObject({
action: "manual",
command: "DOCKER_ENGINE_UPGRADE=1 bash install-agenthits.sh update",
});
expect(rowNamed(rows, "docker").reason).toContain("maintenance window");
expect(rowNamed(rows, "postgres")).toMatchObject({
action: "manual",
command: null,
});
expect(rowNamed(rows, "postgres").reason).toContain("maintenance window");
});

it("gives Traefik and Redis no reason or command, since the panel runs them", () => {
const rows = rowsFor({
traefikImage: "traefik:v3.6.1",
redisImage: "redis:7",
});

for (const name of ["traefik", "redis"] as const) {
expect(rowNamed(rows, name)).toMatchObject({
action: "ui",
reason: null,
command: null,
});
}
});

it("marks Docker Engine behind its pin as a maintenance-window step with the engine upgrade command", () => {
const docker = rowNamed(rowsFor({ docker: "29.4.1" }), "docker");

expect(docker).toMatchObject({
installed: "29.4.1",
target: PINNED_VERSIONS.docker,
outdated: true,
action: "manual",
command: "DOCKER_ENGINE_UPGRADE=1 bash install-agenthits.sh update",
});
expect(docker.reason).toContain("maintenance window");
});

it("compares Docker Engine versions numerically", () => {
expect(rowNamed(rowsFor({ docker: "29.10.0" }), "docker").outdated).toBe(
false,
);
expect(rowNamed(rowsFor({ docker: "29.8.1" }), "docker").outdated).toBe(
true,
);
});

it("reads the Traefik version from its image tag and marks it updatable from the panel", () => {
const traefik = rowNamed(
rowsFor({ traefikImage: "traefik:v3.6.1" }),
"traefik",
);

expect(traefik).toMatchObject({
installed: "3.6.1",
target: TRAEFIK_VERSION,
outdated: true,
action: "ui",
reason: null,
command: null,
});
});

it("treats Traefik at the pinned version as current", () => {
expect(
rowNamed(
rowsFor({ traefikImage: `traefik:v${TRAEFIK_VERSION}` }),
"traefik",
).outdated,
).toBe(false);
});

it("compares Postgres by major version and reports a major change as manual", () => {
const postgres = rowNamed(
rowsFor({ postgresImage: "postgres:16" }),
"postgres",
);

expect(postgres).toMatchObject({
installed: "16",
target: PINNED_VERSIONS.postgres,
outdated: true,
action: "manual",
command: "POSTGRES_IMAGE=postgres:16 bash install-agenthits.sh update",
});
expect(postgres.reason).toBe(
`Major version change (16 to ${pinnedPostgresMajor}) needs a migration. The panel never changes the Postgres major version.`,
);
});

it("compares the full Postgres version within the pinned major", () => {
const current = rowNamed(
rowsFor({ postgresImage: `postgres:${PINNED_VERSIONS.postgres}-alpine` }),
"postgres",
);

expect(current).toMatchObject({
installed: PINNED_VERSIONS.postgres,
status: "current",
outdated: false,
command: null,
});

const minorBehind = rowNamed(
rowsFor({ postgresImage: `postgres:${pinnedPostgresMajor}` }),
"postgres",
);

expect(minorBehind).toMatchObject({
installed: pinnedPostgresMajor,
target: PINNED_VERSIONS.postgres,
status: "outdated",
outdated: true,
action: "manual",
command: null,
});
expect(minorBehind.reason).toContain("maintenance window");
});

it("compares Postgres minor versions numerically", () => {
expect(
rowNamed(
rowsFor({ postgresImage: `postgres:${pinnedPostgresMajor}.10` }),
"postgres",
).outdated,
).toBe(false);
});

it("keeps the image digest out of the Postgres command", () => {
const postgres = rowNamed(
rowsFor({ postgresImage: "postgres:16@sha256:abcdef0123" }),
"postgres",
);

expect(postgres.command).toBe(
"POSTGRES_IMAGE=postgres:16 bash install-agenthits.sh update",
);
});

it("marks Redis behind its pin as updatable from the panel, comparing versions numerically", () => {
expect(rowNamed(rowsFor({ redisImage: "redis:7" }), "redis")).toMatchObject(
{
installed: "7",
target: PINNED_VERSIONS.redis,
outdated: true,
action: "ui",
},
);
expect(
rowNamed(rowsFor({ redisImage: "redis:8.9.9" }), "redis").outdated,
).toBe(true);
expect(
rowNamed(rowsFor({ redisImage: "redis:8.10.2" }), "redis").outdated,
).toBe(false);
});

it("reports components whose version cannot be read as unknown rather than outdated", () => {
const rows = rowsFor({
docker: null,
traefikImage: null,
postgresImage: "postgres:latest",
redisImage: "redis:latest",
});

for (const name of ["docker", "traefik", "postgres", "redis"] as const) {
const row = rowNamed(rows, name);
expect(row.status, name).toBe("unknown");
expect(row.outdated, name).toBe(false);
expect(row.installed, name).toBeNull();
expect(row.command, name).toBe(
name === "docker"
? "DOCKER_ENGINE_UPGRADE=1 bash install-agenthits.sh update"
: null,
);
expect(row.reason, name).not.toBeNull();
}
});

it("marks the panel outdated when its update check reports a newer build", () => {
expect(
rowNamed(
rowsFor({
panel: {
installed: "agenthits-dev",
latest: "agenthits-dev-2",
updateAvailable: true,
},
}),
"panel",
),
).toMatchObject({
installed: "agenthits-dev",
target: "agenthits-dev-2",
status: "outdated",
outdated: true,
action: "panel",
});
});

it("reports the panel as unknown when the update check did not return a version", () => {
expect(
rowNamed(
rowsFor({
panel: {
installed: "agenthits-dev",
latest: null,
updateAvailable: null,
},
}),
"panel",
),
).toMatchObject({
status: "unknown",
outdated: false,
action: "panel",
target: null,
});
});
});

describe("host stack update rules", () => {
it("allows the panel to update only Traefik and Redis", () => {
expect(HOST_STACK_COMPONENTS.filter(isHostStackUiComponent)).toEqual([
"traefik",
"redis",
]);
});

it("refuses docker, postgres and panel with a message that names the next step", () => {
const refusals = hostStackRefusals([
"traefik",
"docker",
"postgres",
"panel",
]);

expect(refusals).toHaveLength(3);
expect(refusals[0]).toContain(
"DOCKER_ENGINE_UPGRADE=1 bash install-agenthits.sh update",
);
expect(refusals[1]).toContain("POSTGRES_IMAGE");
expect(refusals[2]).toContain("Web Server Update");
expect(hostStackRefusals(["traefik", "redis"])).toEqual([]);
});
});
Loading