Skip to main content
The cpln stack command deploys Docker Compose projects directly to Control Plane, automatically converting services, volumes, secrets, and networks to their Control Plane equivalents.

When to use this

Migrate from Docker Compose

Move existing Compose projects to Control Plane without rewriting configuration

Local-to-cloud workflow

Develop locally with Compose, deploy to Control Plane for production

Multi-service apps

Deploy interconnected services as a cohesive stack

Preview deployments

Generate and inspect Control Plane manifests before deploying

Prerequisites

Install the Control Plane CLI. See Installation.
You need a docker-compose.yml or compose.yaml file.
You need permissions to create workloads, secrets, volumesets, identities, and push images.

Deploy a project

This reads your Compose file and deploys all services to Control Plane. Turn off an option that is on by default with its --no- form, such as --no-deps. See the stack deploy command reference for all options.

Delete a project

Remove all resources created by a Compose deployment:
If the compose file references variables, for example image: api:${VERSION} next to build, set the same variables and pass the same --env-file options you deployed with. See the stack rm command reference for details.

Preview the generated manifests

Generate Control Plane specs without deploying:
This outputs the converted YAML so you can inspect or modify it before deployment. It builds nothing unless you pass --build, and takes --env-file, --no-interpolate and --no-deps like cpln stack deploy.

Compose variables and env files

cpln stack substitutes variables the way docker compose does:
Variables come from two places, and the environment wins over any file:
  1. The environment of the command.
  2. The .env file next to the compose file, or the files you name with --env-file in its place. Give --env-file more than once to layer files, a later file overriding an earlier one.
Only values are substituted, never keys. The contents of env_file files are used as written. Pass --no-interpolate to keep every value as written; --env-file cannot be combined with it.
To pass a literal $ to a container, such as a shell variable in command or a password, write $$, as docker compose requires: command: ["sh", "-c", "echo $$HOSTNAME"].
Docker compose reads the same .env file for local runs. To deploy other values, set them in the environment or pass --env-file.

Service env files

env_file on a service is separate from the .env file above. It loads the variables of a file into the containers of that service, while .env and --env-file feed the substitution of the compose file itself. A missing .env file is fine; a missing env_file file is not. Each path resolves against the directory of the compose file. When a file does not exist, the command stops before it builds or deploys anything, as docker compose up does, and names the file and every service that reads it. To skip a file while it is missing, list it in the long syntax with required: false:
common.env must exist. local.env is read when it exists and skipped when it does not. When the same variable is set in more than one place, the order follows docker compose: a later env file in the list wins over an earlier one, and environment wins over every env file. A service that uses extends reads the env files it inherits first, so its own env files win over them.

Compose image names and tags

cpln stack deploy builds the image of every service that sets build and pushes it to your org’s registry: The tag can come from a variable, so the same compose file tags images the same way locally and on Control Plane:
compose.yaml
  • Next to build, image must be a plain NAME:TAG. The image always goes to your org’s registry, so a registry host or a path, such as ghcr.io/acme/api:1.4.2, is rejected.
  • Services that build the same image with the same build settings build it once. Building one image from different build settings is an error.
  • A service with image only that names an image another service builds, such as migrations above, runs that image from your org’s registry.
  • cpln stack manifest builds nothing unless you pass --build; the manifest references the images either way.
Give each build its own tag, such as a version or a commit. A tag that is reused for new builds, such as 1.0, needs supportDynamicTags on the workload to redeploy when the tag moves. See Dynamic Tags.

Compose service dependencies

cpln stack deploy follows depends_on conditions the way docker compose up does. Pass --no-deps to ignore them and apply every workload at once. A service that another waits for with service_completed_successfully, such as a database migration, converts to a cron workload that is suspended, so it never runs on a schedule. cpln stack deploy starts one execution in the first location of the GVC and waits for it to finish.
compose.yaml
New services that wait on a dependency are created suspended before anything else deploys, then updated with their real spec when their turn comes, which is why they appear as both Created and Updated. This lets each dependency’s internal firewall admit them from its first deploy.
  • --wait-timeout sets the seconds each wait may take, 600 by default.
  • When a dependency never becomes ready or its job fails, the services that need it are left as they were, the rest of the stack still deploys, and the command exits with an error that names both.
  • A dependency marked required: false is waited for, but never keeps its dependent from deploying.
  • A job serves no traffic, so its ports are dropped with a warning, and it cannot mount a named volume.
  • A dependency on a service that is not in the file (unless it is required: false), a cycle, or an unknown condition is an error, as in docker compose. So is waiting with service_healthy for a service that runs as a job.
A workload’s type cannot change. If a service was deployed as a standard workload before it became a job, cpln stack deploy leaves that workload as it is, skips the services that wait for it, and explains your options. See Workload type may not be changed.

Compose dependencies in manifests

cpln stack manifest converts jobs the same way, but a manifest cannot carry the order: cpln apply creates every workload at once and starts no job, and the command prints a warning saying so. Deploy with cpln stack deploy to keep the order, or start each job yourself after applying:

Customize workloads with x-cpln

Add an x-cpln block to any service to override the generated workload spec. Each top-level key in x-cpln replaces the corresponding section in the workload spec:
docker-compose.yml
The x-cpln block replaces entire spec sections, and doesn’t merge them. If you override containers, you must include all container configuration.
On a service that runs as a job, x-cpln applies on top of the cron workload. Overridden defaultOptions keep the job suspended unless they set suspend themselves, and type must stay cron.

Available overrides

Service-to-service communication

Update service URLs to use the Control Plane local syntax:
Replace {GVC} with your actual GVC name.
See the Service-to-Service guide for the full endpoint syntax.

How workload type is determined

The converter analyzes your service definition to select the appropriate workload type:
Use x-cpln to override the automatically determined type if needed.

Translation reference

Resource mapping

Port protocol

Specify the protocol directly in the port string:

Defaults

Secrets and configs

Both secrets and configs are converted to Control Plane secrets:
  • Default mount path: /run/secrets/{name} (if no absolute path specified)
  • Identities and policies are automatically created for workloads using secrets

Healthcheck conversion

Docker healthchecks are converted to readiness probes: Supported test formats:
  • String: "curl http://localhost/health" → /bin/sh -c wrapper
  • CMD: ['CMD', 'curl', 'http://localhost'] → direct execution
  • CMD-SHELL: ['CMD-SHELL', 'curl http://localhost'] → /bin/sh -c wrapper
  • NONE: Disables the readiness probe
Durations use the docker compose units h, m, s, ms and us, alone or combined such as 1m30s, and are rounded up to whole seconds. A duration without a unit, such as 30, is not a compose duration: it is ignored with a warning, and the platform default applies. A service that runs as a job gets no readiness probe.

Network modes

GPU support

Services with GPU devices are automatically configured:
GPU workloads receive:
  • NVIDIA T4 GPU
  • Minimum CPU: 2000m (overrides default)
  • Minimum Memory: 7168Mi (overrides default)
  • Capacity AI: Disabled

Service inheritance

Extend services from the same or different compose files:
Child values take precedence over parent values.

Limitations

The following Docker Compose features are not supported:

Troubleshooting

Update hostnames to use the Control Plane local syntax:
Ensure both services are in the same network or no networks are defined (global network).
Ensure all ports are explicitly listed in ports or expose in your Compose file. Only services with ports defined get external inbound access.
Secrets referenced in Compose are converted to Control Plane secrets. The converter automatically creates identities and policies. Check that:
  • The secret file exists at the specified path
  • You have permissions to create secrets, identities, and policies
The command stops before it builds or deploys anything, with an error such as:
The env_file of each service named in the error lists this file. Create the file, or list it with required: false as shown in Service env files. The .env file that variable substitution reads is optional and never causes this error.
Directory bind mounts are not supported. Convert to either:
  • Named volumes (for persistent data)
  • File bind mounts (for configuration files → converted to secrets)
The command stops with an error such as:
A service that sets both image and build is built and tagged with the image name, and always pushed to your org’s registry, so image cannot name another registry or hold an unresolved variable. To fix it:
  • Remove a registry host or path, such as ghcr.io/acme/, from image.
  • If image holds a variable, such as api:${VERSION}, set it and do not pass --no-interpolate. An empty tag, such as api:, means the variable is not set.
cpln stack deploy fails with Workload type may not be changed and prints a hint that names each rejected workload, such as:
The rejected workload keeps running its previous version, and the services that wait for it are not deployed. To keep the workload’s current type:
  • For a service that becomes a job, pass --no-deps. Every workload is then applied at once, with no jobs.
  • For any other service, undo the change that set the new type, such as an added or removed named volume, or an x-cpln type.
Changing the type is a last resort. The platform allows it only by deleting the workload with cpln workload delete and deploying again, and the workload and everything that calls it are down in between. The CLI never deletes a workload for you.
cpln stack deploy exits with errors such as:
A service is applied only once every dependency it waits for is ready or has completed, so the services that need a failed dependency keep their previous version. Check the dependency’s logs with cpln logs, or its job executions with cpln workload cron get, fix it, and deploy again. For a dependency that needs longer than 600 seconds, raise --wait-timeout.
By default, all services can reach each other. If you’re using named networks, ensure communicating services are in the same network.

Next steps

Apply YAML Manifests

Deploy resources from YAML files

Service-to-Service

Configure internal networking

Workload Reference

Understand workload configuration

Stack Command Reference

Full stack command reference