| name | coolify-remote |
| description | Manage a remote Coolify PaaS instance from the CLI — list apps, set env vars with post-write verification, flip domains, enable TLS, trigger deploys and wait for them to finish. Use whenever you need to mutate a Coolify app without clicking through the UI or hand-rolling curl + polling loops. |
| when_to_use | Trigger on phrases like "deploy to coolify", "set the env var on coolify", "change the domain", "enable https on coolify", "redeploy the app", "is the deploy done", "coolify status", "sync .env to coolify", "flip the hermes app to a real domain", anything mutating a Coolify-managed application. |
| allowed-tools | Bash(coolify-remote:*) Bash(python3 *coolify-remote*:*) |
coolify-remote
Stdlib-only Python 3 CLI wrapping Coolify's REST API. One file, no pip installs. Lives at ${CLAUDE_SKILL_DIR}/../../bin/coolify-remote; the plugin auto-adds bin/ to PATH.
When to reach for this
- User wants to change something on a Coolify app (env, domain, TLS, deploy).
- User asks about deploy status or wants to redeploy and confirm it worked.
- User says "the env var isn't showing up in the container" — see Env propagation gotcha below.
- User wants to sync a local
.env to a Coolify app.
Do NOT use for server provisioning — Coolify's server objects are already-bootstrapped hosts, not Hetzner VMs. For VM lifecycle use the (separate) hcloud CLI.
When NOT to use this — fall back to coolify CLI / Coolify API directly
This wrapper is deliberately narrow. It covers the mutations we do weekly (env, domain, TLS, deploy, exec) plus read-only listing of apps and servers. It does NOT wrap Coolify's full surface area. If the user wants to do something outside that scope, don't try to contort coolify-remote — hit the Coolify REST API directly with curl (auth with $COOLIFY_API_KEY) or use the Coolify web UI.
Specific cases where you should use raw curl/the Coolify API/the UI, not coolify-remote:
- Creating or deleting applications, services, or databases. The wrapper only manages apps that already exist. Use
POST /api/v1/applications/{public,private-github,dockerfile,...} or the UI's "New Resource" flow.
- Managing projects, teams, environments, or servers (add, delete, validate, reconfigure). No wrapper commands exist —
coolify-remote server list is read-only. Use /api/v1/projects, /api/v1/teams, /api/v1/servers directly.
- Database resources (Postgres, MySQL, Redis, Clickhouse, etc.). The wrapper's
app commands only target application resources. Database resources have their own endpoints (/api/v1/databases/*).
- Rolling back to a specific deployment, canceling an in-flight deploy, or streaming live build logs.
deploy --wait polls to a terminal state but can't cancel, roll back, or tail. Use the UI or /api/v1/deployments/{uuid} endpoints.
- Managing private keys, SSH keys, webhooks, or shared variables. Not wrapped — use the UI or
/api/v1/security/keys, /api/v1/shared-variables.
- Anything else the wrapper doesn't expose. The commands listed below are the complete surface; if it's not there, it's not wrapped.
Don't get stuck in a loop. If coolify-remote --help doesn't show a subcommand for what the user wants, or a command returns "no such subcommand", immediately switch to curl-ing the Coolify API (base: $COOLIFY_URL, Authorization: Bearer $COOLIFY_API_KEY) or the UI rather than re-trying coolify-remote with different flag combos. The wrapper's purpose is to collapse the common Coolify fumbles — not to be a full API client.
Configure
Layered config, highest precedence first:
--env-file <path>
.env.local / .env (walked up from cwd)
- Shell environment (including Claude Code settings)
Project .env files override the shell — drop a .env in the repo you're working in and it wins. Only COOLIFY_* keys are picked up.
COOLIFY_URL=http://1.2.3.4:8000
COOLIFY_API_KEY=...
COOLIFY_SSH_HOST=1.2.3.4
COOLIFY_SSH_USER=root
COOLIFY_SSH_KEY=~/.ssh/agent-plus
COOLIFY_SSH_PORT=22
Or globally in ~/.claude/settings.json:
{ "env": { "COOLIFY_URL": "...", "COOLIFY_API_KEY": "..." } }
If either is missing, the CLI prints a suggestion pointing to both locations.
Commands
coolify-remote app list
coolify-remote app show <name-or-uuid>
coolify-remote env list <app>
coolify-remote env list <app> --show
coolify-remote env set <app> KEY=val [KEY=val...] [--verify] [--deploy] [--wait]
coolify-remote env sync <app> .env [--prefix HERMES_] [--deploy] [--wait]
coolify-remote domain set <app> https://app.example.com --force-https --deploy --wait
coolify-remote tls enable <app> --domain https://app.example.com
coolify-remote deploy <app> [--wait]
coolify-remote app exec <app> -- <cmd>
coolify-remote app exec <app> -t -- <cmd>
coolify-remote server list
All list/show commands support --json for piping to jq.
Post-processing: prefer jq
When you need to extract or reshape fields from the JSON output, reach for jq rather than inline python3 -c "...". It's faster to type, more legible, and avoids the shell-escaping / multi-line string pitfalls that plague ad-hoc Python one-liners. jq is a single static binary available on every platform, so it's the portable choice for agent-driven post-processing pipelines.
Apps are resolved by name
Every command takes <app> which is matched against name, then uuid, then fqdn substring. You don't need to copy UUIDs around.
coolify-remote deploy hermes --wait
coolify-remote deploy b1c6e2f0-4a3d-4d77-ae6f-123456789ab --wait
Running commands inside a container (app exec)
Coolify has no REST exec endpoint — every obvious path (/applications/{uuid}/execute, /exec, /command, /run, /terminal, /shell on both application and server routes) returns HTTP 404. The web UI's terminal is a WebSocket feature gated by is_terminal_enabled. Rather than wrap that, this command SSHes to the Coolify host and runs docker exec on the matching container.
coolify-remote app exec hermes -- whoami
coolify-remote app exec hermes -- ls /data | head
coolify-remote app exec hermes -- sh -c 'cat /app/.env | wc -l'
coolify-remote app exec hermes -t -- sh
coolify-remote app exec hermes -v -- whoami
How it works: resolves the app by name → UUID, SSHes to $COOLIFY_SSH_HOST (or the hostname parsed from COOLIFY_URL), runs docker ps --filter name=<uuid> to find the live container (Coolify names them <uuid>-<timestamp>), then docker exec -i into it. Stdout, stderr, and exit codes all propagate to your local shell — so cron/skillify scripts can branch on $?.
What you need on the host: working key-based SSH as root (or whichever user can talk to the Docker socket). The CLI won't prompt for a password — if your key isn't already set up, COOLIFY_SSH_KEY=~/.ssh/mykey points it at one.
Cron-friendly pattern (inherited from Hermes's skillify model):
coolify-remote app exec hermes -- sh -c 'curl -sf http://localhost:3000/health' \
|| echo "hermes unhealthy" >&2
Container not running? The command exits with code 2 and a clear no running container found for app <uuid> message — distinguishable from a real command that happened to produce no output.
Coolify label quirk: an earlier implementation tried --filter label=coolify.applicationId=<uuid> — that label doesn't exist on Coolify's application containers (only on the proxy layer). Container-name-prefix matching works reliably across Coolify versions.
Windows / Git Bash gotcha: MSYS rewrites absolute Linux paths in arguments before they reach Python. coolify-remote app exec hermes -- cat /etc/os-release becomes cat C:/Program Files/Git/etc/os-release locally, then fails in the container. Two fixes, both reliable:
coolify-remote app exec hermes -- sh -c 'cat /etc/os-release'
MSYS_NO_PATHCONV=1 coolify-remote app exec hermes -- cat /etc/os-release
Affects every Windows CLI that shells out with Unix-style paths — not specific to this wrapper.
Env propagation gotcha (READ THIS)
Coolify stores env vars immediately but does not inject them into a running container. The container must be redeployed to see new values. This has burned us before: OPENAI_API_KEY was visible in Coolify's UI / env list but empty inside the container, because no redeploy had happened.
The right pattern:
coolify-remote env set hermes OPENAI_API_KEY=sk-... --verify --deploy --wait
--verify reads the envs back via API and confirms the value Coolify stored matches what you sent. Catches silent no-ops on write.
--deploy triggers a redeploy so the container actually picks up the change.
--wait blocks until the deployment finishes (status finished / failed).
--verify only checks Coolify's API, not the running container. To verify in-container, you either need to redeploy (--deploy) or exec into the container via the host. If an env refuses to propagate even after a successful redeploy, fall back to writing it via the app's own config system (e.g. Hermes config set, gbrain config set) — some apps layer their own config over env.
Domain + TLS workflow
Coolify's PATCH field for the domain is domains (not fqdn — fqdn is read-only on the application object). Setting fqdn returns HTTP 422. The wrapper uses the correct field; you don't need to remember.
Typical flow for flipping an app from the default sslip.io URL to a real domain with HTTPS:
coolify-remote tls enable hermes --domain https://hermes.example.com
tls enable does four things: PATCH domain + is_force_https_enabled=true, trigger deploy, wait for Let's Encrypt to issue, HEAD the HTTPS URL as a smoke test. Exits non-zero on any step failing.
If you only want to change the domain without touching TLS:
coolify-remote domain set hermes https://new.example.com --deploy --wait
Deploy with --wait
--wait polls /api/v1/deployments/<deployment_uuid> every 3s until it hits a terminal state (finished, failed, cancelled-by-*). Prints state transitions as they happen. Exits 0 only on finished.
This replaces the hand-rolled until [ "$(curl … | python3 -c …)" = "finished" ] loops that used to fail on the Windows bash shim.
Syncing a local .env to Coolify
coolify-remote env sync hermes .env --prefix HERMES_ --deploy --wait
Useful when you've been editing locally and want to push a subset of keys up. --prefix filters so you don't accidentally upload unrelated vars. Values are upserted (POST, falling back to PATCH on 422 "already exists").
Quirks
fqdn is read-only, domains is writable. The wrapper only accepts domains via domain set. Don't hand-roll PATCHes with fqdn.
is_force_https_enabled needs a redeploy to take effect — Traefik labels are recomputed at deploy time.
- Deploy trigger is
GET /api/v1/deploy?uuid=…&force=true, not POST. Yes, really.
- POST to
/api/v1/applications/<uuid>/envs returns 422 if the key exists; the wrapper retries with PATCH automatically.
--verify after env set checks Coolify's API, not the container. Only a redeploy puts new values into the container's process env.
Troubleshooting
HTTP 401: COOLIFY_API_KEY is wrong or expired. Generate a new one in Coolify → Keys & Tokens.
Validation failed on PATCH: you're sending the wrong field name. Use domain set / env set rather than raw PATCH. If the wrapper itself 422s, check coolify-remote app show <app> against Coolify's API docs — the schema shifts between versions.
- Deploy hangs on
queued forever: another deploy is in flight, or the worker is stuck. Check coolify-remote app show <app> and the Coolify web UI.
- TLS smoke test fails after
tls enable: DNS hasn't propagated, or Let's Encrypt rate-limited you (5 duplicate certs / week). Check dig +short <host> and Coolify's deployment logs.