Publish your first app
Take a Docker image and turn it into an entry in the Companion App Store that anyone can install in one click.
Time: ~45 minutes · You’ll need: a published Docker image, and a GitHub account


The path from a directory to a store entry, including the draft-listing route that Testing before you submit describes:
flowchart LR
subgraph build["Build the directory"]
direction TB
A["Copy apps/_template<br/>to apps/my-app"] --> B["Write config.json"]
B --> C["Write docker-compose.json"]
C --> D["Add metadata/"]
end
subgraph ship["Ship it"]
direction TB
E["Submit for review<br/>(form, cihub submit, or PR)"] -->|"available: false"| F["Hidden draft you can<br/>install on your own Hub"]
F -->|"verified, screenshot saved"| G["Follow-up PR:<br/>available: true"]
E -->|"available: true"| H["Visible in the store<br/>on merge"]
G --> H
end
build --> shipWhat you’ll have at the end
- An app directory that satisfies every publication requirement
- A submission for review — through the web form ,
cihub submit, or a pull request against CI-Marketplace
The four required files
Every app is a directory named for its slug. In the CI-Marketplace repository it lives under apps/<slug>/. All four files are required — missing or empty ones cause the app to be skipped at deploy and flagged by the audit tooling.
- config.json
- docker-compose.json
- description.md
- logo.png
Start from the template
If you have access to the CI-Marketplace repository, clone it and copy the template:
cp -r apps/_template apps/my-appOtherwise, create a folder named my-app with the four files below. The web form and cihub submit accept the folder from anywhere on your machine.
The slug you choose is your app’s identity — the directory name and the id in config.json must match.
Write config.json
Ten fields are required:
| Field | Type | Notes |
|---|---|---|
id | string | Must match the directory name |
name | string | Human-readable name |
version | string | Semver recommended |
short_desc | string | One line, 500 characters or fewer |
author | string | You, or the upstream maintainer |
port | number | Primary port; optional for no_gui and mcp entries |
source | string | URL to the source repository |
categories | string[] | e.g. ["utilities"] |
available | boolean | true → published and visible; false → ingested as a hidden draft |
supported_architectures | string[] | e.g. ["arm64", "amd64"] |
A minimal valid file:
{
"id": "my-app",
"name": "My App",
"version": "1.0.0",
"short_desc": "A short description of what the app does.",
"author": "your-name",
"port": 8080,
"source": "https://github.com/you/my-app",
"categories": ["utilities"],
"available": true,
"supported_architectures": ["arm64", "amd64"]
}Set available: false while you’re still iterating. Your app is ingested as a
draft and stays hidden from the store until you flip it.
Optional fields worth knowing: description, website, exposable, dynamic_config, form_fields, cihub_app_version, force_pull, no_gui, and mcp. See the config.json reference.
Write docker-compose.json
JSON, not YAML, on schema version 2. Exactly one service must be marked isMain.
{
"schemaVersion": 2,
"services": [
{
"name": "my-app",
"image": "org/my-app:1.0.0",
"internalPort": 8080,
"isMain": true,
"volumes": [
{ "hostPath": "${APP_DATA_DIR}/data", "containerPath": "/data" }
],
"environment": [{ "key": "KEY", "value": "value" }]
}
]
}environment is a list of key and value pairs, not an object, and every value needs at least one character. Common per-service properties: name, image, internalPort, isMain, volumes, environment, dependsOn, entrypoint, extraLabels, plus ports, command, healthCheck, and more. See Dynamic Compose.
Pin a real image tag rather than latest where you can. Users get whatever
the tag resolves to at install time.
Add metadata
metadata/description.md— the long description on your app’s detail pagemetadata/logo.pngorlogo.jpg— at least one, and it must not be 0 bytes
Test on a device
Install your app on a Hub and confirm it starts, serves on its port, and persists data across a restart.
This step is harder than the marketplace README implies. See Testing before you submit below — the documented local route is currently disabled in the dashboard.
Capture a verification screenshot
Save a screenshot of your app running to:
e2e/screenshots/my-app.pngThis is the QA record, in the CI-Marketplace repository, that the app was installed and verified on a device. It is strongly recommended for the pull-request route and surfaced by the audit tooling, but it is not currently a hard deploy gate — the pipeline ships any apps/<slug>/ with a valid config.json. Store visibility is controlled by available, not by the screenshot.
Submit it
Pick a route. All three need the same four files and end in review — see Submitting apps to the marketplace for the details.
- Web form (recommended). Go to ci.computer/store-submit and submit your app there. You don’t need repository access.
- CLI. Run
cihub login, check the folder withcihub submit --dry-run ./my-app, thencihub submit ./my-app. It ingests your app as pending; it never publishes on its own. - Pull request. If you have access to CI-Marketplace, commit your directory and open a PR against it. The pipeline deploys on merge.
Testing before you submit
The marketplace instructions say to install your app on a device before submitting. A custom app store could point Hub at your fork, but that route is not available from the dashboard.
Hub’s Settings → App Stores → Add App Store dialog is disabled: it shows “Feature coming soon” above the detail text “Custom app store setup is under maintenance. Your Hub uses the CI Marketplace automatically; no manual store URL is needed.” The cihub CLI has no app-store command either. See Custom app stores for what the App Stores page can still do.
So test from a draft listing instead: submit your app with available: false. It deploys but stays out of the public store, and you can install it on your own Hub and verify it there. Flip available to true in a follow-up PR once it works.
Why not call the API directly? The Hub backend does still serve an app-store creation endpoint, so the capability was not deleted from the server. It is not a supported path — nothing in the UI manages a store created that way, and the request shape is not guaranteed — so it is not documented as a workaround here.
Whether the dialog returns is genuinely unsettled: the dialog text says “coming soon”, while the comment in the source that disabled it describes manual store URLs as a removed pre-Companion-Hub feature. The supported path remains unsettled.
Next
- Submitting apps to the marketplace — the reference, including AI-enabled apps
- config.json reference
- Dynamic Compose