Skip to content

Latest commit

 

History

106 Commits

Folders and files

NameName
Last commit message
Last commit date
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

cleat-cli

cleat is the command-line client for Cleat, a self-hosted PaaS that deploys Phoenix/Elixir, Go, Node (Next.js / TanStack Start), Ruby on Rails, Rust (Loco) and static apps to Hetzner Cloud and AWS Lightsail over SSH.

It talks to the panel's JSON API (/api/v1) to list servers and apps, manage env vars, trigger deploys, and watch build and runtime logs.

Install

Requires Elixir 1.17+ / OTP 27+. CI also runs Elixir 1.19 / OTP 28.

git clone https://github.com/cleat-cloud/cleat-cli.git
cd cleat-cli
mix deps.get
mix install

mix install builds the escript and copies it to ~/.local/bin (override with mix install /usr/local/bin or CLEAT_INSTALL_DIR). Make sure the destination is on your PATH, then check cleat version.

API contract

The panel is the source of truth: cleat-deploy priv/api_contract.json (CleatDeployWeb.Api.Contract). This repo vendors a copy at test/fixtures/api_contract.json.

CI checks the fixture against cleat-cloud/cleat-deploy main. Locally:

# sibling checkout of the panel
mix test test/cleat/contract_test.exs
scripts/check_panel_contract.sh

# or point at a specific file
CLEAT_DEPLOY_CONTRACT=/path/to/cleat-web/priv/api_contract.json mix test

When the panel adds a resource or key (compatible change): merge the panel PR first, then copy priv/api_contract.json into the fixture and list the new resource in test/cleat/contract_test.exs @used.

When the panel renames or drops a key the CLI reads (breaking change): update the CLI in the same window as the panel merge. mix test fails on missing keys even before the full-file diff.

Quick start

# Point at your panel and authenticate. Credentials can be passed as flags.
cleat login --panel https://panel.example.com --email you@example.com

# Inspect what you have access to.
cleat whoami
cleat servers list
cleat apps list

# Trigger a deploy and watch it build.
cleat deploy my-app --watch

# Follow a build log.
cleat logs 42 --follow

cleat login exchanges your email/password for a personal access token and stores it in ~/.config/cleat/config.json. The token is shown only once by the panel and is stored as a hash server-side.

Commands

Command Description
cleat login Authenticate and store a token
cleat logout Revoke the token and clear local credentials
cleat whoami Show the authenticated user and tenant
cleat init Write .cleat_deploy/deploy.json in the current project
cleat config Show or edit CLI configuration
cleat servers list List servers
cleat servers show ID Show one server
cleat apps list List apps
cleat apps show APP Show one app (by id or slug)
cleat apps create Create an app
cleat apps update APP Edit repo / branch / auto-deploy / host / port / runtime
cleat apps logs APP App journal, with --tail, --since, --grep, --follow
cleat servers logs ID Host journal, with --unit, --tail, --since, --grep, --follow
cleat events [APP] Search collected log events, with --server, --unit, --query, --severity, --min-severity, --since, --until, --limit
cleat env list APP List env vars (secrets masked)
cleat env set APP K=V Upsert one or more env vars
cleat env unset APP KEY Delete an env var
cleat deploy APP Trigger a deploy (by id or slug)
cleat deploy --repo O/R Register if needed, then deploy
cleat drop [DIR|FILE] Publish a local folder or file, no git (Netlify-Drop style)
cleat cancel APP Cancel the active deploy
cleat status APP List recent deployments for an app
cleat logs DEPLOYMENT_ID Print a deployment's build log
cleat mcp Run as a Model Context Protocol server on stdio

Run cleat help for the full option list.

Agent integration (MCP)

cleat can run as a Model Context Protocol server on stdio, so coding agents (Claude Code, Codex, Grok) can deploy and inspect apps as native tools instead of shelling out:

claude mcp add cleat -- cleat mcp
codex  mcp add cleat -- cleat mcp
grok   mcp add cleat -- cleat mcp

Tools: whoami, servers_list, server_logs, logs_search, apps_list, apps_show, apps_create, apps_update, apps_logs, env_list, env_set, env_unset, deploy, deploy_status, deploy_logs, cancel_deploy, drop, init_project.

deploy queues and returns a deployment_id; follow it with deploy_status and deploy_logs. Credentials come from cleat login (or CLEAT_PANEL_URL / CLEAT_TOKEN).

Project manifest

cleat init writes .cleat_deploy/deploy.json, the same manifest the panel reads to decide how to build an app:

{
  "runtime": "phoenix",
  "release_name": "my_app",
  "memory_max_mb": 400
}

For Go projects it detects binaries under cmd/*/main.go:

{
  "runtime": "golang",
  "binaries": ["server", "worker"]
}

For static sites, cleat init detects the absence of mix.exs/go.mod plus an index.html or package.json, or you can force it with cleat init --runtime static:

{
  "runtime": "static"
}

The panel optionally runs npm ci && npm run build and serves the output directory (dist, build, public, _site, out, or build_dir) through Caddy with an SPA fallback — no runtime process. Plain HTML/CSS/JS folders with an index.html at the repo root are published as-is, no build step.

For server-rendered JS (Next.js, TanStack Start), cleat init detects next or a @tanstack/*-start dependency and picks the node runtime. The panel installs Node, runs npm ci && npm run build, and keeps the app alive with a systemd unit behind Caddy (reverse_proxy). devDependencies are pruned after the build (npm prune --omit=dev), and TanStack Start / Nitro releases ship no node_modules at all since the .output bundle is self-contained. The start command is resolved at build time:

  1. start_command, if set in the manifest
  2. npm run start, if the project declares a start script
  3. node .output/server/index.mjs for TanStack Start / Nitro output
  4. npm exec -- next start for Next.js output
{
  "runtime": "node",
  "build_command": "npm run build",
  "start_command": "npm start",
  "node_version": "22"
}

Only runtime is required; build_command, start_command, and node_version (uses the latest 22.x when omitted) are optional overrides. Point build_dir at a subdirectory for monorepos ({"runtime": "node", "build_dir": "web"}).

For Ruby on Rails, cleat init detects a Gemfile plus config/application.rb (or a rails gem) and picks the rails runtime. The panel installs Ruby via mise, runs bundle install, assets:precompile and db:prepare, then serves the app with Puma behind Caddy. DATABASE_URL, SECRET_KEY_BASE and RAILS_MASTER_KEY come from the panel env vars.

{
  "runtime": "rails",
  "ruby_version": "3.3.6",
  "start_command": "bundle exec puma -C config/puma.rb"
}

Ruby version resolution order: ruby_version → .ruby-version → the Gemfile ruby directive → 3.3.6. Start command: start_command → bundle exec puma -C config/puma.rb → bundle exec puma -b tcp://0.0.0.0:$PORT.

For Rust / Loco, cleat init detects a Cargo.toml and picks the rust runtime. The panel installs rustup, runs cargo build --release, publishes bin/server plus config//assets//frontend/, and starts a systemd unit. Loco apps (loco-rs or config/production.yaml) use ./bin/server start and ./bin/server db migrate. Set DATABASE_URL and JWT_SECRET as panel env vars.

{
  "runtime": "rust",
  "start_command": "./bin/server start"
}

Commit the manifest so the panel picks it up on the next deploy.

Logs

apps logs reads an app's systemd journal; servers logs reads the host journal (all units, or one with --unit):

cleat apps logs lumina --tail 500 --since 1h --grep error
cleat servers logs 5 --unit caddy --since 30m

--since accepts 30m, 2h, 1d or an ISO date (2026-09-21, 2026-09-21 14:30). --grep is a case-sensitive substring filter. --tail defaults to 200 (max 5000). --follow keeps polling and prints new lines.

Collected log events

apps logs and servers logs read the live journal. cleat events searches the panel's collected log store instead: events are enriched with tenant, app, deploy, server and unit by the panel's collector, so they can be filtered by app, severity and time window without touching the VM.

# errors and worse for one app in the last hour
cleat events my-app --min-severity err --since 1h

# free-text search across the tenant, newest first
cleat events --query "timeout" --limit 50

# exact level, specific unit, machine-readable
cleat events --app landing --severity warning --unit caddy --json

# filter by release sha and cluster similar errors
cleat events my-app --release abc123 --group

Levels: emerg, alert, crit, err, warning, notice, info, debug. --min-severity includes that level and above. The collector is opt-in on the panel (LOG_COLLECTOR_ENABLED=true); if it is off, cleat events returns no rows.

Environment variables

Env vars are stored encrypted by the panel and written to the server (/etc/<app>/env) during a deploy. They are masked by default:

cleat env list my-app              # secrets shown as •••
cleat env list my-app --reveal     # show values in the clear

cleat env set my-app DATABASE_URL=libsql://... SECRET_KEY_BASE=...
cleat env set my-app FOO=bar --deploy   # apply immediately (one deploy)

cleat env unset my-app OLD_KEY --deploy

Keys must be UPPER_SNAKE_CASE. A value with = (KEY=a=b) is supported. Without --deploy, run cleat deploy APP to apply the changes.

Deploying a repo

Deploy an app that is already registered:

cleat deploy my-app --watch
cleat deploy my-app --ref deploy-cleat        # override the branch

Register and deploy in one shot. If the repo is unknown, pass --server and --host; the app is created with sane defaults (slug from the repo name). The runtime is detected from the local project the same way cleat init does, so a Next.js / TanStack Start repo registers as node; override it with --runtime (phoenix, golang, node, rails, rust or static):

cleat deploy --repo owner/repo --server 3 --host repo.example.com --watch
cleat deploy --repo owner/lumina --server 3 --subdomain lumina --runtime node

Edit an existing app (including its runtime) and cancel a stuck deploy:

cleat apps update my-app --branch develop --no-auto-deploy
cleat apps update my-app --runtime node
cleat cancel my-app

Drop (no git)

cleat drop packages a local folder or a single file and publishes it as a static site — no repository needed. It works exactly like Netlify Drop:

# against an existing static app
cleat drop ./dist --app landing --watch

# create the static app on the fly (server required; host optional for plain static)
cleat drop ./site --server 3 --host landing.example.com --watch

# a single file: uploaded as index.html (or its own name when not HTML)
cleat drop ./almanaque.html --server 3 --host almanaque.example.com --watch

Plain static sites get a host automatically. When the target is a single .html/.htm file, or a directory with an index.html and no build manifest (package.json, mix.exs, go.mod), you can publish without --host:

cleat config set sites_base_domain sites.example.com   # once

cleat drop ./minha-loja --server 3 --watch
# → minha-loja.sites.example.com

cleat deploy --repo owner/minha-loja --server 3        # same detection

Re-running a drop against the same slug updates the existing static app instead of creating a new one. Pass --host/--subdomain to override, or --sites-base-domain for a one-off. A non-static target without a host still asks for one.

.git, node_modules and .DS_Store are excluded from the upload. The panel enforces a size limit (default 50 MB, CLEAT_DROP_MAX_BYTES to override) and only accepts drops for apps with runtime: "static".

Configuration

Resolution order for both the panel URL and the token:

  1. --panel / --token flags
  2. CLEAT_PANEL_URL / CLEAT_TOKEN environment variables
  3. ~/.config/cleat/config.json written by cleat login

Set CLEAT_CONFIG to use a different config file path. Add --json to any read command for machine-readable output.

Named subdomains

Create one DNS wildcard (*.sites.example.com → your server) and let the CLI build the host for you. Set the base domain once:

cleat config set base_domain sites.example.com

Then --subdomain expands to <name>.<base_domain>:

cleat drop ./site --server 3 --subdomain exemplo --watch
# → exemplo.sites.example.com

cleat deploy --repo owner/repo --server 3 --subdomain exemplo --runtime static --watch

--base-domain overrides the stored value per command, and CLEAT_BASE_DOMAIN sets it via the environment. Use --host for a full domain. The panel issues a Let's Encrypt certificate per host automatically; it does not need to know the base domain.

For static sites, set sites_base_domain to give plain static drops and deploys a base domain when they omit --host:

cleat config set sites_base_domain sites.example.com

Override it per command with --sites-base-domain, or via CLEAT_SITES_BASE_DOMAIN.

Development

mix test
mix precommit

License

MIT

About

Command-line client for Cleat — deploy and manage Phoenix/Elixir, Go, Node, Rails and static apps on your own servers

Topics

Resources

Stars

0 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages