Folder structure
Companion Hub keeps its runtime state in one folder on the host, called the data root. Hub’s configuration names it ROOT_FOLDER_HOST. Hub creates the folders below the first time it starts.
Where the data root sits depends on your operating system:
| Platform | Data root |
|---|---|
| macOS | ~/Library/Application Support/companion-hub |
| Linux | $XDG_DATA_HOME/companion-hub, which is ~/.local/share/companion-hub unless you set XDG_DATA_HOME |
| Windows | %APPDATA%\companion-hub |
The cihub CLI uses the same location. Set CI_HUB_DATA_DIR to point it somewhere else; the desktop app doesn’t read that variable. On Linux, both ignore an XDG_DATA_HOME that another snap app set, such as the one in a terminal inside a Snap Store editor, and use ~/.local/share/companion-hub instead.
- settings.json
- hub-compose.yml
- docker-compose.prod.yml
- .env.dev
- .env
The folder named ci-marketplace is the app store’s identifier. Apps you create yourself sit under a store folder named _user.
Two things live outside the data root:
- PostgreSQL data is in the Docker named volume
ci_hub_pgdata, not in a folder. Stopping Hub does not remove it. Back it up with Docker’s volume tooling if you need Hub’s own database, which holds your users, installed-app records, and registration. - The Cloudflare tunnel token is in a
tunnelfolder next to the data root, not inside it. On Linux that is~/.local/share/tunnel. Hub reads an oldertunnelfolder inside the data root only if it finds one there.
Who writes what
Hub and the desktop app generate most of this tree. Your own changes belong in user-config, in the settings.json file that the Settings page writes, and in your app’s own data.
| Path | Written by | Edit by hand? |
|---|---|---|
| app-data/ | Installed apps, at runtime | |
| apps/ | Hub, from the app store at install and update | No — updates overwrite it |
| backups/ | Hub, when you back up an app | |
| cache/ | Hub | |
| logs/ | Hub and the desktop app | |
| media/ | Hub creates the folders; you and every app that mounts it fill them | Yes — visible to every app that mounts it |
| repos/ | Hub, for git-based app stores | |
| state/ | Hub | settings.json only, or use the Settings page |
| state/traefik/ | Hub, rewritten each time Hub starts | Only your own files in dynamic/ — see Traefik below |
| user-config/ | You | Yes — this is the override layer |
| docker-compose.prod.yml | The desktop app, copied from its bundle on each launch | No — the desktop app overwrites it |
| .env.dev and .env | The desktop app, rewritten on each launch | No — the app rewrites both |
App data
The app-data folder holds each app’s persistent data, one folder per app inside a folder for its store. Each installed app has an app.env file here, which Hub generates, and most catalog apps keep their files in a data subfolder.
You can move this folder. In Settings → General settings, set App data path (appDataPath), or set CI_HUB_APP_DATA_PATH. Hub then puts app-data inside that path instead of inside the data root. Backups stay under the data root.
Apps
The apps folder holds each installed app’s definition: the manifest (config.json), the compose file the store ships (docker-compose.json), the compose file Hub renders from it (docker-compose.yml), and the store listing’s metadata folder. The layout matches app-data: one folder per store, then one per app.
- config.json
- docker-compose.json
- docker-compose.yml
Do not edit files under apps directly, because updates overwrite that tree. Put overrides in user-config instead.
Backups
The backups folder stores app backups as .tar.gz files, grouped by store and app.
- <app-urn>-<timestamp>.tar.gz
Each archive is named <app-urn>-<timestamp>.tar.gz, where the app URN is <app-name>:<store> and the timestamp is in milliseconds since 1970. It contains three things: the app’s data, its installed definition, and any overrides you keep in user-config. See Backup and restore for how retention works and Back up and prove the restore to verify one.
Logs
The logs folder contains Hub’s log files:
- error.log
- app.log
- desktop.log
error.log records errors, app.log records general Hub output, and desktop.log records what the desktop app did to start the Hub stack. The Settings → Logs tab reads the first two.
Media
Hub creates a shared media tree the first time it starts. Unlike most app data, media is not scoped to a single app, so several apps can mount it. For example, the *arr stack uses the download folders. The data folder is for finished libraries: books, comics, images, movies, music, podcasts, ROMs, and TV. The downloads, torrents, and usenet folders each have watch, complete, and incomplete subfolders.
Repos
The repos folder holds cloned git repositories for git-based app stores. A default Hub gets its catalog from CI Cloud and does not use it. See Custom app stores.
State
The state folder holds instance state that Hub needs at runtime:
settings.jsonholds the settings you change on the Settings page, your Portal credentials, and your AI preferences. It is the one file here you can edit yourself — see Custom settings. Stop Hub before you edit it.seedis the random value Hub derives its secrets and each app’s generated passwords from. Keep it withstate/when you back up or move a Hub..env.resolvedis a snapshot of the environment Hub resolved at startup. Hub rewrites it each time it starts.traefik/holds Traefik’s configuration and certificates.hardware/holds probe results about your CPU, memory, and GPU, which Hub reads when it recommends models.docker-engine.jsonrecords the Docker engine Hub chose.update-listener.tokenis the key the desktop app uses so the Hub can hand it an update. The app writes a new one each time it starts. Don’t copy or share it.inference-pinned-models.jsonrecords the models you pinned, so they load again after a restart.inference-footprint-sightings.jsonrecords how much memory loaded models used, which Hub uses to decide what fits.tailscale-serve-ownership.jsonrecords the Tailscale Serve entries Hub published, so it removes only those.CI_HUB_STATUS.mdis a status report that Hub refreshes every 15 minutes.
Hub keeps settings.json, seed, and .env.resolved readable only by the account that runs Hub (mode 600), because they hold device keys and the secret Hub derives its passwords from. Hub tightens an older, more open file the next time it starts and never opens a file to other accounts. If cihub runs as a different account than the one that runs Hub, it can’t read the device key in settings.json, and commands such as cihub claim say No device key on this machine.
The data root also holds a .docker/config.json: a copy of your Docker client configuration (registry logins and proxies only, mode 600) that Hub’s own container uses. The desktop app rewrites it at each launch.
Traefik
The state/traefik folder holds Traefik’s static configuration (config/traefik.yml), the dynamic routes Hub writes for the Hub and each app (dynamic/), the Let’s Encrypt storage (acme_storage.json), and your own certificates (tls/).
The desktop app, cihub, and Hub itself copy the bundled traefik.yml and dynamic.yml over the files in this folder when they start, so hand edits to those two files are lost. Hub also writes hub.yml, apps.yml, and port-expose.yml in dynamic/, so don’t use those names for your own files. The Persist Traefik config switch (persistTraefikConfig in settings.json, or PERSIST_TRAEFIK_CONFIG) still exists, but Hub’s startup code does not read it, so it does not stop the rewrite. Hub rewrites only those two files. To add your own routes, save a separate file in state/traefik/dynamic/, which Traefik watches and Hub leaves alone. See Customize Companion Hub compose and Traefik config.
User config
The user-config folder holds your overrides. Each app can have a custom docker-compose.yml and app.env. See Customize app config.
A hub-compose.yml at the root of user-config (or the older name cihub-compose.yml, when hub-compose.yml is absent) does not change the running Hub stack. Hub’s backend reads it only when it builds the compose commands for its log stream and log download. The desktop app and cihub up do not read it, so it can’t change the Hub’s containers or ports. See Customize Companion Hub compose and Traefik config.
- docker-compose.yml
- app.env
- hub-compose.yml
Docker Compose
docker-compose.prod.yml is the compose file for the Hub stack: the Hub, its database and queue, Traefik, and the optional tunnel and VPN containers. The desktop app copies it from its bundle each time it launches, so do not edit it. See Architecture for what it starts.
Environment
The desktop app writes the Hub stack’s environment to .env.dev on macOS and Linux, and to .env on Windows, and keeps an identical copy under the other name. It holds values such as ROOT_FOLDER_HOST, the generated database, queue, and signing secrets, the pinned Hub image, and the Portal address. The app rewrites both files each time it launches from the values it manages, so a line you add by hand is not kept. Hub itself reads the file as /data/.env inside its container.
See Environment variables for every variable Hub reads and for where each kind of change belongs.
The CLI
The cihub command-line tool is not stored in the data root. The desktop app and the Linux packages install it on your PATH. See the CLI reference.
Companion Hub does not write a VERSION file. Settings → System shows the
Hub stack version under Hub stack. To compare the CLI and the stack from a
terminal, run cihub doctor.