Kamal Deploy
An Omarchy shell plugin that turns every
Kamal project on your machine into a bar-panel
checklist. It finds config/deploy.yml and config/deploy.<env>.yml files
under folders you point it at, and lets you check any number of
destinations — across one project or several — then run setup / deploy /
logs / console / accessory / lock actions against all of them at once, each
one opening (or refocusing) your default terminal so you never have to open
one by hand.
Why
Deploying multiple Kamal apps across multiple environments usually means a
hand-rolled shell menu (kamal_menu() in .bashrc, a bin/deploy script,
whatever) that only knows about the one repo you're sitting in. This plugin
generalizes that: point it at your projects folder once, and every
destination in every project shows up as a clickable target, from the bar,
on any workspace.
Install
git clone https://github.com/eddygarcas/omarchy-kamal-deploy.git \
~/.config/omarchy/plugins/eduard.kamal-deploy
omarchy-shell shell rescanPlugins
omarchy plugin enable eduard.kamal-deploy
What it does
Click the bar icon (a compass) to open the panel. A ⬇ update icon
appears next to ↻ (both bigger now, next to the panel title) only when
this plugin's own GitHub repo has a newer manifest.json version than the
one installed — checked once per panel-open, silently, never surfacing an
error if you're offline or GitHub's unreachable, it just means no icon.
Hover it to see the current/latest versions; click it for update
instructions — this plugin doesn't update itself. An earlier version did
(git fetch + fast-forward from origin/main), but that pulls in
whatever is on the branch right now regardless of whether the
marketplace's own commit verification has reviewed it, silently trading
the reviewed, listed snapshot for unreviewed code — outside what a
plugin's own update mechanism can safely do on its own. Update by hand
instead: cd into the plugin's folder, git pull, omarchy restart shell.
-
Search folders — folders to scan for
config/deploy*.yml. Type a path (~is expanded, relative paths are taken as relative to your home folder) — press Tab to autocomplete it, terminal-style: one match completes the folder name and appends/so the next Tab drills in; several matches complete as far as they agree and show the rest as clickable chips — then hit Add, or click Detect to pick up common project folders (~/Code,~/Projects,~/dev,~/RubymineProjects, …) automatically. Saved inshell.jsonunder"eduard.kamal-deploy", so it survives restarts. Click ↻ to rescan on demand. As a safety baseline, every folder must resolve inside your home folder —/etc,/, or a..-traversal trick like~/../../etcare all rejected, both here and again bydiscover.shitself (in caseshell.jsonwas hand-edited). -
Checklist — one section per project (named from
service:in its basedeploy.yml, or the folder name, with a small language icon — a Nerd Font glyph for Ruby (any*.rbfile or aGemfile/Rakefilein the project root), Go (*.goorgo.mod), TypeScript (*.ts/*.tsxortsconfig.json), or Node (*.js/*.jsxorpackage.json), a plain dot for anything else — checked in that order, so Ruby wins if a project has both aGemfileand apackage.json, e.g. a Rails app with a JS asset pipeline), one checkbox per destination (defaultforconfig/deploy.yml,<env>for eachconfig/deploy.<env>.yml) — laid out as a two-column grid once a project has more than one destination, one column otherwise. Select all / Clear toggle everything at once. As soon as anything is checked, a SELECTED (N) chip row appears (click a chip's ✕ to uncheck just that one) followed by the shared action bar — every button carries an icon for its action:- Deploy — Provision (
ruby provision <env>— enabled only once every selected target's project has aprovisionscript, see Provision Wizard below), Setup (kamal setup -v), Deploy (kamal lock releasethenkamal deploy -v), Rollback (kamal rollback). - Application — Tail logs (
kamal app logs -f), Rails console (kamal app exec -i 'bin/rails console'), Bash shell (kamal app exec -i bash), Restart (kamal app restart), Details (kamal details). - Operations — Lock status, Release lock, Audit log.
- Accessories — known accessory names (parsed from every selected
project's
accessories:block) are listed as a hint; type one into the field and Boot / Reboot / Stop / Restart / Logs / Remove become available (Reboot releases the lock first, same as the original script). Letters, numbers,-and_only — Logs opens a real terminal via a launcher that reassembles its arguments through a shell, so this is enforced strictly rather than just rejecting whitespace.
Clicking any action runs it once per checked target — a
staging+productionselection queues (or opens) two of whatever that action uses, one per destination, since each needs its ownkamal ... -d <env>in its own project directory; they can't be merged into a single command. - Deploy — Provision (
Most actions never open a terminal at all. Provision, Setup, Deploy,
Restart, Details, Lock status, Release lock, Audit log, and every
accessory action except its own Logs run in the background — a spinner
appears under RESULTS while it's in flight, then the real command's
output (and exit status) renders in a scrollable console-styled card you
can dismiss whenever you're done reading it, or clear in bulk once a batch
finishes. Each one also fires a desktop notification (notify-send, app
name "Kamal Deploy") the moment it finishes — done or failed — since the
panel is often closed by then; it names the project/env/action and, on
failure, the exit code, and just points you back to the panel rather than
repeating the full output. A handful of actions still open a real terminal, because they
either need genuine interactive input or never finish on their own: Tail
logs and an accessory's own Logs (-f, streams forever), Rails console and
Bash shell (kamal app exec -i, a real interactive session), and Rollback
(prompts you to pick a version). Those go through scripts/run.sh, opened
via omarchy-launch-or-focus-tui — your actual default terminal (foot,
kitty, alacritty, ghostty, whatever xdg-terminal-exec resolves to), with
a stable per-target-per-action window so clicking the same action twice on
the same target refocuses the running one instead of piling up windows.
Both paths resolve a clicked target the same way, through
$XDG_RUNTIME_DIR/eduard.kamal-deploy/targets.json (written by the last
scan) — a background run just calls scripts/run-background.sh (no
terminal, no colors, no "press any key" pause, plain captured
stdout/stderr) instead of scripts/run.sh; both share the actual
action→command mapping from scripts/dispatch.sh so the two paths can't
drift apart on what a given action actually runs. Both also share
scripts/ensure-kamal.sh, which only checks whether kamal is on
PATH — it deliberately doesn't try to install it for you (that would
mean fetching and running whatever's currently published to RubyGems
with no version pin and no confirmation), so a missing kamal fails
with a clear error telling you to install it yourself.
Setup and Deploy — the two actions that build and push a Docker image
from this machine, unlike every other action which only talks to the
remote host's own docker daemon over SSH — first check docker info.
If local Docker isn't reachable, the action stops right there (no kamal
process ever spawned) and a desktop notification tells you to start
Docker and retry, instead of failing a few seconds later on a confusing
docker/API error. This lives in dispatch_action itself
(action_requires_docker in scripts/dispatch.sh), so it applies no
matter which of run.sh/run-background.sh ends up running the action.
Provision Wizard
Click + next to the rescan button (↻) to open the wizard for setting up a brand-new server. It:
- Target folder — pick one of your discovered projects, or type a
custom path (Tab to autocomplete, same as search folders). Same
home-folder safety baseline as search folders — a path outside
~is flagged in red and blocks both the checks and Generate, andgenerate-provision.shrefuses to write outside$HOMEeven if it's called some other way. - Checks — runs
scripts/provision-check.shagainst that folder and shows: doesconfig/deploy.yml(ordeploy.<env>.yml) exist, does theGemfilehave thekamalandnet-sshgems, doesssh-add -lshow a loaded key, and whether aprovisionfile is already there (would be overwritten).
Target folder and Checks are shared — below them, Deploy and
Provision are two separate tabs, since deploy.yml and the provisioning
script are independent artifacts you might only want one of at a time.
Deploy is selected by default, since a fresh project needs
config/deploy.yml before provisioning even makes sense; switch to
Provision freely, your Deploy fields stay put. Styled as classic
attached tabs — rounded on top, flat on the bottom — with the active one
flush against the card holding its fields below it, and the inactive one
a separate closed pill beside it.
- Tailor (Provision tab) — swap size, storage path/owner, extra firewall ports beyond 22, Docker log rotation size/count, ulimit, and four toggles (UFW, fail2ban, unattended-upgrades, SSH hardening).
- Generate (Provision tab) — renders
templates/provision.erbwith those choices viascripts/generate-provision.sh(plain RubyERB, no gems needed beyond the stdlib) into<target>/provision,chmod +x, then rescans so the project's Provision button lights up immediately.
The template itself is the user's own Scaleway/Ubuntu Kamal provisioning
script (idempotent SSH-based steps: essentials, swap, storage dir, Docker +
kamal network, then the four toggleable hardening steps, then Docker
daemon log rotation), parameterized instead of hardcoded. A toggle turned
off removes that step from the generated script entirely rather than
leaving it disabled in place — re-run the wizard any time to regenerate with
different choices; it always overwrites, never merges.
Deploy config (Deploy tab)
The same wizard can also generate config/deploy.yml itself — the
provision script needs it anyway, since it reads servers: from that
file at runtime to know which hosts to SSH into. Type an Environment
(blank for the base config, or a name like staging/production — letters,
numbers, - and _ only, since it becomes part of the generated
filename — for a config/deploy.<env>.yml override), Role/Hosts (comma-separated
IPs or hostnames) plus an optional Cmd to override the container's
default command, a second role like workers is optional too, and click
Generate deploy.yml:
- Base config (blank environment) — only offered when
config/deploy.ymldoesn't exist yet. Rendersservice,image,retain_containers, andservers:from the form, then a full skeleton for everything a real deployment typically needs —proxy(SSL, app port, healthcheck, response timeout, request buffering),registry,env(clear/secret), andbuilder— followed by the exact same aliases/ssh/volumes/asset_path/boot/accessories guidancekamal initships (copied verbatim from the installedkamalgem's own template, never re-evaluated by this wizard's own ERB pass — so its one illustrative<%= %>example line stays literal, matching whatkamal inititself produces). Every value that isn't service/image/servers is either a genuine Kamal default (/uphealthcheck,Dockerfile,arch: amd64,retain_containers: 5) or a generic placeholder straight from Kamal's own configuration docs (DB_USER/DB_PASSWORDforenv,<your registry server>forregistry) — never made-up, and never copied from any one real project's actual values. - Named environment — a minimal,
servers:-only override, the normal Kamal pattern for adding a destination to an app that already has a base config. Ifconfig/deploy.ymldoesn't exist yet either, it's generated too (full skeleton, same Service/Image/servers from the form) — an override with nothing to override isn't useful on its own, and this saves a second trip through the wizard for a brand-new project. Once a base exists, later named-environment generations only ever touch that one override file.
The wizard will never let you regenerate an existing base config with a minimal one — that would silently delete every other setting in it. Want to change server IPs in an existing base file? Edit it by hand, or add a named environment override instead.
Customizing the commands
ruby provision <env> and bin/rails console come straight from the
original Rails-flavored kamal_menu() script this plugin is based on. If
your stack differs, edit the case "$ACTION" branches in
scripts/dispatch.sh — it's plain bash, one kamal ... (or arbitrary
command) per action, shared by both scripts/run.sh (terminal actions) and
scripts/run-background.sh (background actions), so a change here applies
to whichever of the two a given action actually runs through. (The
original script's Rack::Attack status check isn't included — too specific
to one app to generalize; add it back the same way if you use it.)
Permissions & dependencies
- Requires
kamal,jq,find,md5sum,awk, andruby(stdliberb+json, no gems) onPATH(all standard on an Omarchy/Arch install with Kamal already set up). Ifkamalisn't found when an action runs, bothscripts/run.shandscripts/run-background.shfail with a clear error rather than installing it for you — the plugin never runsgem installon your behalf. - Every background action fires one
notify-sendcall when it finishes (standard on any Omarchy/Arch install, part oflibnotify) — silently skipped if it's missing, the same way every other best-effort call in this plugin degrades. - Setup and Deploy additionally require the
dockerCLI onPATHand a reachable Docker daemon (docker info) — checked before either one spawnskamal, since both build and push an image from this machine. - Runs
bash scripts/discover.sh <folders...>to scan the filesystem and cache results at$XDG_RUNTIME_DIR/eduard.kamal-deploy/targets.json(or/tmp/eduard.kamal-deploy-$UIDif$XDG_RUNTIME_DIRisn't set — that fallback directory's ownership and permissions are checked, refusing to read or write through it if it's not exactly this user's own0700directory, since/tmpis shared). The file itself is never opened directly for writing (a fresh temp file is written instead andrename(2)d into place — atomic, and replaces a symlink at that path rather than following it) and every read is checked for a symlink first and capped at 4 MiB. - Every path field runs
bash scripts/complete-path.sh <partial>(read-only directory listing) on Tab, restricted to$HOMEthe same way every write path in this plugin is. - Runs
bash scripts/run.sh <target> <action> [accessory]inside your default terminal for Tail logs / Rails console / Bash shell / Rollback / an accessory's own Logs — realkamal,ruby, and shell commands, with your real credentials and SSH keys. Every other action runs the same way throughbash scripts/run-background.sh <target> <action> [accessory]instead, captured (not shown live) by the panel rather than opened in a terminal — same commands, same credentials, just no TTY; it refuses to run any of the terminal-only actions itself as a second guard against that mismatch. Reviewscripts/dispatch.sh(what each action actually runs) before installing if that matters to you. - The Provision Wizard runs
bash scripts/provision-check.sh <folder> [env](read-only) andbash scripts/generate-provision.sh <folder> <options>, which writes<folder>/provision(overwriting any existing file with that name) and marks it executable, and/orbash scripts/generate-deploy.sh <folder> <env> <options>, which writes<folder>/config/deploy.ymlor<folder>/config/deploy.<env>.yml(refusing to touch an existing base config — see Deploy config above). Neither ever opens the destination path directly: a base config is written withO_EXCL(atomically fails if anything — file or symlink — is already there, rather than trusting an earlier existence check);provisionand a named-environment override, which are meant to be regenerable, are written to a fresh temp file andrename(2)d into place, which replaces a symlink instead of following it. - Reads/writes
"eduard.kamal-deploy".searchPathsin~/.config/omarchy/shell.json. - Once per panel-open, runs
bash scripts/check-update.sh(read-only) — onecurlrequest (capped at 1 MiB) toraw.githubusercontent.comfor this repo's ownmanifest.json, silent on any failure including offline. Clicking the update icon it can reveal only displays instructions — nothing is fetched, run, or written on click. - Like every Quickshell plugin,
Panel.qmlruns unsandboxed inside the sharedomarchy-shellprocess — review it before installing.
Remove
omarchy plugin remove eduard.kamal-deploy
This deletes ~/.config/omarchy/plugins/eduard.kamal-deploy/ and removes
the widget from your bar layout. It does not revert the
"eduard.kamal-deploy" key in shell.json — delete it by hand, or run
omarchy refresh shell, if you want it gone too.
Files
| File | Purpose |
|---|---|
manifest.json |
Plugin manifest (bar-widget) |
Panel.qml |
Bar icon + task-list panel UI + Provision Wizard |
scripts/discover.sh |
Scans search folders for config/deploy*.yml, prints/caches JSON |
scripts/cache-dir.sh |
Resolves + validates the targets-cache directory, sourced by the three scripts below |
scripts/ensure-kamal.sh |
Installs the kamal gem if it's missing, sourced by run.sh and run-background.sh |
scripts/dispatch.sh |
Shared action→kamal/ruby command mapping, sourced by both scripts below |
scripts/run.sh |
Resolves a clicked target, runs it in a real terminal (interactive/streaming actions) |
scripts/run-background.sh |
Same resolution, runs in the background instead, output captured for the panel |
scripts/complete-path.sh |
Read-only Tab-completion for the panel's folder fields |
scripts/check-update.sh |
Read-only: compares installed vs. GitHub manifest.json version |
scripts/detect-common.sh |
Lists common project-folder names that exist under $HOME |
scripts/provision-check.sh |
Read-only pre-flight checks for the Provision Wizard |
scripts/generate-provision.sh |
Renders templates/provision.erb into <folder>/provision |
templates/provision.erb |
The parameterized provisioning script itself |
scripts/generate-deploy.sh |
Renders templates/deploy.erb (+ deploy-tail.yml for a new base config) into config/deploy.yml or config/deploy.<env>.yml |
templates/deploy.erb |
The service/image/servers: header this wizard controls |
templates/deploy-tail.yml |
Verbatim copy of kamal init's proxy/registry/builder/… guidance |
License
MIT — see LICENSE.