Every CasaOS and ZimaOS box ships a command line tool called casaos-cli, and almost nobody documents it. The official README covers about half the commands. Several of the ones it does cover fail on current releases, and the failures are different on CasaOS and ZimaOS because ZimaOS runs a forked backend behind the same client.
This CasaOS CLI and ZimaOS CLI cheat sheet comes from running the casaos-cli subcommands on real systems and recording what actually happened: app install, apply, update and uninstall, app stores, global variables, message bus subscriptions and the health checks. It also covers the ZimaOS-only commands (zimaos, rauc) and the service names that change between the two. The testing ran in September 2026 on CasaOS v0.4.15 (most of it on Debian 13, with install and uninstall repeated on Debian 12) and on ZimaOS v1.7.1 on a ZimaBoard 2. The matrix below marks anything we did not run.
Which casaos-cli commands work on CasaOS and ZimaOS
Both platforms report casaos-cli 0.4.4, but they are different builds (November 2023 on CasaOS, April 2025 on ZimaOS) talking to different backends. The matrix is what we measured, not what the help text promises:
| Command | CasaOS on Debian | ZimaOS |
|---|---|---|
app-management list apps | Works | Works |
app-management list app-stores | Works | Works |
app-management search | Works (633 apps, see the store note below) | Fails: 502, backend panics |
app-management install / start / stop / restart / logs | Works | Works |
app-management show local | Works | Works |
app-management apply | Works | Not tested |
app-management update app | Works | Works |
app-management uninstall | Works | Works |
app-management set global / show global | Works (no key argument) | show global works |
app-management register app-store | Works | Not tested |
message-bus list event-types | Works | Returns nothing |
message-bus subscribe socketio / websocket | Works (one event name at a time) | Not tested |
message-bus trigger, subscribe websocket actions | Not tested (no action types registered) | Not tested |
convert appfile, local-storage set merge | Not tested | Not tested |
local-storage list merges | Fails: 503 | Fails: 404 |
user list events | Works (empty) | Fails: 404 |
healthcheck services / ports-in-use / logs | Fails: 502 | Fails: 404 |
qrcode, version, completion | Works | Works |
The short version: app lifecycle commands are reliable on both, the three healthcheck commands are dead on both, and search is broken on ZimaOS. Here is the same client on each box:

And on ZimaOS, where search and the health check fail with different codes:

casaos-cli quick reference
The commands worth knowing, in one table (the rarely useful and broken ones are covered in the matrix above). Most have aliases (ls for list, up for install, down for uninstall), which are listed where they save typing:
| Task | Command |
|---|---|
| List installed apps | casaos-cli app-management list apps |
| List app stores | casaos-cli app-management list app-stores |
| Browse the store | casaos-cli app-management search [-c Media] [-t official|by_casaos|community] [-r] |
| Show an app’s config | casaos-cli app-management show local <appid> [--yaml] |
| Validate a compose file | casaos-cli app-management install -d -f docker-compose.yml |
| Install from a compose file | casaos-cli app-management install -f docker-compose.yml (alias up) |
| Start, stop, restart | casaos-cli app-management start|stop|restart <appid> |
| Last N log lines | casaos-cli app-management logs <appid> -l 50 |
| Change an installed app | casaos-cli app-management apply <appid> -f new.yml [-d] |
| Update an app | casaos-cli app-management update app <appid> [-f] |
| Uninstall, keep AppData | casaos-cli app-management uninstall <appid> -n (alias down) |
| Set a variable for every app | casaos-cli app-management set global <KEY> <value> |
| List global variables | casaos-cli app-management show global |
| Add or remove a store | casaos-cli app-management register|unregister app-store <url|id> |
| List bus event types | casaos-cli message-bus list event-types |
| Stream every bus event | casaos-cli message-bus subscribe socketio |
| Stream one source | casaos-cli message-bus subscribe websocket -s <source> events [-n <event>] |
| QR code for the web UI | casaos-cli qrcode |
| Talk to another API address | casaos-cli -u localhost:18080 ... (host:port, no http://) |
| Shell completion | casaos-cli completion bash |
Get a shell on CasaOS or ZimaOS
On CasaOS you already have one: it is a normal Debian or Ubuntu server (often a small box from our homelab mini PC shortlist), and our CasaOS on Debian guide covers the install. ZimaOS keeps SSH off until you enable it under Settings, General, Developer mode, then SSH Access. The login is your ZimaOS account, not root. On our ZimaBoard 2 that account sits in the wheel group, so sudo works with its password, and the ZimaBoard 2 setup guide shows the toggle.
casaos-cli itself needs no sudo on either platform. Without -u it reads the gateway port from /etc/casaos/gateway.ini (80 on all our boxes) and calls the local API, which accepts any request from loopback. The docker CLI is different: as a normal ZimaOS user, docker ps fails with permission denied while trying to connect to the Docker daemon socket at unix:///var/run/docker.sock, so prefix Docker commands with sudo.
Check versions and services
Each platform has its own version command. On CasaOS:
casaos -v
casaos-cli version
The CasaOS release and the CLI build it bundles:
v0.4.15
0.4.4
(build time: 2023-11-20T09:52:33Z, commit: 51949f2cdc71eaec41db443fb93d8178ca6e5192)
ZimaOS replaces casaos with a zimaos binary. zimaos -h lists four flags: -v for the version, -license-info for the licence status, -c for a config address, and -u, described as “update system (optionally specify version, e.g., -u or -u 1.2.3)”. We did not run -u. We updated our board through Settings, Update instead, which is the documented path and shows the release notes first.
zimaos -v
ZimaOS adds the build commit and date:
v1.7.1
git commit: a6e52af55e44e97f9e1284896e85efbcc1dc1cc6
build date: 2026-08-05T09:00:34Z
The service names are where scripts written for one platform break on the other. ZimaOS renamed most units but kept the CasaOS config paths:
| Role | CasaOS unit | ZimaOS unit |
|---|---|---|
| Web UI and API gateway | casaos-gateway | zimaos-gateway |
| Main service | casaos | zimaos |
| Apps and stores | casaos-app-management | zimaos-app-management |
| Users and login | casaos-user-service | zimaos-user |
| Disks and pools | casaos-local-storage | zimaos-local-storage |
| Event bus | casaos-message-bus | zimaos-message-bus |
| Cloud mounts | rclone | rclone |
| Extra on ZimaOS | n/a | zimaos-virt-management, icewhale-files, casaos-installer |
| Config directory | /etc/casaos | /etc/casaos |
| State and databases | /var/lib/casaos | /var/lib/casaos |
Listing both name patterns in one command works on either system:
systemctl list-units 'casaos*' 'zimaos*' rclone.service --no-pager --no-legend
On ZimaOS, rauc status shows which of the two system slots booted. That matters before an update, because a bad update rolls back to the other slot. Our board was running from slot B:
rauc status
The first block names the booted slot, and the slot list shows both kernels marked good. The zimaos-zimacube compatible string is not a typo. It is named after IceWhale’s ZimaCube NAS line, which still ships ZimaOS on the current ZimaCube 2, and our ZimaBoard 2 reports it as well.
=== System Info ===
Compatible: zimaos-zimacube
Variant:
Booted from: kernel.1 (B)
=== Bootloader ===
Activated: kernel.1 (B)
=== Slot States ===
[boot.0] (/dev/disk/by-partlabel/casaos-boot, vfat, inactive)
○ [kernel.0] (/dev/disk/by-partlabel/casaos-kernel0, raw, inactive)
bootname: A
boot status: good
[rootfs.0] (/dev/disk/by-partlabel/casaos-system0, raw, inactive)
⏺ [kernel.1] (/dev/disk/by-partlabel/casaos-kernel1, raw, booted)
bootname: B
boot status: good
[rootfs.1] (/dev/disk/by-partlabel/casaos-system1, raw, active)
List, search and inspect apps
list apps is the command you will run most. It shows each app ID, its state and the web UI address:
casaos-cli app-management list apps
The app ID in the first column is what every other command takes:
APPID STATUS WEB UI IMAGES DESCRIPTION
----- ------ ------ ------ -----------
jellyfin running http://192.168.1.50:8097/ linuxserver/jellyfin:10.11.10 No description available
uptimekuma running http://192.168.1.50:3001/ louislam/uptime-kuma:1.23.10-alpine It is a self-hosted monitoring tool like "Uptime Robot".
search does not take a search term. It dumps the whole catalogue (633 apps across both stores on our CasaOS box), so filter it with grep, or narrow it with -c for a category, -t for the author type, or -r for the store’s recommended list:
casaos-cli app-management search | grep -i -E '^(nextcloud|immich) '
Both apps are in the store, from different maintainers:
immich Media Cp0204 alextran1502
nextcloud Productivity ZimaOS Team Nextcloud
Category names must match the store exactly. The ones in use were Home, Others, Media, Productivity, Developer, Networking, AI, Social and Finance. An unknown name such as -c Utilities returns Error: no compose app found from this store rather than an empty list.
To see how an installed app is wired, show local prints its ports, volumes and containers. Add --yaml for the full Compose file, which is also the starting point for apply:
casaos-cli app-management show local uptimekuma --yaml
The YAML is the stored Compose project, with memory already converted to bytes and the store’s icon label added:
name: uptimekuma
services:
uptimekuma:
container_name: uptimekuma
deploy:
resources:
reservations:
memory: "134217728"
image: louislam/uptime-kuma:1.23.10-alpine
labels:
icon: https://cdn.jsdelivr.net/gh/IceWhaleTech/CasaOS-AppStore@main/Apps/UptimeKuma/icon.png
network_mode: bridge
Install an app from the command line
install takes a Compose file, not an app name. Store apps are Compose files already on disk under /var/lib/casaos/appstore, one folder per store. Find the one you want:
sudo find /var/lib/casaos/appstore -ipath '*/Apps/Syncthing/docker-compose.yml'
Three copies came back on our Debian 13 box, and they are not the same:
/var/lib/casaos/appstore/default/Apps/Syncthing/docker-compose.yml
/var/lib/casaos/appstore/casaos.app/4e6bf64b187e30664ab8e6ed48331e96/build/sysroot/var/lib/casaos/appstore/default.new/Apps/Syncthing/docker-compose.yml
/var/lib/casaos/appstore/github.com/2a1238d53212bfce4e8f861dcb8ef3fe/big-bear-casaos-master/Apps/syncthing/docker-compose.yml
Use the copy under a default.new folder (the live official store, which sits under casaos.app/ on CasaOS and cdn.jsdelivr.net/ on ZimaOS) or the BigBear one, not default/. The default folder is a snapshot the installer unpacked, and it goes stale. It still pinned Uptime Kuma 1.23.10 while the live store had already moved to 2.4.0, and running the update script did not refresh it. Validate the file with a dry run, then install:
APP_FILE=$(sudo find /var/lib/casaos/appstore -path '*/default.new/Apps/Syncthing/docker-compose.yml' | head -1)
casaos-cli app-management install -d -f "${APP_FILE}"
casaos-cli app-management install -f "${APP_FILE}"
The same find pattern returned the official store copy on both CasaOS and ZimaOS. The dry run validates and stops, and the real install returns in under a second because the work happens in the background:
2026/09/28 22:46:28 only validation has been done because `dry_run` is specified - skipping compose app installation
2026/09/28 22:46:28 compose app is being installed asynchronously
Your own Compose files work the same way. On ZimaOS we installed a traefik/whoami file with a minimal x-casaos block (main, title, port_map), and it appeared in list apps and on the dashboard like a store app. If a published port is already taken, the install is rejected up front with 400 Bad Request - there are ports in use.
Wait for asynchronous operations
Every lifecycle command returns “asynchronously” and exits before the work is done: install, apply, start, stop, restart, update and uninstall. Chain them without waiting and they race. We ran update app -f, then apply, then uninstall on the same app back to back. The uninstall finished first and CasaOS forgot the app, and then the still-running apply recreated the container. That left a live container on port 3001 that list apps no longer showed and the UI could not manage. Only sudo docker rm -f removed it.
In scripts, poll list apps until the state you asked for shows up before sending the next command, with a cap so a failed app cannot hang the script. The trailing space in the pattern stops uptimekuma from matching an app called uptimekuma2:
APP=uptimekuma
casaos-cli app-management stop "${APP}"
timeout 120 sh -c "until casaos-cli app-management list apps | grep -qE '^${APP} +exited'; do sleep 2; done"
casaos-cli app-management start "${APP}"
timeout 120 sh -c "until casaos-cli app-management list apps | grep -qE '^${APP} +running'; do sleep 2; done"
For an uninstall, wait until the app disappears from the list: until ! casaos-cli app-management list apps | grep -q "^${APP} "; do sleep 3; done.
Start, stop, restart and read logs
Unlike install, these take the app ID. logs prints the last N lines from every container in the app and does not follow, whatever its help text says:
casaos-cli app-management logs uptimekuma -l 5
The output is Compose-style, prefixed with the container name:
(showing last 5 lines)
...
uptimekuma | 2026-09-28T22:13:09Z [SERVER] INFO: No user, need setup
uptimekuma | 2026-09-28T22:13:09Z [SERVER] INFO: Adding route
uptimekuma | 2026-09-28T22:13:09Z [SERVER] INFO: Adding socket handler
uptimekuma | 2026-09-28T22:13:09Z [SERVER] INFO: Init the server
uptimekuma | 2026-09-28T22:13:09Z [SERVER] INFO: Listening on 3001
For a live tail, use sudo docker logs -f <container>. The container name is in the show local output and usually matches the app ID.
Change an installed app with apply
apply replaces an app’s Compose definition and recreates it. It is the CLI equivalent of editing an app’s settings in the UI. Export the current file, edit it, dry-run it, apply it. This moves Uptime Kuma’s host port from 3001 to 3002:
casaos-cli app-management show local uptimekuma --yaml > /tmp/uk.yml
sed -i 's/published: "3001"/published: "3002"/' /tmp/uk.yml
casaos-cli app-management apply uptimekuma -d -f /tmp/uk.yml
casaos-cli app-management apply uptimekuma -f /tmp/uk.yml
Docker confirms the new mapping once the recreate finishes:
uptimekuma 0.0.0.0:3002->3001/tcp, [::]:3002->3001/tcp Up 1 second (health: starting)
The WEB UI column in list apps still said 3001 afterwards. The exported file still carried port_map: "3001" in its x-casaos block, and that key is what the dashboard link uses, so change both when you move a port. If the export step fails (for example because the app ID is wrong, which returns 404 Not Found - compose app `nosuchapp` not found), the shell redirect still leaves an empty file behind, and applying it fails with 400 Bad Request - request body has an error: value is required but missing. Check that the YAML file is not empty before you apply it.
Update and uninstall apps
update app compares the installed app with its store entry and does nothing if they match:
casaos-cli app-management update app uptimekuma
Our app had been installed from the stale snapshot, and the plain update still called it current:
2026/09/28 22:13:39 compose app `uptimekuma` is up to date
Adding -f skips the check and pulls whatever the live store pins. For Uptime Kuma that was a jump from 1.23.10 to 2.4.0, a major release with a database migration. Back up /DATA/AppData/<app> before a forced update, because there is no rollback command.
uninstall removes the containers and the app’s folder in /var/lib/casaos/apps. Without flags it also deletes /DATA/AppData/<app>, which is where the app’s data lives. We confirmed both: Syncthing’s AppData folder was gone after a plain uninstall, and Uptime Kuma’s survived -n. Use -n unless you really want the data gone. If an app has outgrown the store template, the kept AppData also makes it easy to move to a standalone setup such as our Uptime Kuma on Debian guide:
casaos-cli app-management uninstall uptimekuma -n
Set global environment variables for all apps
A global variable is injected into every container CasaOS starts, unless the app’s own Compose file already defines a variable with the same name, in which case the app’s value wins. It is the clean way to set a time zone or an API key once instead of per app:
casaos-cli app-management set global TZ Africa/Nairobi
casaos-cli app-management show global
The list includes a placeholder OpenAI key that every install ships with:
Global Key Global Value
-------------- ------------
OPENAI_API_KEY sk-xxxxxx
TZ Africa/Nairobi
Three details from testing. The values live in plain text in /etc/casaos/env. A running container only picks up a new value when it is recreated (after an update or apply, our Uptime Kuma container showed TZ=Africa/Nairobi in docker inspect). And show global <Key>, as the help text documents it, fails with Error: accepts 0 arg(s), received 1. Run show global with no argument and grep the output.
Manage app stores
A fresh CasaOS install registers two stores, and ZimaOS points its official store at a jsDelivr URL instead of casaos.app. List them to get the IDs:
casaos-cli app-management list app-stores
On Debian the official store is ID 0 and BigBear is ID 1:
ID URL STORE ROOT
-- --- ----------
0 https://casaos.app/store/main.zip /var/lib/casaos/appstore/casaos.app/4e6bf64b187e30664ab8e6ed48331e96/build/sysroot/var/lib/casaos/appstore/default.new
1 https://github.com/bigbeartechworld/big-bear-casaos/archive/refs/heads/master.zip /var/lib/casaos/appstore/github.com/2a1238d53212bfce4e8f861dcb8ef3fe/big-bear-casaos-master
Add a store by the URL of its zip with register app-store <url> and drop one by ID with unregister app-store <id>. Registering a URL that is already there returns 409 Conflict - appstore source already exists. Re-registering is also the fix for a broken store: after the CasaOS update script ran, our Debian box listed only 177 apps because app-management was looking for BigBear under a folder that did not exist (lstat /var/lib/casaos/appstore/github.com/1dfcdd9e390897b516b1d990c9197b56: no such file or directory in its log). unregister app-store 1 followed by register app-store with the same URL brought the count back to 633. If you only want apps from the official store, unregistering BigBear also removes the duplicate entries from UI search.
Watch events on the message bus
Every install, uninstall, disk hot-plug and dashboard refresh goes over an internal message bus, and the CLI can listen in. That is useful for scripting (react when a USB disk appears) and for debugging an install that hangs. See what exists first:
casaos-cli message-bus list event-types
On CasaOS we counted 44 app-management events, 65 from the web UI, 3 from casaos (including casaos:system:utilization) and 2 from local-storage (local-storage:disk:added and local-storage:disk:removed), plus one from CasaOS-UserService. The action-type list was empty, so message-bus trigger has nothing to trigger on a stock install. On ZimaOS the event-type list came back empty too.
subscribe socketio streams everything. For one source, the websocket form needs -s before the subcommand:
casaos-cli message-bus subscribe websocket -s casaos events -n casaos:system:utilization
A utilization event arrives every five seconds with CPU, memory, disk and network counters as JSON strings:
2026/09/28 22:54:02 subscribed to ws://localhost:80/v2/message_bus/event/casaos?names=casaos:system:utilization via websocket
{
"name": "casaos:system:utilization",
"properties": {
"sys_cpu": "{\"model\":\"intel\",\"num\":2,\"percent\":36.3,\"power\":{\"timestamp\":\"1790636044\",\"value\":\"0\"},\"temperature\":0}",
"sys_disk": "{\"avail\":18772983808,\"health\":true,\"size\":26206609408,\"used\":6317686784}",
"sys_mem": "{\"available\":2401153024,\"free\":306978816,\"total\":4112314368,\"used\":1012670464,\"usedPercent\":24.6}",
The help says -n takes names separated by commas. It does not: -n app:install-begin,app:install-end fails with websocket.Dial ... bad status, while each name on its own subscribes fine. Leave -n off to get every event from that source, or run one subscriber per event name.
Run casaos-cli against a remote box
The global -u flag points the client at another API address. It takes a bare host:port. With a scheme it builds http://http//localhost:80/... and fails. Aimed straight at another server’s LAN IP it fails, because the API only skips authentication for loopback, and the CLI has no token option:
Error: 401 Unauthorized - Unauthorized
An SSH tunnel turns your request into a loopback request on the far side, and then it works. We ran this from the ZimaBoard against the Debian 13 CasaOS box:
ssh -f -N -L 18080:localhost:80 [email protected]
casaos-cli -u localhost:18080 app-management list apps
The remote app list came back. One oddity: the WEB UI column showed the local machine’s IP, not the server’s, because the client builds those links from its own address. The same loopback trust means anyone with SSH access to a CasaOS or ZimaOS box has full app control through this API, and so does any container you run with network_mode: host. The security section of our CasaOS install guide has the full test.
CasaOS-only and ZimaOS-only maintenance commands
These are not part of casaos-cli, but they are the other commands people look for:
| Task | CasaOS on Debian/Ubuntu | ZimaOS |
|---|---|---|
| Update the platform | curl -fsSL https://get.casaos.io/update | sudo bash | Settings, Update (zimaos -u exists, not tested by us) |
| Uninstall | sudo casaos-uninstall (interactive) | Not available, it is the OS |
| Forgotten web UI password | Move /var/lib/casaos/db/user.db aside, restart casaos-user-service | IceWhale docs: delete /var/lib/casaos/db/user.db and reboot (not tested by us; the board also has user.db-wal and user.db-shm, so move all three aside rather than deleting) |
| Show the booted system slot | n/a | rauc status |
| Where user data lives | /DATA | /DATA (its own partition) |
| Change the IP address | Debian networking | Settings, Network, or nmcli |
The Debian side of that table, with real output for the update, reset and uninstall, is in the CasaOS on Debian 13 / 12 guide. For the ZimaOS network commands, the ZimaOS static IP guide covers both the dashboard and nmcli. IceWhale also publishes a repair script, zimaos-fix.sh, in its ZimaOS GitHub repository. It replaces zero-byte files under /mnt/overlay/etc with the originals from the read-only root, and runs resize2fs on the data partition only when that filesystem is under 2 GB. It does not reboot for you. We did not need or run it, so read it before you do.
Enable shell completion
The subcommand tree is deep enough that tab completion pays for itself. On Debian with bash-completion installed:
casaos-cli completion bash | sudo tee /etc/bash_completion.d/casaos-cli > /dev/null
Open a new shell and casaos-cli app-management <Tab> lists the subcommands. completion zsh, fish and powershell exist too. On ZimaOS, /usr is a read-only overlay (writing there returns Read-only file system), so load the script from your own ~/.bashrc with source <(casaos-cli completion bash). We confirmed that registers the completion for the ZimaOS login shell.
casaos-cli to REST API map
Every casaos-cli command is a thin HTTP call to the gateway on port 80, and the app-management journal logs each one. When a CLI command is broken, or when you want to call the API from a script without the client, these are the requests it sends:
| casaos-cli command | HTTP request (as logged) |
|---|---|
list apps | GET /v2/app_management/compose |
install -f <file> [-d] | POST /v2/app_management/compose?dry_run=true|false |
show local <id> | GET /v2/app_management/compose/<id> |
apply <id> [-d] | PUT /v2/app_management/compose/<id>?dry_run=true|false |
update app <id> [-f] | PATCH /v2/app_management/compose/<id>?force=true|false |
start / stop / restart <id> | PUT /v2/app_management/compose/<id>/status |
uninstall <id> -n | DELETE /v2/app_management/compose/<id>?delete_config_folder=false |
set global <key> <value> | PUT /v2/app_management/global/<key> |
show global | GET /v2/app_management/global |
register app-store <url> | POST /v2/app_management/appstore?url=<url> |
list app-stores | GET /v2/app_management/appstore |
From the box itself, curl -s http://localhost/v2/app_management/compose returns the same data as list apps as raw JSON, which is easier to feed to jq than the table output. From anywhere else the same URL answers 401 unless you go through a tunnel.