Skip to Content
DocumentationGuidesCreate a dynamic compose file

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.

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 v1Schema v2
schemaVersion fieldAbsentRequired, set to 2
environment shapeObject of key: valueArray of { key, value } pairs
LoadingAuto-migrated to v2 on loadImproved validation and error messages
The two schema versions side by side.

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.

Last updated on