mirror of
https://github.com/Dokploy/dokploy.git
synced 2026-09-12 19:51:00 +05:00
fix(server): pin locally-built swarm services to the build node
A locally built application image (git/dockerfile/buildpacks, no registry
configured) only exists on the node that built it. The swarm service is
created with no placement constraint, so on a multi-node swarm the
scheduler can place a task on another node that lacks the image. That task
is rejected with "No such image" while the previous task keeps running, so
the deployment reports success ("done") but the app is never updated.
Pin such services to the build node by adding a `node.id==<build node>`
placement constraint (resolved from the deploy connection's
`docker info` Swarm.NodeID), mirroring how bind mounts are already pinned.
Only applied for locally-built images and only when the user has not
configured their own placement; images pushed to a registry and external
(docker) images are unaffected.
This commit is contained in:
parent
7ba9818894
commit
568945e768
@ -8,28 +8,37 @@ type MockCreateServiceOptions = {
|
||||
StopGracePeriod?: number;
|
||||
Ulimits?: Array<{ Name: string; Soft: number; Hard: number }>;
|
||||
};
|
||||
Placement?: { Constraints?: string[] };
|
||||
};
|
||||
[key: string]: unknown;
|
||||
};
|
||||
|
||||
const { inspectMock, getServiceMock, createServiceMock, getRemoteDockerMock } =
|
||||
vi.hoisted(() => {
|
||||
const inspect = vi.fn<() => Promise<never>>();
|
||||
const getService = vi.fn(() => ({ inspect }));
|
||||
const createService = vi.fn<
|
||||
(opts: MockCreateServiceOptions) => Promise<void>
|
||||
>(async () => undefined);
|
||||
const getRemoteDocker = vi.fn(async () => ({
|
||||
getService,
|
||||
createService,
|
||||
}));
|
||||
return {
|
||||
inspectMock: inspect,
|
||||
getServiceMock: getService,
|
||||
createServiceMock: createService,
|
||||
getRemoteDockerMock: getRemoteDocker,
|
||||
};
|
||||
});
|
||||
const {
|
||||
inspectMock,
|
||||
getServiceMock,
|
||||
createServiceMock,
|
||||
getRemoteDockerMock,
|
||||
infoMock,
|
||||
} = vi.hoisted(() => {
|
||||
const inspect = vi.fn<() => Promise<never>>();
|
||||
const getService = vi.fn(() => ({ inspect }));
|
||||
const createService = vi.fn<
|
||||
(opts: MockCreateServiceOptions) => Promise<void>
|
||||
>(async () => undefined);
|
||||
const info = vi.fn(async () => ({ Swarm: { NodeID: "node-123" } }));
|
||||
const getRemoteDocker = vi.fn(async () => ({
|
||||
getService,
|
||||
createService,
|
||||
info,
|
||||
}));
|
||||
return {
|
||||
inspectMock: inspect,
|
||||
getServiceMock: getService,
|
||||
createServiceMock: createService,
|
||||
getRemoteDockerMock: getRemoteDocker,
|
||||
infoMock: info,
|
||||
};
|
||||
});
|
||||
|
||||
vi.mock("@dokploy/server/utils/servers/remote-docker", () => ({
|
||||
getRemoteDocker: getRemoteDockerMock,
|
||||
@ -70,9 +79,12 @@ describe("mechanizeDockerContainer", () => {
|
||||
getServiceMock.mockClear();
|
||||
createServiceMock.mockClear();
|
||||
getRemoteDockerMock.mockClear();
|
||||
infoMock.mockClear();
|
||||
infoMock.mockResolvedValue({ Swarm: { NodeID: "node-123" } });
|
||||
getRemoteDockerMock.mockResolvedValue({
|
||||
getService: getServiceMock,
|
||||
createService: createServiceMock,
|
||||
info: infoMock,
|
||||
});
|
||||
});
|
||||
|
||||
@ -158,4 +170,76 @@ describe("mechanizeDockerContainer", () => {
|
||||
const [settings] = call;
|
||||
expect(settings.TaskTemplate?.ContainerSpec).not.toHaveProperty("Ulimits");
|
||||
});
|
||||
|
||||
it("pins a locally built image to the build node", async () => {
|
||||
const application = createApplication({
|
||||
sourceType: "github",
|
||||
registry: null,
|
||||
placementSwarm: null,
|
||||
});
|
||||
|
||||
await mechanizeDockerContainer(application);
|
||||
|
||||
const call = createServiceMock.mock.calls[0];
|
||||
if (!call) {
|
||||
throw new Error("createServiceMock should have been called once");
|
||||
}
|
||||
const [settings] = call;
|
||||
expect(settings.TaskTemplate?.Placement?.Constraints).toContain(
|
||||
"node.id==node-123",
|
||||
);
|
||||
});
|
||||
|
||||
it("does not pin external (docker) images to the build node", async () => {
|
||||
const application = createApplication({ sourceType: "docker" });
|
||||
|
||||
await mechanizeDockerContainer(application);
|
||||
|
||||
const call = createServiceMock.mock.calls[0];
|
||||
if (!call) {
|
||||
throw new Error("createServiceMock should have been called once");
|
||||
}
|
||||
const [settings] = call;
|
||||
expect(settings.TaskTemplate?.Placement?.Constraints ?? []).not.toContain(
|
||||
"node.id==node-123",
|
||||
);
|
||||
});
|
||||
|
||||
it("does not pin images that are pushed to a registry", async () => {
|
||||
const application = createApplication({
|
||||
sourceType: "github",
|
||||
registry: null,
|
||||
rollbackRegistry: {} as ApplicationNested["rollbackRegistry"],
|
||||
});
|
||||
|
||||
await mechanizeDockerContainer(application);
|
||||
|
||||
const call = createServiceMock.mock.calls[0];
|
||||
if (!call) {
|
||||
throw new Error("createServiceMock should have been called once");
|
||||
}
|
||||
const [settings] = call;
|
||||
expect(settings.TaskTemplate?.Placement?.Constraints ?? []).not.toContain(
|
||||
"node.id==node-123",
|
||||
);
|
||||
});
|
||||
|
||||
it("respects a user-defined placement instead of auto-pinning", async () => {
|
||||
const application = createApplication({
|
||||
sourceType: "github",
|
||||
registry: null,
|
||||
placementSwarm: { Constraints: ["node.labels.zone==eu"] },
|
||||
});
|
||||
|
||||
await mechanizeDockerContainer(application);
|
||||
|
||||
const call = createServiceMock.mock.calls[0];
|
||||
if (!call) {
|
||||
throw new Error("createServiceMock should have been called once");
|
||||
}
|
||||
const [settings] = call;
|
||||
expect(settings.TaskTemplate?.Placement?.Constraints).toEqual([
|
||||
"node.labels.zone==eu",
|
||||
]);
|
||||
});
|
||||
});
|
||||
|
||||
@ -126,6 +126,22 @@ export const mechanizeDockerContainer = async (
|
||||
const authConfig = await getAuthConfig(application);
|
||||
const docker = await getRemoteDocker(application.serverId);
|
||||
|
||||
const isNodeLocalImage =
|
||||
application.sourceType !== "docker" &&
|
||||
!application.registry &&
|
||||
!application.buildRegistry &&
|
||||
!application.rollbackRegistry;
|
||||
|
||||
if (isNodeLocalImage && !application.placementSwarm) {
|
||||
const buildNodeId = (await docker.info())?.Swarm?.NodeID;
|
||||
if (buildNodeId) {
|
||||
Placement.Constraints = [
|
||||
...(Placement.Constraints ?? []),
|
||||
`node.id==${buildNodeId}`,
|
||||
];
|
||||
}
|
||||
}
|
||||
|
||||
const settings: CreateServiceOptions = {
|
||||
authconfig: authConfig,
|
||||
Name: appName,
|
||||
|
||||
Loading…
Reference in New Issue
Block a user