Skip to content

[Bug]: Docker Cleanup fires on schedule but silently no-ops: cleanupAll() swallows every error in an empty catch #5044

Description

@mtb77

Environment

  • Dokploy v0.29.14 (self-hosted, Docker Swarm, amd64)
  • One local (web server) instance plus several external remote servers connected via SSH
  • Docker 29.4.2 on all hosts

To Reproduce

  1. Enable "Daily Docker Cleanup" for the web server and/or a remote server (enableDockerCleanup = true, confirmed in Postgres).
  2. Let the nightly schedule run for days or weeks.
  3. Check the dokploy service logs: the scheduled job fires exactly as designed. We see SERVER-BACKUP[<date>] Running Cleanup <server name> every night at 23:50 for 11 consecutive nights, and Docker Cleanup ... Running docker cleanup for the local web server.
  4. SSH into any of the hosts and run docker system df and journalctl -u docker | grep -iE 'delete|untag'.
  5. Nothing was ever pruned. In our case: the remote server had accumulated 139 GB of unused images (disk at 95 percent), and the Dokploy host itself still carried dangling image layers dating back to 2025-11, i.e. roughly 9 months old, even though its own cleanup requires no SSH hop at all.

Current vs. Expected behavior

Current: the cleanup schedule fires, but every prune subcommand failure is silently discarded, so the feature can be broken indefinitely while looking healthy. There is no log line, no error, and the cleanup notification path reports nothing wrong.

Expected: failures of the underlying docker ... prune commands are logged (and ideally surfaced through sendDockerCleanupNotifications), so a permanently failing cleanup is visible.

Root cause

cleanupAll() in packages/server/src/utils/docker/utils.ts swallows every error with an empty catch (the individual cleanupImages/cleanupContainers/... helpers log at least to console.error, but the scheduled path goes through cleanupAll):

export const cleanupAll = async (serverId) => {
    for (const [key, command] of Object.entries(cleanupCommands)) {
        if (excludedCleanupAllCommands.includes(key)) continue;
        try {
            if (serverId) { await execAsyncRemote(serverId, dockerSafeExec(command)); }
            else { await execAsync(dockerSafeExec(command)); }
        }
        catch { }   // <- every failure of every subcommand vanishes here
    }
};

(Quoted from the compiled @dokploy/server/dist/utils/docker/utils.js inside the running v0.29.14 container; the same empty catch is present in the repository source.)

Since the failure reproduces on the local web server as well (no SSH involved), the empty catch is hiding whatever the real underlying error is. One suspect worth checking: the busy-wait loop in dockerSafeExec (ps aux | grep -E "^.*docker [A-Za-z]"). On a remote server this greps the host's entire process list via SSH, and any long-lived process matching that pattern would make the loop spin until failure.

Relation to existing issues

#4549 looked at unused images accumulating on remote servers and was closed after concluding the per-server flag simply defaults to off. This report is a different failure mode: the flag is on, the scheduler demonstrably fires every night, and cleanup still never happens because the execution errors are unobservable.

Suggested fix

  1. Replace the empty catch in cleanupAll with a console.error that includes the failing command, the serverId/name, and the error.
  2. Optionally propagate per-command failures into sendDockerCleanupNotifications so operators learn about a broken cleanup instead of assuming success.

I will follow up with a PR for step 1.

Metadata

Metadata

Assignees

No one assigned

    Labels

    bugSomething isn't working

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions