Creating a dynamic compose file
Dynamic compose is the format Companion Hub uses to describe an app: a simplified, custom version of the docker-compose.yml file that gives you more control over how the app is deployed. Hub reads an appβs docker-compose.json and generates the Docker Compose file that runs it.
This guide covers Schema Version 2, the current and recommended version. If youβre upgrading from an older version, see the migration section below.
Here is an example docker-compose.json file for a simple app like Nginx:
{
"schemaVersion": 2,
"services": [
{
"name": "nginx",
"image": "nginx",
"internalPort": 80,
"isMain": true
}
]
}These few lines are enough to deploy an nginx app.
How the files fit together
docker-compose.json is the source of an appβs compose. Hub generates the appβs docker-compose.yml from it each time the app installs, updates, starts, or restarts, so you never write that file yourself. config.json describes the app to the store, and its dynamic_config field defaults to true. Traefik labels land on whichever service you mark isMain.
flowchart LR
subgraph APP["App folder in the app store"]
CJ["config.json"]
DJ["docker-compose.json<br/>schemaVersion: 2, services[]"]
end
V1["v1 docker-compose.json"] -. "auto-migrated<br/>when loaded" .-> DJ
CJ --> H["Companion Hub"]
DJ --> H
H --> GEN["Generated docker-compose.yml<br/>for the app"]
GEN --> MAIN["isMain service<br/>Traefik labels, internalPort"]
GEN --> OTHER["Other services<br/>dependsOn, addPorts, volumes"]Migrating from Schema v1 to v2
Schema version 2 introduces important structural changes to improve consistency and type safety. Here are the key differences:
| Schema v1 | Schema v2 | |
|---|---|---|
| schemaVersion field | Absent | Required, set to 2 |
| environment shape | Object of key: value | Array of { key, value } pairs |
| Loading | Auto-migrated to v2 on load | Improved validation and error messages |
Required schemaVersion field
All dynamic compose files must now include the schemaVersion field set to 2:
{
"schemaVersion": 2,
"services": [...]
}Environment variables structure change
v1 format (object):
{
"environment": {
"FOO": "bar",
"PASSWORD": "${MYAPP_PASSWORD}"
}
}v2 format (array of key-value pairs):
{
"environment": [
{
"key": "FOO",
"value": "bar"
},
{
"key": "PASSWORD",
"value": "${MYAPP_PASSWORD}"
}
]
}Automatic migration
Companion Hub automatically migrates v1 schemas to v2 format when loading apps. However, itβs recommended to update your docker-compose.json files manually to take advantage of the improved validation and error messages.
Creating your first dynamic compose file
Build the docker-compose.json file one setting at a time.
Creating the file
In your app folder, create docker-compose.json next to config.json. If you started from the apps/_template folder in the marketplace repository, the file already exists, and its config.json already contains "dynamic_config": true. That field defaults to true, so you only add it if you want to be explicit.
Defining the services
Now add the schemaVersion field and your services array which defines all your services:
{
"schemaVersion": 2,
"services": []
}The schemaVersion field indicates which version of the dynamic compose schema youβre using (always use 2). The services key is the same as in the docker-compose.yml. Inside this array you can add multiple apps.
Adding a service
Letβs add a service called myapp:
{
"schemaVersion": 2,
"services": [
{
"name": "myapp"
}
]
}The name key defines the service name. Hub doesnβt set a container name, so Docker Compose derives it from the appβs project name and this service name. Find the exact container name with cihub app list.
Adding an image
Add your Docker image. For this example weβll use myapp:latest:
{
"schemaVersion": 2,
"services": [
{
"name": "app",
"image": "myapp:latest"
}
]
}Exposing the UI
If your service exposes a web UI, add the port and main service configuration:
{
"schemaVersion": 2,
"services": [
{
"name": "myapp",
"image": "myapp:latest",
"internalPort": 80,
"isMain": true
}
]
}The internalPort specifies your appβs port, and isMain indicates where the traefik labels should go. Traefik labels should always be on the part of the app that exposes the UI.
Adding volumes
To store app data, you can bind mount directories. For example, to mount ${APP_DATA_DIR}/data/myapp to /data with read-write permissions:
{
"schemaVersion": 2,
"services": [
{
"name": "myapp",
"image": "myapp:latest",
"internalPort": 80,
"isMain": true,
"volumes": [
{
"hostPath": "${APP_DATA_DIR}/data/myapp",
"containerPath": "/data",
"readOnly": false
}
]
}
]
}The volumes key is an array where you can define multiple volume mounts.
Adding environment variables
Environment variables in schema v2 use an array format with key and value pairs:
{
"schemaVersion": 2,
"services": [
{
"name": "myapp",
"image": "myapp:latest",
"internalPort": 80,
"isMain": true,
"volumes": [
{
"hostPath": "${APP_DATA_DIR}/data/myapp",
"containerPath": "/data",
"readOnly": false
}
],
"environment": [
{
"key": "FOO",
"value": "bar"
},
{
"key": "PASSWORD",
"value": "${MYAPP_PASSWORD}"
}
]
}
]
}You can add as many environment variables as needed and use values from form fields as you normally would. Note that value can be a string, number, or boolean.
Adding multiple ports
Many apps require multiple ports for features like APIs or setup UIs. Add additional ports like this:
{
"schemaVersion": 2,
"services": [
{
"name": "myapp",
"image": "myapp:latest",
"internalPort": 80,
"isMain": true,
"volumes": [
{
"hostPath": "${APP_DATA_DIR}/data/myapp",
"containerPath": "/data",
"readOnly": false
}
],
"environment": [
{
"key": "FOO",
"value": "bar"
},
{
"key": "PASSWORD",
"value": "${MYAPP_PASSWORD}"
}
],
"addPorts": [
{
"containerPort": 8080,
"hostPort": 8080,
"tcp": true
},
{
"containerPort": 25565,
"hostPort": 25565,
"udp": true
}
]
}
]
}Create an addPorts array and define your container and host ports as numbers (not strings). Specify if the port is UDP or TCP - if not defined, TCP is assumed. For ports that need both TCP and UDP:
"addPorts": [
{
"containerPort": 8080,
"hostPort": 8080,
"tcp": true
},
{
"containerPort": 8080,
"hostPort": 8080,
"udp": true
}
]Adding a healthcheck
Add container health monitoring with a healthcheck:
{
"schemaVersion": 2,
"services": [
{
"name": "myapp",
"image": "myapp:latest",
"internalPort": 80,
"isMain": true,
"volumes": [
{
"hostPath": "${APP_DATA_DIR}/data/myapp",
"containerPath": "/data",
"readOnly": false
}
],
"environment": [
{
"key": "FOO",
"value": "bar"
},
{
"key": "PASSWORD",
"value": "${MYAPP_PASSWORD}"
}
],
"addPorts": [
{
"containerPort": 8080,
"hostPort": 8080,
"tcp": true
},
{
"containerPort": 25565,
"hostPort": 25565,
"udp": true
}
],
"healthCheck": {
"test": "curl --fail http://localhost || exit 1",
"retries": 3,
"interval": "30s",
"timeout": "10s"
}
}
]
}Adding dependencies
If your service depends on other services (like a database), add the dependsOn key:
{
"schemaVersion": 2,
"services": [
{
"name": "myapp",
"image": "myapp:latest",
"internalPort": 80,
"isMain": true,
"volumes": [
{
"hostPath": "${APP_DATA_DIR}/data/myapp",
"containerPath": "/data",
"readOnly": false
}
],
"environment": [
{
"key": "FOO",
"value": "bar"
},
{
"key": "PASSWORD",
"value": "${MYAPP_PASSWORD}"
}
],
"addPorts": [
{
"containerPort": 8080,
"hostPort": 8080,
"tcp": true
},
{
"containerPort": 25565,
"hostPort": 25565,
"udp": true
}
],
"healthCheck": {
"test": "curl --fail http://localhost || exit 1",
"retries": 3,
"interval": "30s",
"timeout": "10s"
},
"dependsOn": {
"service1": {
"condition": "service_healthy"
}
}
}
]
}For simple dependencies without conditions, you can use the shorter array format:
"dependsOn": ["service1", "service2"]Other configuration options
For additional configuration options like custom commands, network modes, resource limits, and more, see the Dynamic Compose Reference documentation.
Try it out
Validate your dynamic compose configuration with the validator on the Dynamic Compose Reference page. For the schema Hub itself enforces, run pnpm test in a clone of the marketplace repository, which checks every appβs docker-compose.json.
For a complete reference of all available configuration options, see the Dynamic Compose Reference documentation.
Docker Compose to Dynamic Config Converter
Converting an existing docker-compose.yml file to Companion Hubβs dynamic compose format can be time-consuming and error-prone. Weβve created a tool to help you automatically convert your Docker Compose files to the dynamic compose format.
Docker Compose YAML
Dynamic Compose Output
The converter works for most cases, but please review the converted configuration to ensure it meets your needs. Some advanced features may not be fully supported.