$ aoox

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)
Early stage
The CLI can already install aoox (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:

Linux / macOS, no Node.js
curl -fsSL https://aoox.dev/install-cli.sh | sh

aoox --version

Installs 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.

Windows, Alpine, or already have Node.js?
Use npm instead — requires Node.js 22+ first. Later, update with aoox update.
From npm
npm install -g @hideandseeklab/aoox

aoox --version

The command is installed as aoox even though the package is named @hideandseeklab/aoox. To track main before it's released, install from source:

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 --version

Installing 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.

Simplest form (no domain, accessed by IP)
$ 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.
With a domain + automatic HTTPS, and creating the owner right away
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.com if it isn't already present — asks for confirmation unless --yes is given.
  • --web-domain and --api-domain require each other, and both need --acme-email for a Let's Encrypt certificate via the built-in proxy.
  • Without --admin-email, create the owner account via /setup in 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 --force is given.
  • --force on an existing install backs up .env.dist and 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 runs docker compose down -v for the existing stack (the database volume is deleted), generates new secrets, and requires you to type the install folder to confirm — --yes alone is not enough; non-interactively pass --confirm-wipe=/opt/aoox.--new-secrets or --wipe-data on 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.dist with mode 0600 — no need for manual openssl rand.
This command replays the steps in Installation automatically: cloning the distribution compose files, filling in the env, 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

  1. Create an API token in the panel

    Settings → API token: give it a name (e.g. my-laptop), pick an expiry, then copy the aoox_… 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-ready aoox login --url … --token … line.

  2. 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.json

    You 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 port 3000, so 203.0.113.10 works on a fresh install with no panel domain. If the dashboard does not advertise a public API address, the CLI assumes <same host>:3001 and 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.

  3. Confirm you're connected

    $ aoox whoami
    you@example.com · owner · Admin
    Panel: https://panel.example.com

    Add --json for 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 login

If 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>.

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.

  1. 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/--app skip the interactive picker — used in CI. The generated .aoox.json file contains no token, so it's safe to check in.

  2. 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:a1b2c3d

    The build runs on your local Docker (docker build with --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 (+ -dirty if there are uncommitted changes); override it with --tag.

The application is automatically switched to 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: --set is the only set the application linked with aoox link uses (or the only set you can see); --env is 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.
A typical session
$ 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)
For local work, pass --set and --env
Without them aoox run reproduces the linked application as deployed, usually production. See Local development.
CommandDoes
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 of KEY, 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/--env instead use exactly one set environment: no links and no own env.
  • --file .env.local goes 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.
What was injected and from which layers (printed to standard error)
$ aoox run -- node server.js
aoox  Injecting 9 variables into your process · Application "shop-web": shared/production < backend/production < own env

The 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

Formats and standard output
$ 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 to eval. Notes (such as the one about ${{…}} references) go to standard error. In this mode there is no "file exists" check and no .gitignore warning, because no file is written.
  • The shell format wraps each value in single quotes and writes a single quote inside a value as '\'', so spaces, $ and quotes survive eval. Without --file the file is .env, .env.json or .env.sh by format, always with mode 0600 where the OS supports it.
Upload a file and look at the result
$ 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 export prefix, 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, promote and diff never print values. push and promote ask 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 with set.
  • 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 typeStored value
GREETING="hello world"hello world
QUERY=a=b=ca=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-file parser, 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

RoleUnprotected environmentsProtected environments (e.g. production)
Instance owner / adminEverythingEverything
Project admin, developerlist, pull, set, unset, push, promote, runOnly the variable names. Anything else prints error "backend/production" is protected; only instance owners and admins can read it.
ViewerVariable names onlyVariable names only
  • Links: env link/unlink need 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 --reveal and aoox run (reading changes nothing and is audited), but set, unset, push, promote, link and unlink are 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.

  1. Install the CLI, create a narrow token, sign in

    $ npm i -g @hideandseeklab/aoox
    $ aoox login

    In 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.

  2. 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 -- … becomes aoox run --set <slug> --env development -- ….

  3. Always pass --set and --env locally

    Without them you get the deployed app's variables
    aoox run -- … with no --set/--env needs 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/--env exactly one environment is injected: no links, no own env.
  4. 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.
  5. Tools that only read a file

    $ aoox env pull --set backend --env development --file .env

    The file is written with mode 0600 and never overwritten without --force. Make sure .env is in .gitignore; aoox warns when it is not.

  6. Do you need aoox link?

    Not for local variables: --set/--env only need you to be signed in. You do need it for aoox deploy, for aoox run without flags, and for managing links with aoox env links/link/unlink. It writes .aoox.json (panel address, project and application ids and names, no secrets), which is safe to commit.

  7. CI

    Skip aoox login and set AOOX_URL and AOOX_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/--env are 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 image

build 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:

OrderSourceNameVersion
1package.jsonname (scope removed: @acme/web becomes web)version
2pyproject.toml[project] or [tool.poetry] nameversion (skipped when dynamic)
3Cargo.toml[package] name[package] version (not a workspace one)
4composer.jsonname (vendor/pkg becomes pkg)version, if present
5pubspec.yamlnameversion
6*.csprojAssemblyName, else the file nameVersion, or VersionPrefix-VersionSuffix (one file only)
7pom.xmlartifactId of the projectversion of the project (not the parent&apos;s)
8VERSION—first non-empty line
9git tag—the tag HEAD is on, without a leading v
10folder namethe 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 seeMeaning
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:5000A 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.

Needs 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 changed meaning
Before 0.1.0-alpha.8 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 date

If 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:

See what would change first, then apply
$ sudo aoox reinstall --dry-run
$ sudo aoox reinstall
StepWhat 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).
Composedocker-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.distMerged, 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.
Restartdocker 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).

To repair a running install, use aoox reinstall
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

CommandDoesFlags
aoox installInstalls 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 loginStores the dashboard address and API token for other commands (the API address is discovered automatically).--url/-u, --token/-t
aoox whoamiShows the account and panel currently in use.--url/-u, --token/-t, --json
aoox linkLinks a repo folder to an application in the panel (used by aoox deploy).--project, --app
aoox deployBuilds 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 | promoteLists, 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 | unlinkShows 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 setSets a custom domain for the panel itself (dashboard + API) without SSH.--web, --api, --acme-email, --[no-]domain-only
aoox domain clearRemoves the panel's custom domain and goes back to IP access (IP:3000), after a confirmation.--yes/-y
aoox registry domainSets or clears a custom domain for the self-hosted registry.--set, --clear
aoox registry passwordShows 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 updateUpdates the aoox CLI itself from npm (replaces the old panel update — see aoox update panel).--check
aoox update panelChecks or applies an update for the aoox panel itself (the old aoox update).--apply
aoox registry buildBuilds ./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 pushPushes an existing local image to the registry; refuses a version that already exists there.--yes/-y, --registry, --force
aoox reinstallRepairs/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

SourceContentsNotes
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 / --tokenOverride 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_TOKENThe same values, via the environment.Used in CI; also shown in --help.
  • Precedence: flag → environment → config.json contents. If none is complete, the CLI tells you to run aoox login.
  • Working with several panels? Don't keep logging in and out — save one as the default and override it with --url/--token as needed.

In CI (no terminal)

aoox login needs an interactive terminal. In a pipeline, skip login and supply credentials via the environment:

.gitlab-ci.yml
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)
Store the token as a masked CI variable, and create a separate token per pipeline so it can be revoked without affecting the others.

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.pem still 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

MessageCause & 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 terminalRunning in CI/a script. Pass --url and --token, or set AOOX_URL and AOOX_TOKEN.
Cannot reach the panelWrong 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 HTTPYou 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 panelA 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 commandThe 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 yetaoox 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 requiredRun 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.
Not available yet
Commands for projects, standalone log tailing, and databases; an official npm package; shell autocomplete; --json output for every command (currently just whoami).

Next steps

Found a mistake? Edit this page on GitHub ↗