Not affiliated with IceWhale. An independent, community-maintained distribution of CasaOS, not produced or endorsed by Shanghai IceWhale Technology Limited. CASAOS is their trademark, used here only to say what this is a release of. The original project is IceWhaleTech/CasaOS; report problems with this distribution at ReCasaOS/CasaOS/issues.
The service that installs and runs apps on a CasaOS host. It is the Docker Compose layer of the system: it keeps the app store catalogues, turns a store entry into a docker-compose.yml, runs Compose against the local Docker daemon, and reports container status, logs and available updates back to the dashboard.
This repository is part of ReCasaOS, a maintained release of the project after upstream IceWhaleTech stopped shipping in 2025. It descends from alvins82's fork, whose Docker SDK bump is what keeps CasaOS installable on a current daemon.
The service listens on a random loopback port, writes that address to /var/run/casaos/app-management.url, and registers its paths with CasaOS-Gateway:
| Path | What is behind it |
|---|---|
/v2/app_management |
the current API |
/v1/apps, /v1/container, /v1/app-categories |
the v1 API, kept for older clients |
/doc/v2/app_management, /v1doc/v1/app_management |
the OpenAPI specs and their viewer |
Under /v2/app_management: /appstore registers and lists stores, /apps serves the merged catalogue (with /apps/upgradable and /apps/{id}/compose), /categories the category list and its counts, /compose and /compose/{id} install an app and then apply settings, update, start, stop or uninstall it (/status, /containers, /logs, /healthcheck), /container/{id} reports and health-checks a single container, and /image pulls images ahead of an install. /compose/{id}/env reads and replaces the .env next to that app's docker-compose.yml and re-creates the app so the new values apply. The full list is api/app_management/openapi.yaml; the Go server, types and MessageBus client are generated from it and are not committed.
Two jobs run on a schedule from main.go: the store catalogues are refreshed every 10 minutes, and every 15 seconds a sweep starts apps Docker abandoned at boot because their storage was not mounted yet.
On a host:
/etc/casaos/app-management.conf— paths and the app store URLs, written frombuild/sysroot/etc/casaos/app-management.conf.sampleon first start/etc/casaos/env— variables injected into every app/var/lib/casaos/appstore— the downloaded catalogues/var/lib/casaos/apps/<name>/docker-compose.yml— one directory per installed app; this is the file the settings page and the compose editor write/var/log/casaos/app-management.logcasaos-app-management.service, started afterdocker.serviceandcasaos-message-bus.service
There is no database. An installed app is its compose file plus whatever the Docker daemon reports about it.
Components are not installed on their own. The distribution is installed and upgraded with one command:
curl -fsSL https://github.com/ReCasaOS/CasaOS-Install/releases/latest/download/install.sh | sudo bashWhat a release contains, and how it is built, is described in CasaOS-Install.
From alvins82:
- Docker 26 and later. The SDKs were pinned to docker/cli and docker/docker v24, compose/v2 v2.23 and compose-go v1, which is why CasaOS stopped working against a current daemon. They are now v26.1.0, v2.27.0 and compose-go/v2 v2.1.0; the client already negotiated the API version with the daemon, and
pkg/docker/api_negotiation_test.gonow asserts it still does. This is the change that made the fork necessary. Offered upstream as #217, still unmerged. - Apps abandoned at boot. Docker gives up on a container whose bind mount does not exist yet, which happens on every reboot when apps live on a disk mounted after
docker.service. A recovery sweep starts those apps once their storage appears, and leaves alone the ones the user stopped.
Here:
- An app can keep its secrets in a
.env. An installed app may carry a.envnext to itsdocker-compose.yml, edited from the dashboard throughGET/PUT /v2/app_management/compose/{id}/env:PUTreplaces the whole file (an empty body removes it) and re-creates the app so the new values apply. A reference to one of its keys (${KEY},$KEY,${KEY:-default}), or to a key the runtime defines ($AppID,${TZ},$PUID,${PGID}), now survives every settings round trip and the App Store update as written, where before the first save or update baked the resolved value into the compose file and every later.envedit was a no-op; the key has to be in.envwhen the compose is saved, or the reference is baked as a literal. It is kept inenvironmentand every other free-text field (image,command,labels,env_file,x-casaos...) and in the host side ofportsandvolumes, which come back in the long syntax; a typed field such ascpus,mem_limitorprivilegedcannot hold one, and the editing load (GET /compose/{id}as YAML) fails with a500naming that field instead of serving the resolved value. A.envthat defines a key the runtime sets itself (TZ,PUID,PGID,AppID, the environment of the CasaOS process) is refused with a400naming the key, since the runtime's value wins over the file anyway. When the compose file does not load, the pull fails or the app fails to start after a.envchange, the previous.envis put back together with the previousdocker-compose.ymland the app is started from them again. - A
$no longer doubles on every save. The settings handler parsed the incoming YAML without interpolation and then re-escaped every$, so a value it had produced itself came back doubled:$2a$12$…became$$2a$$12$$…, then$$$$2a$$$$12$$$$…, and the container received a broken password hash. Parsing with interpolation on makes the round trip idempotent. Fixes CasaOS#1988. - Architecture filtering. An app declaring
architectures: []was hidden on every host, because an explicit empty list decodes to an empty slice rather thanniland onlynilwas treated as "runs anywhere" — which is what the dashboard already assumed. Category counts were computed from the unfiltered catalogue, so on arm hosts they were larger than the number of cards listed under them. Both now agree with the list. Supersedes the draft in #202, which dereferenced that empty list and hardcoded amd64. - Releases. The upstream workflow needed IceWhale's secrets and is gone.
.github/workflows/release.ymlruns the tests and publishes with GoReleaser on av*.*.*tag using the defaultGITHUB_TOKEN. Our change to.goreleaser.yamlis one line,release.github.owner, nowReCasaOS, so the archive layout the installer consumes is unchanged. - No geo-IP at install time.
build/scripts/migration/script.dran__get_download_domainat top level, curlingipconfig.io/countryand thenifconfig.io/country_codeto pick a download mirror by country.install.shruns every script in that directory on every install and every upgrade, so both services were contacted each time, before the script had even decided whether a migration was due. On a current box it is not, though this repository is the one whose list still carries a reachable v0.4.5 entry. The migration tools now come from GitHub unconditionally, and the domain is a constant: it is not an environment knob either, because what it points at is downloaded and run as root without verification. - The compose lifecycle test is opt-in. It skipped itself only when no Docker daemon was reachable, which is the wrong question: a CI runner has a daemon and no CasaOS behind it, so the test ran and failed on the missing app store. It now requires
CASAOS_INTEGRATION. - Backups.
POST /v2/app_management/compose/{id}/backupcopies an app to an rclone remote -- S3, SFTP, FTP, or anything else rclone speaks -- on demand or on a schedule with a retention (/backup/schedules,/backup/runs,/backup/destinations). None of the transport is new code: this distribution already installs rclone and runs it as a daemon on a unix socket, so the retries, the resume and the incremental comparison are its. What is written here is what rclone cannot know: which paths belong to the app and which of them are data (/var/run/docker.sockis a door,/devand/procare kernel interfaces, tmpfs is empty at every start, an anonymous volume has no name to restore it under -- each is named in the manifest with its reason), and how to hold the app still while it is copied, since a database copied while it writes produces a backup that looks fine and does not restore. What is about to be stopped is written down before the first container goes down and erased after the last comes back, and a note still there when the service starts is an app a killed backup left off, started again. - A container CasaOS did not install can be identified and removed.
GET /v2/app_management/container/{id}/detailanswers what the dashboard could not: image, command, restart policy, networks, published ports, host paths, named volumes with their size, environment.DELETE /container/{id}removes it, and removes named volumes only when asked and only when the daemon says nothing else uses them, re-deciding against the daemon at the moment of the request rather than against what the screen was drawn from. A container belonging to a compose project is refused: the app's own uninstall is how a stack goes. - An app whose compose file has no
x-casaosis a first-class app. Six defects on this distribution began with one written by hand: it stayed grey while every container ran, its status was computed and then dropped by the dashboard's own grid, clicking it opened nothing. The status no longer depends on a catalogue entry, and a web port is inferred from what the containers publish when the file names none -- the main service's, against a short list of ports images serve interfaces on, and nothing rather than a wrong link when none matches. - An update never replaces a floating tag with a fixed one. An app tracking
developcame back pinned to whatever version the catalogue named, because the decision read the catalogue's tag alone. Both references decide now; a digest is the opposite case, sincepull app@sha256:...returns the same image for ever, so a digest-pinned app can only be updated by rewriting it. - One container at a time.
POST /compose/{id}/container/{cid}/statusstarts, stops or restarts one service of an app instead of the whole stack, andGET /compose/{id}/container/{cid}/statssamples its CPU and memory asdocker statsreports them. - The module path is
github.com/ReCasaOS/CasaOS-AppManagement. The Go module and every import of it moved offgithub.com/IceWhaleTech, and the shared library is nowgithub.com/ReCasaOS/CasaOS-Common v0.4.22, so logs, stack traces andgo version -mon the shipped binary name this distribution rather than upstream; App Store catalogue and CDN icon URLs still point at IceWhale's repositories and are unchanged. - Loopback is not an identity (v0.4.49). Any request from 127.0.0.1 skipped the token. Loopback is not this box's services alone: a container on the host network, or any local account, reaches the same addresses. A request skips the token only when it is one of this box's services, which come from loopback with the secret the gateway writes for this boot (
/var/run/casaos/internal.secret, readable by root only); the dashboard always had a token. Installing a compose file is root on the host, so this mattered here most. - A retention for backups somebody asks for, and a copy of the catalogue (v0.4.50).
POST /compose/{id}/backuptakeskeep: once the backup has landed, older backups of that app at that destination beyond that number are deleted, the same arithmetic a schedule's retention uses. Absent keeps everything, as before. And the App Store's catalogue, thestore/main.zipIceWhale builds onCasaOS-AppStore@gh-pages, is fetched from this distribution's nightly copy of it (ReCasaOS/_appstore, the same bytes) when the original stops answering, the pre-2025_appstoreURL an upgraded box may still carry included; the configured URL is not changed, and the original is tried first on every update (corrected in v0.4.53: v0.4.50 mapped a URL no box uses). - Every app at once (v0.4.52).
GET /compose/updatesis what updating everything would do: the apps with an update on offer and, for each, the images the update would write (check=trueasks the registries first).POST /compose/updateswith a list of ids runs the same update the button runs, one app after another, on this box rather than from the browser, andGET /compose/updates/runsays what became of each: updated, current with the reason, failed with the error. One run at a time.
The compose editor in an app's settings is a dashboard feature. The endpoint it saves through, PUT /v2/app_management/compose/{id}, is upstream; what this fork changed behind it is the $ handling and the .env references above.
go generate ./...
go build ./...
go test ./route/v2/ ./service/ -count=1Go 1.26.8 or later, per go.mod. codegen/ is generated and not committed, so go generate comes first; it fetches oapi-codegen and the MessageBus spec, so the first run needs network access.
go test ./... also runs ./pkg/docker, which drives a real Docker daemon and pulls images from Docker Hub. On a Windows workstation those tests fail on a goroutine-leak assertion against a go-winio worker; CI runs them on Linux. TestComposeAppLifecycle in ./service stays skipped unless CASAOS_INTEGRATION is set — it needs a live CasaOS host, not just a daemon.
Apache License 2.0 — see LICENSE; the upstream copyright notices are kept in the source. The service is the work of IceWhale and its contributors. The Docker SDK bump and the boot recovery sweep are alvins82's.