DocsCLI
Getting started with the CLI
aoox is the official aoox CLI: it stores panel credentials on your machine and talks to the same API as the dashboard.
- Command
- aoox
- Auth
- An aoox_… API token from the panel
- Needs
- Install script (no Node.js), or Node.js 22+ (npm)
install), sign in (login/whoami), and build + deploy a local repo (link/deploy). Commands for projects, databases, log tailing, and more don't exist yet — use the dashboard, a webhook, or call the API directly with the same token.Installing the CLI
On Linux or macOS, the install script sets up aoox with no Node.js required at all — the tarball bundles its own Node.js runtime:
curl -fsSL https://aoox.dev/install-cli.sh | sh
aoox --versionInstalls to /usr/local/lib/aoox + /usr/local/bin/aoox (root/sudo), or ~/.local/lib/aoox without that access (then the script prints a note: sudo aoox install will not find a binary in ~/.local/bin, so re-run it with sudo sh or call sudo ~/.local/bin/aoox install). Running the same command again upgrades in place. Supports Linux (glibc — not Alpine/musl) and macOS, x64 and arm64.
aoox update.npm install -g @hideandseeklab/aoox
aoox --versionThe command is installed as aoox even though the package is named @hideandseeklab/aoox. To track main before it's released, install from source:
git clone https://github.com/hideandseeklab/aoox-cli.git
cd aoox-cli
npm install
npm run build
npm link # makes the aoox command available globally
aoox --versionInstalling aoox via aoox install
This command is different from the section above: aoox install doesn't connect to an existing panel — it installs its own panel (postgres + api + web) on a fresh Linux VPS, as an alternative to the manual steps in Installation. Run it as root, directly on the target server.
$ sudo aoox install
==> Detecting the server public IP
Install aoox in /opt/aoox and run docker compose up -d? (Y/n)
==> Running docker compose up -d
==> Waiting for the API to be ready
aoox is installed in /opt/aoox.
Open: http://203.0.113.10:3000
Create the first owner account at /setup.sudo aoox install \
--web-domain panel.example.com \
--api-domain api.panel.example.com \
--acme-email me@example.com \
--admin-email me@example.com \
--admin-name "Admin" \
--admin-password "a-strong-password" \
--yes- Docker is installed automatically via
get.docker.comif it isn't already present — asks for confirmation unless--yesis given. --web-domainand--api-domainrequire each other, and both need--acme-emailfor a Let's Encrypt certificate via the built-in proxy.- Without
--admin-email, create the owner account via/setupin the browser, same as a manual install. - The default install folder is
/opt/aoox(change it with--dir); an existing install in that folder is rejected unless--forceis given. --forceon an existing install backs up.env.distand the compose files to<dir>/backups/<timestamp>/and reuses the existing secrets and every existing value (only flags you pass replace values), so the data survives.- Starting completely over needs all three of
--force --new-secrets --wipe-data: it runsdocker compose down -vfor the existing stack (the database volume is deleted), generates new secrets, and requires you to type the install folder to confirm —--yesalone is not enough; non-interactively pass--confirm-wipe=/opt/aoox.--new-secretsor--wipe-dataon their own are refused: new secrets cannot unlock the old database volume, which is what used to break the install. - Secrets (
JWT_SECRET,ENCRYPTION_KEY, the Postgres password) are generated at random and written to.env.distwith mode0600— no need for manualopenssl rand.
docker compose up -d, then waiting for /auth/setup-status to respond. It's not a replacement for aoox login — once the panel is installed, log in as usual to use aoox link/aoox deploy.Signing in to the panel
Create an API token in the panel
Settings → API token: give it a name (e.g.
my-laptop), pick an expiry, then copy theaoox_…token — it's only shown once. A token acts as its owner, with the same role — see Account security. The dialog that shows the token also has a copy-readyaoox login --url … --token …line.Run aoox login
$ aoox login Use the address you open the dashboard with: your panel domain (e.g. https://dashboard.example.com), or http://<server-IP>:3000 if no panel domain is set yet. ? Dashboard URL https://panel.example.com Looking for the panel... ok API token (aoox_…) ******** Verifying token... ok Logged in as you@example.com (owner) at https://panel.example.com Token saved to /home/you/.config/aoox/config.jsonYou only ever enter the dashboard address — the one you open in your browser, never the API address. The CLI reads the API address from the dashboard (
/.well-known/aoox.json). You can leave out the scheme: a bare host is tried over https, then http, and a bare IP also on port3000, so203.0.113.10works on a fresh install with no panel domain. If the dashboard does not advertise a public API address, the CLI assumes<same host>:3001and says so. An API address given by an older script still works (with a one-line note).The token is asked for via a hidden prompt, not an argument, so it doesn't end up in shell history or the process list. Before saving, the CLI calls
GET /auth/me— rejected credentials are never written to disk.Confirm you're connected
$ aoox whoami you@example.com · owner · Admin Panel: https://panel.example.comAdd
--jsonfor script-friendly output; the plain text lines above are hidden automatically.
Which panel am I connected to?
Running aoox with no arguments lists the commands and, below them, where the CLI is connected. The dashboard and API addresses come from the saved login (no network); the registry line asks the panel for its default registry with a short timeout and never fails the command. aoox --help and aoox <command> --help are unchanged.
$ aoox
...
Connected to
Dashboard https://dashboard.example.com
API https://api.example.com
Registry registry.example.com (self-hosted)
$ aoox # not logged in
Not connected — run: aoox loginIf the registry cannot be checked you see Registry (could not check: panel unreachable); a localhost:… registry address gets a short warning to set a domain with aoox registry domain --set <host>.
Build & deploy from a local repo
aoox link connects a repo folder to a single application in the panel, then aoox deploy builds the image on your machine, pushes it to a registry, and triggers a deploy — following the log until it's done, just like the Deploy tab in the dashboard.
Link the folder to an application
$ cd my-app-repo $ aoox link ? Select a project › shop ? Select an application › shop (shop) Linked: shop / shop (shop) Written to .aoox.json — safe to commit, contains no secrets.--project/--appskip the interactive picker — used in CI. The generated.aoox.jsonfile contains no token, so it's safe to check in.Deploy
$ aoox deploy ==> Build localhost:5000/shop/shop:a1b2c3d ==> docker login localhost:5000 ==> Push localhost:5000/shop/shop:a1b2c3d ==> Updating the application & triggering a deploy [building] Building localhost:5000/shop/shop:a1b2c3d from https://… ... Deployed: localhost:5000/shop/shop:a1b2c3dThe build runs on your local Docker (
docker buildwith--dockerfile/--context), then gets pushed to that application's registry — the self-hosted registry by default if there's more than one, or pick manually with--registry. The default tag is the git short SHA (+ -dirtyif there are uncommitted changes); override it with--tag.
sourceType: image, using the registry and tag just pushed — not rebuilt on the server the way a Git build is. The two application sources. A deployment already in progress for the same application is rejected (409) — wait for it to finish first.Variables (aoox env, aoox run)
aoox env reads and changes the environments of variable sets (the same ones as Variables in the dashboard) and manages which of them the linked application uses. aoox run starts a command with the variables the application gets at deploy time, without writing a file.
- A set environment is chosen with
--set <slug>(short-s) and--env <name>(-e). - Defaults:
--setis the only set the application linked withaoox linkuses (or the only set you can see);--envis the only environment of that set it links. With several candidates the command stops and lists them — it never guesses. There is no default environment name. --app <id>uses another application, and--stack <id>a compose stack, instead of the linked application.
$ aoox env list
backend (Backend) · shared with shop, blog · your role: developer
development 3 variables
staging 0 variables
production (protected) 7 variables used by 2
$ aoox env set API_URL=https://api.example.com GREETING="hello world" -s backend -e development
backend/development: added API_URL, GREETING (5 variables now)
$ aoox env list --set backend --env development # names only, values masked
API_URL=••••••••
$ aoox env list --set backend --env development --reveal # values (audited)
$ aoox env pull --set backend --env development # writes ./.env (mode 0600)
$ aoox env unset OLD_KEY --set backend --env development
$ aoox run -- npm run dev # the deployed app's variables (likely production)aoox run reproduces the linked application as deployed, usually production. See Local development.| Command | Does |
|---|---|
aoox env list [--set slug] [--env name] [--reveal] [--json] | Without flags: every set you can see, your role, its sharing and each environment's variable count (also as aoox env sets). With --set: one set. With --env: that environment's variable names; --reveal shows the values through the audited reveal action. |
aoox env pull [--set slug] [--env name] [--file .env] [--format dotenv|json|shell] [--force] | Writes the variables to a file with mode 0600 (owner only, where the OS supports it). It never overwrites an existing file unless --force, and warns once when the file is not covered by .gitignore (it never edits .gitignore itself). |
aoox env set KEY=VALUE [KEY2=VALUE2 …] [--set slug] [--env name] [--secret | --no-secret] | Adds or changes variables. Comments (including notes) and the order of the existing lines are kept; a new variable goes at the end. --secret marks the variables as secret, --no-secret as explicitly not secret; without either, the flags stay as they are (see Secret variables). A value with ${{project.KEY}} is refused: write ${{vars.KEY}}. |
aoox env unset KEY [KEY2 …] [--set slug] [--env name] | Removes variables; comments and the other lines stay. |
aoox env push --file .env [--set slug] [--env name] [--yes] | Replaces the whole environment with a local file, after showing which variables are added, changed and removed and asking for confirmation. |
aoox env history [--set slug] [--env name] [--limit 20] [--json] | The version history of an environment, newest first: when, who, and the variable names added (+), changed (~) or removed (-). Never values. |
aoox env diff --from name --to name [--set slug] [--json] | Compares two environments of one set by variable name: only on one side, different values, same. Prints names only. |
aoox env promote --from name --to name [KEY …] [--all] [--yes] [--set slug] | Copies the named variables (or all of them) and their secret flag from one environment of the set into another, after showing what changes and asking for confirmation. |
aoox env links [--app id | --stack id] [--json] | The set environments the application (or stack) uses, in merge order (1 = first, overridden by later ones), with the variables a later link or the own env overrides. |
aoox env link --set slug --env name [--position n] [--app id | --stack id] | Links a set environment. It goes at the end (so it wins over the others) unless --position (1-based) is given; an environment that is already linked is moved there. Takes effect on the next deploy. |
aoox env unlink (--set slug --env name | --position n) [--app id | --stack id] | Removes a link. Takes effect on the next deploy. |
aoox run [--set slug --env name] [--file .env.local] [--quiet] [--app id | --stack id] -- command [args…] | Starts the command with the variables on top of your current environment (see below). Nothing is written to disk. Output goes straight to your terminal, the command's exit code is the exit code of aoox run, and Ctrl+C or SIGTERM is passed to it. |
What aoox run passes to the command
- By default it does what the panel does when it creates the container, as far as a terminal can: it reads every set environment the application links, in order (a later one wins on the same key), then puts the application's own env on top. Every read is audited. If any of them cannot be read (a protected environment, a viewer, a token scope), nothing is started.
${{vars.KEY}}is replaced by the merged set value ofKEY, one level deep like the panel.${{database.…}}and${{secret.…}}references, and a${{vars.KEY}}that no linked set defines, are left as written, with one warning naming those variables: only the panel resolves them at deploy time. Secrets synced from Infisical are not included (a warning says so).--set/--envinstead use exactly one set environment: no links and no own env.--file .env.localgoes on top of all of that: the order, lowest to highest, is your current environment, then aoox, then the file. The file is only read, and its references are not resolved.
$ aoox run -- node server.js
aoox Injecting 9 variables into your process · Application "shop-web": shared/production < backend/production < own envThe count is what aoox adds or overrides, including --file; variables inherited from your shell are not counted. The line is coloured only on a terminal: NO_COLOR turns colour off, FORCE_COLOR forces it, and pipes and CI logs stay plain. --quiet (-q) hides the line, but warnings about references aoox cannot resolve are always printed.
Pull formats and push
$ aoox env pull --env staging # ./.env, the stored text with its comments
$ aoox env pull --format json # ./.env.json {"KEY":"value"}
$ aoox env pull --format shell # ./.env.sh export KEY='value'
$ aoox env pull --format json --file - | jq .PORT # standard output only
$ eval "$(aoox env pull --format shell --file -)" # load into the current shell--file -writes the data to standard output and nothing else there, so it can be piped or passed toeval. Notes (such as the one about${{…}}references) go to standard error. In this mode there is no "file exists" check and no.gitignorewarning, because no file is written.- The
shellformat wraps each value in single quotes and writes a single quote inside a value as'\'', so spaces,$and quotes surviveeval. Without--filethe file is.env,.env.jsonor.env.shby format, always with mode0600where the OS supports it.
$ aoox env push --file .env.staging --set backend --env staging
Environment: backend/staging
+ added (1): NEW_FLAG
~ changed (1): API_URL
- removed (1): OLD_KEY
= unchanged: 6
Variables that are not in the file are removed: this replaces the whole environment.
? Replace "backend/staging" with the contents of .env.staging? (y/N)- The file is checked before anything is sent. Refused: invalid names, an
exportprefix, trailing whitespace, duplicate names and${{project.…}}. Quoted values (KEY="value") are accepted with a warning, because the panel stores the quotes as part of the value. Windows line endings and a leading BOM are normalised. push,promoteanddiffnever print values.pushandpromoteask for confirmation; without a terminal (CI) they refuse unless you pass--yes. If someone else changed the environment in between, nothing is written (409), as withset.- Every change is recorded as a version (see history and restore); restoring an old version is done in the dashboard.
How values are stored (quoting)
The panel keeps one environment as plain KEY=VALUE text, one variable per line. The key is everything before the first =; the value is everything after it, exactly as given. There is no quote or escape processing — not in the CLI, not in the panel, not when the value is put into the container — so the quotes you type are removed by your shell, and quotes that are part of the value stay part of it.
| You type | Stored value |
|---|---|
GREETING="hello world" | hello world |
QUERY=a=b=c | a=b=c |
MSG='say "hi" # not a comment' | say "hi" # not a comment |
EMPTY= | an empty value |
- Rejected before anything is sent: an invalid name (letters, digits and
_, not starting with a digit), a value with a newline (one variable per line), and a value that ends with whitespace (the panel trims the end of each line). ${{vars.KEY}}and${{database.<slug>.url}}references are stored and pulled as written; the panel resolves them when the container is created.- Compose stacks read the resolved text through Docker Compose's own
--env-fileparser, which does strip quotes around a whole value. Avoid surrounding quotes in values used by stacks.
Editing at the same time
set, unset and push read the environment, change the text and save it back together with the version they read. If someone else saved in between (the dashboard, another terminal), the panel answers 409 and the CLI says that nothing was written — run the command again to apply your change on top of the latest version. The dashboard's editor does the same.
Who can do what
| Role | Unprotected environments | Protected environments (e.g. production) |
|---|---|---|
| Instance owner / admin | Everything | Everything |
| Project admin, developer | list, pull, set, unset, push, promote, run | Only the variable names. Anything else prints error "backend/production" is protected; only instance owners and admins can read it. |
| Viewer | Variable names only | Variable names only |
- Links:
env link/unlinkneed the developer role in the application's project; adding or removing a link to a protected environment needs the project admin role. The set must be shared with the project. - In CI create an API token and export
AOOX_TOKEN/AOOX_URL. The token can be limited to some set environments (Settings → API token → the variables limit), e.g.backend/staging,backend/*or*/development: other environments then answer 403. See API token scopes. - A read-only token can still run
aoox env pull,aoox env list --revealandaoox run(reading changes nothing and is audited), butset,unset,push,promote,linkandunlinkare refused. Viewers can never read values, with or without a token. - Every read of values is recorded in the audit log without the values. Reading is a request to the API, so an environment is never cached on disk by the CLI.
- Creating, sharing and deleting sets and environments is done in the dashboard by an instance owner or admin.
Local development
Run your app on your laptop with the variables of one set environment, without a .env file and without linking the folder. You only need to be signed in.
Install the CLI, create a narrow token, sign in
$ npm i -g @hideandseeklab/aoox $ aoox loginIn the panel, Settings → API token → Create token: turn Read-only on and set Limit to variables to the environment you develop against, e.g.
backend/development. A laptop token then cannot touch production. See API token scopes.Put the command in package.json
package.json"scripts": { "start:dev": "aoox run --set backend --env development -- nest start --watch" }Coming from Infisical?
infisical run --env=dev -- …becomesaoox run --set <slug> --env development -- ….Always pass --set and --env locally
Without them you get the deployed app's variablesaoox run -- …with no--set/--envneeds a linked application and injects what its container gets at deploy: the application's linked set environments plus its own env. That is usually production, and changing the application's links in the panel would silently change what your laptop gets. With--set/--envexactly one environment is injected: no links, no own env.What you see
$ aoox run --set backend --env development -- nest start --watch aoox Injecting 5 variables from backend/development (no links, no own env)- The line goes to standard error and is coloured only on a terminal (plain when piped or with
NO_COLOR).--quiet(-q) hides it, never the warnings. - Fail closed: if the read is refused (bad token, no access, a protected environment, a token scope), the command is not started at all.
- Variables live in the child process only; nothing is written to disk.
- The line goes to standard error and is coloured only on a terminal (plain when piped or with
Tools that only read a file
$ aoox env pull --set backend --env development --file .envThe file is written with mode
0600and never overwritten without--force. Make sure.envis in.gitignore; aoox warns when it is not.Do you need aoox link?
Not for local variables:
--set/--envonly need you to be signed in. You do need it foraoox deploy, foraoox runwithout flags, and for managing links withaoox env links/link/unlink. It writes.aoox.json(panel address, project and application ids and names, no secrets), which is safe to commit.CI
Skip
aoox loginand setAOOX_URLandAOOX_TOKEN(a masked CI variable) from a read-only token limited to the environment the job needs.$ AOOX_URL=https://panel.example.com aoox run --set backend --env development -- npm test
Limits
${{database.…}}and${{secret.…}}references stay as written, with a warning: only the panel resolves them at deploy time. Point those variables at a local database in a local-only variable or--file.- Secrets synced from Infisical are not included.
- If
--set/--envare omitted and several candidates exist, the command stops and lists them; it never guesses.
Build & push images (aoox registry)
Two commands for getting an image into the registry without deploying it — handy when you want to pick the image later in the dashboard (New application → Ready-made image). Neither needs aoox link; they use your normal login (or --url/--token). The registry is the self-hosted one by default; pass --registry <id> to use another.
$ aoox registry build myapp:1.0.0
==> Build registry.example.com/myapp:1.0.0
...
Built registry.example.com/myapp:1.0.0
Next: aoox registry push myapp:1.0.0 (or build with --push next time)
$ aoox registry push myapp:1.0.0
==> docker login registry.example.com
==> Push registry.example.com/myapp:1.0.0
Pushed registry.example.com/myapp:1.0.0
Use it in the dashboard: New application → Ready-made imagebuild reads ./Dockerfile (-f/--dockerfile, --context) and names the image <registry host>/<name>:<version>. push only pushes an image that already exists locally and accepts the short name or the full one. To do both in one step, use aoox registry build myapp:1.0.0 --push. Names are lowercase.
You do not have to type everything. Leave out the version (aoox registry build myapp) or the whole argument (aoox registry build) and the CLI reads it from the project: the build context for build, the current folder for push. A typed name:version always wins. The first file found, in this order, supplies each value:
| Order | Source | Name | Version |
|---|---|---|---|
| 1 | package.json | name (scope removed: @acme/web becomes web) | version |
| 2 | pyproject.toml | [project] or [tool.poetry] name | version (skipped when dynamic) |
| 3 | Cargo.toml | [package] name | [package] version (not a workspace one) |
| 4 | composer.json | name (vendor/pkg becomes pkg) | version, if present |
| 5 | pubspec.yaml | name | version |
| 6 | *.csproj | AssemblyName, else the file name | Version, or VersionPrefix-VersionSuffix (one file only) |
| 7 | pom.xml | artifactId of the project | version of the project (not the parent's) |
| 8 | VERSION | — | first non-empty line |
| 9 | git tag | — | the tag HEAD is on, without a leading v |
| 10 | folder name | the folder, lowercased | — |
Values from a file that are unusable (a $(…) or ${…} variable, a dynamic version) are skipped and the next source is tried. Names are lowercased; a + in a version (build metadata, which tags cannot hold) becomes _. If HEAD is past the last git tag, nothing is guessed: tag the release, type name:version, or add --force to use the git describe form such as 1.4.2-3-gabc1234.
$ aoox registry build
==> Image web:1.4.2 (name and version from package.json)
? Build web:1.4.2? (Y/n/e)Because a derived name or version can be wrong, the CLI asks before it does anything: Y continues, n stops, and e lets you type another name:version. --yes (-y) skips the question. Without a terminal, or when CI is set, it refuses and asks for name:version or --yes. A fully typed name:version never asks. If you forget to bump the version, the "already exists" check below still protects the registry, and says where to change it.
| You see | Meaning |
|---|---|
error myapp:1.0.0 already exists in registry.example.com. | That version is already in the registry (checked before building, and before pushing). Pick a new version — the hint suggests the next one — or add --force to overwrite it. latest can always be overwritten. |
error No local image myapp:1.0.0. | push never builds. Run aoox registry build myapp:1.0.0 first. |
error A version is required: "myapp" has none. | No version was typed and none was found in the project. Write name:version, e.g. aoox registry build myapp:1.0.0, or add a version to a file from the list above. |
The registry is localhost:5000 | A warning: that address only works on the server itself. Give the registry a domain with aoox registry domain --set registry.example.com. |
Changing the panel's domain
An alternative to Settings → Domain panel in the dashboard — good for provisioning scripts. See Domain for the panel for the details of what happens behind it.
$ aoox domain set --web panel.example.com --api api.panel.example.com \
--acme-email you@example.com
Domain saved: panel.example.com (dashboard), api.panel.example.com (API).
The panel will restart for a few seconds to apply it — the connection to this API will drop briefly.To undo it, aoox domain clear removes the domain and goes back to http://<server IP>:3000 (it asks first; --yes skips the prompt). The proxy and your applications' domains are not touched. See Removing the domain.
INSTALL_DIR already set in the panel's .env.dist (the absolute path of the folder containing docker-compose.dist.yml on that host) — without it the command fails with a 400.Updating aoox
Two different things get updated, with two commands. aoox update updates the CLI itself on your machine; aoox update panel updates the panel (the aoox-api/aoox-web images on the server).
aoox update checked the panel. The old behaviour is now aoox update panel; running aoox update --apply prints Panel updates moved to: aoox update panel --apply and exits with an error instead of touching anything.The CLI: aoox update
Looks up the newest version on npm (no login, no panel needed). Every npm dist-tag is a candidate: a pre-release install is offered the newest version whatever its label (alpha, then beta, then rc), while a stable install only sees stable versions and never moves to a pre-release. Until a stable version exists, pre-releases are published under the latest tag, so a plain npm i -g @hideandseeklab/aoox installs the newest one. If the CLI was installed with npm it runs npm i -g @hideandseeklab/aoox@<version> for you; for the standalone tarball or a source checkout it prints the exact command to run instead of guessing.
$ aoox update --check
Update available: aoox CLI 0.1.0-rc.1 → 0.1.0-rc.2
Run: aoox update
$ aoox update
==> npm i -g @hideandseeklab/aoox@0.1.0-rc.2
Updated aoox CLI 0.1.0-rc.1 → 0.1.0-rc.2
$ aoox update
aoox CLI 0.1.0-rc.2 is up to dateIf the npm registry cannot be reached the command fails with a clear message (non-zero exit status).
The panel: aoox update panel
Checks and applies an update for the panel itself (not the applications it deploys) — compares the aoox-api/aoox-web image digests on the registry, not just a version number.
$ aoox update panel
Running version: 0.1.0-rc.1
api: hideandseeklab/aoox-api:latest (update available)
web: hideandseeklab/aoox-web:latest (up to date)
Update available. Run with --apply to apply it.
$ aoox update panel --apply
Update applied — the panel will restart for a few seconds to apply it.If the install is pinned to a version tag (API_IMAGE / WEB_IMAGE in .env.dist), a newer published version is shown but never applied by this command: it prints the latest version and how to move the pin (set both variables to the new tag, or remove them to follow latest, then run sudo aoox reinstall), and --apply refuses instead of reporting success.
Same as aoox domain set, needs INSTALL_DIR set. See Upgrading for the manual, over-SSH alternative.
Repairing an install (aoox reinstall)
aoox update panel only runs pull + up -d: it doesn't rewrite docker-compose.dist.yml and doesn't add new variables to .env.dist. An existing install therefore misses fixes that need a compose or env change (a real example: Terminal failing with TERMINAL_SSH_USER is not set after an update, because older installs never wrote that variable). aoox reinstall repairs that without touching your data. Run it on the panel server, as root:
$ sudo aoox reinstall --dry-run
$ sudo aoox reinstall| Step | What happens |
|---|---|
| Backup | .env.dist and the compose files are always copied to <dir>/backups/<timestamp>/ before anything changes, even when nothing needs changing (.env.dist stays mode 600). |
| Compose | docker-compose.dist.yml (and docker-compose.domain.yml if present) is rewritten from the copy bundled in the CLI. docker-compose.override.yml, owned by the Domain panel/Environment features, is not touched. |
| .env.dist | Merged, never regenerated: every value, the order, comments and unknown keys are kept; only keys that are missing and have a safe default are added (e.g. TERMINAL_SSH_USER=root, INSTALL_DIR). POSTGRES_PASSWORD, JWT_SECRET and ENCRYPTION_KEY are never changed — if one is missing the command stops with a clear error. |
| Restart | docker compose pull then up -d --force-recreate, using the same compose file list as the running stack. Volumes (the database) are not touched. It then waits for the API to be healthy and prints a summary. |
Safe to run repeatedly: when nothing needs changing, no file is rewritten (a backup folder is still made) and the containers are refreshed. If docker-compose.override.yml uses the !override tag (the option Only allow access through the domain) and the host's Docker Compose is older than 2.24.4, the command stops before changing anything: upgrade the Compose plugin or turn that option off in the dashboard first. Options: --dir (default /opt/aoox), --yes (no prompt), --no-pull, --terminal-ssh-user <user> (fills in or overrides that value), and --dry-run (print the plan only).
aoox install --force is for reinstalling over an existing folder: it backs up .env.dist and the compose files to <dir>/backups/<timestamp>/ first and then keeps your secrets and existing values, so the data stays readable. For a running install that only needs a refresh, aoox reinstall is still the right command. Starting from scratch needs --force --new-secrets --wipe-data, which deletes all panel data (see Installing aoox via aoox install).Without the CLI: edit .env.dist directly (e.g. add TERMINAL_SSH_USER=root) or use the Environment page in Settings, then docker compose … up -d.
Command reference
| Command | Does | Flags |
|---|---|---|
aoox install | Installs aoox (postgres + api + web) on a fresh VPS via Docker Compose. | --web-domain, --api-domain, --acme-email, --admin-email/-name/-password, --dir, --force, --new-secrets, --wipe-data, --confirm-wipe, --no-terminal-authorize, --terminal-ssh-user, --yes/-y |
aoox login | Stores the dashboard address and API token for other commands (the API address is discovered automatically). | --url/-u, --token/-t |
aoox whoami | Shows the account and panel currently in use. | --url/-u, --token/-t, --json |
aoox link | Links a repo folder to an application in the panel (used by aoox deploy). | --project, --app |
aoox deploy | Builds the image locally, pushes it to a registry, then triggers a deploy — following the log until it finishes. | --tag, --dockerfile/-f, --context, --registry |
aoox env list | pull | push | set | unset | history | diff | promote | Lists, downloads, uploads, compares or changes the environments of variable sets (see Variables). | --set/-s, --env/-e, --app, --stack, --url/-u, --token/-t; --reveal, --json (list); --file, --force, --format (pull); --file, --yes (push); --secret/--no-secret (set); --limit, --json (history); --from, --to (diff, promote); --all, --yes (promote) |
aoox env links | link | unlink | Shows or changes the ordered set environments an application (or --stack) uses. | --app, --stack; --set/-s, --env/-e, --position (link, unlink) |
aoox run -- <command> | Runs a command with the variables the linked application gets at deploy time (or one set environment), without writing them to disk. | --set/-s, --env/-e, --app, --stack, --file, --quiet/-q, --url/-u, --token/-t |
aoox domain set | Sets a custom domain for the panel itself (dashboard + API) without SSH. | --web, --api, --acme-email, --[no-]domain-only |
aoox domain clear | Removes the panel's custom domain and goes back to IP access (IP:3000), after a confirmation. | --yes/-y |
aoox registry domain | Sets or clears a custom domain for the self-hosted registry. | --set, --clear |
aoox registry password | Shows the self-hosted registry's address, username and password (owner/admin), or resets the password (owner) without touching any image. | --show, --reset, --yes/-y |
aoox update | Updates the aoox CLI itself from npm (replaces the old panel update — see aoox update panel). | --check |
aoox update panel | Checks or applies an update for the aoox panel itself (the old aoox update). | --apply |
aoox registry build | Builds ./Dockerfile into a local image named for the registry; refuses a version that already exists there. With --push it also pushes it. | --push, --yes/-y, --registry, --dockerfile/-f, --context, --force |
aoox registry push | Pushes an existing local image to the registry; refuses a version that already exists there. | --yes/-y, --registry, --force |
aoox reinstall | Repairs/refreshes an existing panel install without losing data (rewrites compose, merges .env.dist, pull + recreate). | --dir, --dry-run, --yes, --no-pull, --terminal-ssh-user |
aoox help [COMMAND] | Lists commands, or shows help for one. | — |
Configuration & environment variables
| Source | Contents | Notes |
|---|---|---|
config.json | { "url": "<api>", "webUrl": "<dashboard>", "token": "aoox_…" } in oclif's config directory (~/.config/aoox/ on Linux). | Written with mode 0600; the full path is printed at login. |
--url / --token | Override the file's contents for a single command. --url is the dashboard address (an API address is still accepted). | Useful when switching between panels (production vs. staging). |
AOOX_URL / AOOX_TOKEN | The same values, via the environment. | Used in CI; also shown in --help. |
- Precedence: flag → environment →
config.jsoncontents. If none is complete, the CLI tells you to runaoox login. - Working with several panels? Don't keep logging in and out — save one as the default and override it with
--url/--tokenas needed.
In CI (no terminal)
aoox login needs an interactive terminal. In a pipeline, skip login and supply credentials via the environment:
deploy:
image: node:20
variables:
AOOX_URL: https://panel.example.com
script:
- npx -p @hideandseeklab/aoox aoox whoami # AOOX_TOKEN from a CI variable (masked)Behind an antivirus or corporate proxy
Antivirus HTTPS scanning (AVG, Avast, Kaspersky, ESET, Bitdefender) and corporate proxies (Zscaler, Netskope, Fortinet, Palo Alto) re-sign HTTPS traffic with their own root certificate. It is installed in your operating system's certificate store, but Node only knows its own bundled list, so the CLI used to fail with a certificate error even though the panel's certificate is fine.
On Node 22.19+ or 24.5+ the CLI now trusts the system store in addition to Node's list, so it works without any setup. Certificate verification is never turned off. On older Node versions nothing changes and the error names the issuer it saw (for example AVG Web/Mail Shield Root).
- Opt out of the system store with
AOOX_SYSTEM_CA=0. NODE_EXTRA_CA_CERTS=/path/to/root.pemstill works if you would rather trust one specific root.- Older Node: set
NODE_OPTIONS=--use-system-ca(Node 22.15+ / 23.10+), upgrade Node, or add the panel's domain to the antivirus HTTPS-scanning exclusions.
Common pitfalls
| Message | Cause & fix |
|---|---|
| The panel rejected the token (401) | The token expired, was revoked, or was copied wrong. Create a new one in Settings → API token. |
| No interactive terminal | Running in CI/a script. Pass --url and --token, or set AOOX_URL and AOOX_TOKEN. |
| Cannot reach the panel | Wrong address, or the panel isn't reachable from this machine. Use the address you open the dashboard with, check the firewall / security group, or set a panel domain (Infrastructure → Panel domain). Test with curl <dashboard-url>/.well-known/aoox.json. |
| … answers plain HTTP | You used https:// on a port that speaks plain HTTP (e.g. :3000). Use the http:// address, or set a panel domain to get HTTPS. |
| Your aoox CLI is older than the panel | A warning, not an error. Update with aoox update. |
| Config is corrupt (not valid JSON) | The config file was edited by hand. Delete it, then run aoox login again. |
| 403 on a specific command | The token owner's role isn't high enough — a token never grants more than its owner has. If the token has a scope, see read-only/project-limited tokens. |
| This repo is not linked yet | aoox deploy was run before aoox link. Run aoox link first in the repo folder. |
| A deployment is already running for this application (409) | The same application is already being deployed — from the dashboard, a webhook, or another aoox deploy. Wait for it to finish, check the Deploy tab. |
| aoox install: Root is required | Run it with sudo — installation writes to system paths and manages Docker. |
| aoox install: An installation already exists in … | The --dir folder (default /opt/aoox) is already populated. To repair that install, use aoox reinstall. --force reinstalls over it keeping your data and secrets (the previous files are backed up to <dir>/backups/), or use a different --dir. A from-scratch install that deletes all data needs --force --new-secrets --wipe-data. |
--json output for every command (currently just whoami).Next steps
Found a mistake? Edit this page on GitHub ↗