CasaOS keeps the variables it needs to run and the variables it hands to the apps it manages in one environment. You cannot tell, looking at a CasaOS container's env, which half is which — and the two drift into each other.
Maison separates them:
| Lives in | Owned by | Example | |
|---|---|---|---|
| What Maison needs to run | Maison's own environment (docker-compose.yml) |
Maison | DATA_ROOT, APPSTORE_URL, HTTP_ADDR |
| What an app receives | .env.app |
the deployment | APP_NET, APP_DOMAIN, APP_PUBLIC_IP_DASH, APP_DEFAULT_PASSWORD |
Nothing is in both, so there is never a question of which one wins.
${DATA_ROOT}/AppData/maison/.env.app
AppData/maison/ is Maison's own app directory — the same folder a deployment
installs the dashboard's compose stack into, and where Maison keeps its settings and
store cache. Everything Maison owns is in that one place, with no hidden sibling.
It is a plain KEY=VALUE file, and it belongs to the deployment, not to Maison:
on a Yundera PCS the orchestrator writes it at provisioning; on a plain install it is
the operator's to edit. Maison creates it once, with a documented default, and
never overwrites it — an upgrade that silently reverted the deployment's domain,
network and credentials would be a bad day.
An empty value means the deployment does not have this, and the key is skipped
rather than written blank: an app is better off with an unresolved ${APP_DOMAIN} —
which docker compose reports — than a blank one, which silently routes it at
nothing.
On install, and again on every start, Maison reads .env.app and ensures each
key in the app's own .env:
- a key already there is set to the current value, in the line it already occupies;
- a key that is missing is appended;
- everything else is left alone — a variable the operator added to an app's
.envis theirs, and survives.
Keys are ensured one at a time, so neither file's ordering matters.
Merged in alongside are the few variables Maison computes per app and per install,
which a deployment cannot state: AppID, PUID, PGID, TZ, DATA_ROOT,
DATA_HOST_PATH. Setting those in .env.app has no effect.
Because an app is installed against one deployment and started against whatever that deployment has since become — a new app network, a new data root, a new domain, a new public IP. None of that invalidates the app's own configuration, so none of it should stop the app from starting.
This is why a store app's compose refers to its surroundings only through
${APP_NET}, ${DATA_ROOT}, ${APP_DOMAIN} … and never through a resolved
literal. The literal is the bug: it freezes the app to the deployment it happened to
be installed on, and moving the box leaves every app unstartable with reinstall as
the only way out.
The references are the store's to write, not Maison's to insert. Maison used to
rewrite each app's docker-compose.yml — swapping /DATA for ${DATA_ROOT} and
replacing the network the app declared with one of its own — before every up. That
made the file on disk differ from the file the store shipped, made a hand-run
docker compose up differ from an install, and, because the rewrite re-attached only
the main service, silently dropped every other service the app had put on the shared
network. It no longer happens: docker-compose.yml is the store's bytes, and Maison
writes only the override and this .env.
So a store app writes ${APP_NET:-pcs} and ${DATA_ROOT:-/DATA} itself — the
default keeping a bare docker compose up working outside Maison — .env.app says
what they currently are, and every start resolves the two afresh.
Maison could just pass them to docker compose in its own process environment —
it already runs the command. But then the app folder would only work when Maison
brought it up.
Writing them into the .env is the point: a docker compose up -d you run by hand
in AppData/<app>/ must bring the app up exactly as Maison does. The folder
stands on its own. That is the promise of the app model (see
app-model.md), and it is also what makes an app debuggable
without the dashboard in the loop.
Add a line to .env.app. That is the whole procedure — no rebuild, no code change.
It reaches every app on its next start.
Settings › App environment edits this file, for a deployment where opening a shell is not the natural move. It is the file itself in a text box, not a form: the comments are the documentation, there is no fixed schema — a deployment adds its own keys — and an empty value means something a form field cannot express.
Three things the page is careful about, all of them consequences of the above:
- It says the file has another owner. On a PCS the orchestrator wrote it and may write it again, over your edit. Maison cannot prevent that and does not pretend to.
- It saves nothing it cannot read back. A line that is not
KEY=VALUE, a name that is not a shell identifier, a duplicated key — all rejected, with the line number, and the file on disk is left untouched. The parser is deliberately forgiving (it skips a line it cannot read); a save is not, because a typo that silently does nothing would surface days later as an app that will not start. - It restarts nothing. The variables land in each app's
.envon that app's next start, per the section above. Unlike the domains list — which rewrites Caddy labels and so must recreate containers to mean anything — there is nothing here that recreating every container on the box would buy.
A key Maison computes per app (AppID, PUID, PGID, TZ, DATA_ROOT,
DATA_HOST_PATH) is reported as having no effect rather than rejected: the file is the
deployment's, and it is free to say what it likes.
Note the file holds APP_DEFAULT_PASSWORD and DefaultPassword in plain text, so the
page shows credentials. It was always a readable file on a box with no auth (Maison
assumes a trusted network — see the README); the page does not change who can read it,
only how far they have to reach.
APP_NET=mesh # the external network apps are attached to
APP_DOMAIN= # the deployment's base domain
APP_PUBLIC_IP= # …and its public IP, in the spellings the store uses
APP_PUBLIC_IP_DASH=
APP_PUBLIC_IPV4=
APP_PUBLIC_IPV4_DASH=
APP_PUBLIC_IPV6=
APP_PUBLIC_IPV6_DASH=
domain= # lowercase alias: some x-compose-app webui-host use it
APP_EMAIL=
APP_DEFAULT_PASSWORD=casaos
DefaultUserName=admin
DefaultPassword=casaosThat default describes a standalone local install: apps on Maison's own mesh
network, no domain, so no reachable web address. A PCS overwrites it with
APP_NET=pcs and its real domain and IP; dev/docker-compose.yml does the same
through its appenv init container.
CasaOS used REF_SCHEME / REF_PORT / REF_DOMAIN / REF_SEPARATOR to synthesize
an app's web-UI URL as scheme://<app><sep><domain>:<port> (casa-img,
route/v2/appstore_pcs.go). Maison replaced that mechanism entirely with
x-compose-app's webui-host / webui-scheme / webui-port (see
x-compose-app.md), so those variables have no consumer here
and are gone. REF_NET and REF_DOMAIN were duplicates of APP_NET and
APP_DOMAIN, which a PCS already sets; they are gone too.
A REF_* line left over in an old app's .env is inert — nothing interpolates it —
and Maison leaves it alone rather than deleting a line it no longer owns.