#9002·dokku

scheduler-k3s: `dokku run` does not propagate the container's exit code

Author: josegonzalezCreated Sep 2, 2026Updated Sep 2, 2026

Under the k3s scheduler, dokku run exits 1 for any failure rather than reporting the command's own exit status. Under docker-local it reports the real one, so the same command gives a different answer depending on the app's scheduler.

plugins/scheduler-docker-local/scheduler-run waits for the container and returns its status:

bash
"$DOCKER_BIN" container start --attach "$CONTAINER_ID" || EXIT_CODE=$?
DOKKU_CONTAINER_EXIT_CODE="$("$DOCKER_BIN" container wait "$CONTAINER_ID" 2>/dev/null || echo "$EXIT_CODE")"
...
return "$DOKKU_CONTAINER_EXIT_CODE"

TriggerSchedulerRun in plugins/scheduler-k3s/triggers.go has the number in hand and discards it. All three PodFailed paths reach the terminated container status and read only .Message (lines 1250, 1286, and 1317):

go
if status.State.Terminated == nil {
    continue
}
return fmt.Errorf("Unable to attach as the pod has already exited with a failed exit code: %s", status.State.Terminated.Message)

That error reaches common.LogFailWithError, which exits 1 for a plain error. So dokku run <app> sh -c 'exit 3' exits 3 on docker-local and 1 on k3s.

This matters for anything that branches on the status — a CI step, a deploy script — and for anything that records it. A caller that captures the exit code of a one-off command can only report success or failure on k3s. The .Message reported in its place is usually empty, so a failure comes back with neither a code nor a reason.

The mechanism for fixing it already exists. common.LogFailWithError honours common.ErrWithExitCode:

go
if errExit, ok := err.(ErrWithExitCode); ok {
    os.Exit(errExit.ExitCode())
}
os.Exit(1)

and there is precedent for implementing it in-tree: RetireLockFailed in plugins/ps/ps.go does exactly this to propagate 137. An error type in scheduler-k3s carrying status.State.Terminated.ExitCode at those three returns would line the two schedulers up.

Two things worth settling alongside it: whether the attached/TTY path (enterPodExecCommand) already propagates a status or needs the same treatment, and whether the termination .Message is still worth carrying in the error text once the code itself is there.

run:detached is unaffected — it prints the pod name and returns nil.

Noticed while adding one-off command support to Dokku Pro, where a run's exit code is recorded against the background job that started it. The value is exact on docker-local and success/failure only on k3s, which is currently a documented caveat rather than something that can be fixed on that side.