Skip to content

Docker daemon CPU saturation after redeploy while Logs tab is open #4835

Description

@gilangh

To Reproduce

  1. Open an application's Logs tab in Dokploy.
  2. Redeploy or restart that same application while the Logs tab is open.
  3. Close the Logs tab after the new container has replaced the old container.
  4. Monitor the host's processes with pidstat, top, or htop.

This was observed twice on the same single-node Docker Swarm host. It may depend on JSON log rotation/container replacement timing.

Current vs. Expected behavior

Current behavior

After the container is replaced, one or more Docker log requests started by Dokploy remain active even after the browser Logs tab is closed. The Docker daemon then consumes most of the host CPU indefinitely.

In the latest occurrence:

  • dockerd sustained approximately 314% CPU on a four-core host.
  • The affected application was using approximately 1-3% CPU during direct sampling.
  • Other containers were mostly idle.
  • Closing the browser tab did not recover the CPU.
  • Restarting only the Dokploy service immediately reduced dockerd to approximately 18% CPU average, without restarting application containers.

A SIGUSR1 Docker daemon stack dump showed many log-reader goroutines, including:

github.com/docker/docker/api/server/httputils.WriteLogStream
github.com/docker/docker/daemon/logger/loggerutils.tailFiles
github.com/docker/docker/daemon/logger/jsonfilelog.getTailReader
github.com/docker/docker/pkg/tailfile.(*scanner).Scan

Several hot goroutines were repeatedly executing pread while resolving the last 300 lines across current and rotated *-json.log files. Some offsets were already beyond the current file size.

Expected behavior

Closing or navigating away from the Logs tab, and replacing the selected container, should cancel and terminate all associated server-side log commands. No Docker log reader should remain indefinitely or saturate the Docker daemon.

Provide environment information

Dokploy: v0.29.12
Docker Engine: 28.5.0
Docker API: 1.51
Kernel: Linux 6.8.0-134-generic x86_64
Deployment: Self-hosted, single-node Docker Swarm
Dokploy and applications: Same server
Logging driver: json-file

Which area(s) are affected?

Docker, Application, Docker Compose, Logs UI

Additional context

Dokploy's current getContainerLogs() implementation executes docker container logs --timestamps --tail ... and awaits the command:

export const getContainerLogs = async (
appNameOrId: string,
tail = 100,
since = "all",
search?: string,
serverId?: string | null,
useContainerIdDirectly = false,
): Promise<string> => {
const exec = (cmd: string) =>
serverId ? execAsyncRemote(serverId, cmd) : execAsync(cmd);
let target = appNameOrId;
let isService = false;
if (!useContainerIdDirectly) {
// Find the real container ID by appName filter
const findResult = await exec(
`docker ps -q --filter "name=^${appNameOrId}" | head -1`,
);
const containerId = findResult.stdout.trim();
if (!containerId) {
// Fallback: try as a swarm service
const svcResult = await exec(
`docker service ls -q --filter "name=${appNameOrId}" | head -1`,
);
const serviceId = svcResult.stdout.trim();
if (!serviceId) {
throw new Error(`No container or service found for: ${appNameOrId}`);
}
isService = true;
} else {
target = containerId;
}
}
const sinceFlag = since === "all" ? "" : `--since ${since}`;
const baseCommand = isService
? `docker service logs --timestamps --raw --tail ${tail} ${sinceFlag} ${target}`
: `docker container logs --timestamps --tail ${tail} ${sinceFlag} ${target}`;
const escapedSearch = search?.replace(/'/g, "'\\''") ?? "";
const command = search
? `${baseCommand} 2>&1 | grep -iF '${escapedSearch}'`
: `${baseCommand} 2>&1`;
try {
const result = await exec(command);
return result.stdout;
} catch (error: unknown) {
if (
error &&
typeof error === "object" &&
"stdout" in error &&
typeof (error as { stdout: string }).stdout === "string" &&
(error as { stdout: string }).stdout.length > 0
) {
return (error as { stdout: string }).stdout;
}
throw error;
}

There does not appear to be a timeout or abort/cancellation path around this command. If Docker's tail scanner gets stuck during container replacement or log rotation, the in-flight command can survive after the UI stops requesting new logs.

Potential safeguards:

  • Apply a bounded timeout to every docker logs command.
  • Terminate the child process when the HTTP/tRPC request is cancelled or disconnected.
  • Cancel outstanding log requests when the selected container is removed/replaced.
  • Avoid overlapping polling requests for the same container.

Workaround

  1. Close Dokploy Logs tabs before redeploying.
  2. If dockerd remains hot, restart only the Dokploy service to terminate the orphaned log commands.
  3. A full Docker daemon restart was not required in the latest occurrence.

Related issues

Issue #4661 also reports high CPU, but appears unrelated: it concerns a TimeoutNegativeWarning loop in the Dokploy Node.js process. In this report, the hot process was dockerd, and the stack dump directly identified Docker log-tail readers.

Metadata

Metadata

Assignees

No one assigned

    Labels

    bugSomething isn't working

    Type

    No type

    Fields

    No fields configured for issues without a type.

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions