basecamp/kamal · error · Kamal::Cli::LockError
Deploy lock held manually, not waiting. Run 'kamal lock help
Error message
Deploy lock held manually, not waiting. Run 'kamal lock help' for more information
What it means
Before mutating commands, Kamal acquires a deploy lock (a directory on the primary host) and retries in a loop while another automatic deploy holds it. The captured lock status always shows who holds it; if the message does not contain Kamal's AUTOMATIC_DEPLOY_LOCK_MESSAGE, the lock was created manually (via `kamal lock acquire`), which signals an intentional hold — so Kamal refuses to wait and raises Kamal::Cli::LockError telling you to run `kamal lock help`.
Source
Thrown at lib/kamal/cli/base.rb:169
timeout = KAMAL.lock_wait_timeout
interval = KAMAL.lock_wait_interval
deadline = Time.now + timeout
details_shown = false
say "Acquiring the deploy lock (waiting up to #{timeout}s)...", :magenta
loop do
execute_lock_acquire(AUTOMATIC_DEPLOY_LOCK_MESSAGE)
break
rescue LockHeldError
unless details_shown
status = capture_lock_status
say "Deploy lock is held by:", :magenta
puts status
unless status.include?(AUTOMATIC_DEPLOY_LOCK_MESSAGE)
raise LockError, "Deploy lock held manually, not waiting. Run 'kamal lock help' for more information"
end
details_shown = true
end
remaining = (deadline - Time.now).to_i
if remaining <= 0
say "Timed out after #{timeout}s waiting for the deploy lock", :red
raise LockError, "Timed out waiting for deploy lock"
end
say "Retrying in #{interval}s (#{remaining}s remaining)...", :magenta
sleep [ interval, remaining ].min
end
end
def release_lock
say "Releasing the deploy lock...", :magentaView on GitHub (pinned to eee0083b38)
Solutions
- Check who/why holds it: `kamal lock status` — the message shows the reason and version recorded at acquire time.
- If the manual hold is no longer needed, release it: `kamal lock release`, then re-run your deploy.
- If the hold is intentional, coordinate with the operator who acquired it before releasing.
- Never delete the lock directory manually on the host unless `kamal lock release` fails; prefer the CLI so state stays consistent.
Example fix
# before kamal deploy # -> Deploy lock held manually, not waiting. # after kamal lock status # see who/why it is held kamal lock release # once maintenance is done kamal deploy
Defensive patterns
Strategy: try-catch
Validate before calling
status = `kamal lock status`
abort "manual deploy lock held:\n#{status}" if status.include?("held") # adapt to your lock message format
# safe to deploy Type guard
def manual_lock_held?(status_output)
status_output.include?("Locked") && !status_output.include?("Automatic")
end Try / catch
begin
Kamal::CLI::Deploy.new.perform
rescue Kamal::Cli::LockError => e
if e.message.include?("held manually")
`kamal lock status` # record holder, then notify operator instead of force-releasing
end
raise
end Prevention
- Run `kamal lock status` at the top of deploy jobs and fail with context instead of mid-deploy.
- Pair every manual `kamal lock acquire` with an automatic release step (trap/finally) in maintenance runbooks.
When it happens
Trigger: Running any locking command (`kamal deploy`, `kamal app stop`, etc.) with a wait_when_locked timeout configured, while the primary host has a lock directory created by `kamal lock acquire 'reason'` (manual). The first acquire attempt raises LockHeldError, status is captured, and because the manual message differs from the automatic one, LockError is raised immediately.
Common situations: An operator manually locked servers for maintenance (frozen deploys) and forgot to release; a teammate ran `kamal lock acquire` during an incident; stale manual lock from a previous maintenance window never released with `kamal lock release`.
Related errors
- Timed out waiting for deploy lock
- Deploy lock found. Run 'kamal lock help' for more informatio
- Raw is not compatible with interactive
- Detach is not compatible with #{incompatible_options.join("
- Raw is not compatible with #{incompatible_options.join(" or
AI-assisted analysis of basecamp/kamal@eee0083b38 (2026-08-21).
Data as JSON: /api/errors/7cdb308c1039012d.
Report an issue: GitHub.