Deploy a multi-container application onto Cycle. You supply the app knowledge (image, command, env, ports, volumes, how many members and how they address each other); the tool turns it into a Cycle stack spec, creates and builds the stack, and deploys it into an environment. For a workload that truly needs a full virtual machine use deploy_virtual_machine.
Modes: by default everything deploys through a stack (containers that version and deploy together). One application is ONE call and one stack: every container it needs — data nodes, UI, sidecars — goes in the same 'containers' list. Containers are created stopped, so "incremental" means starting and checking them one at a time afterwards, not deploying them in separate calls; a stack cannot be extended with more containers later. Separate stacks are for independent applications that release on their own. stack:false creates one-off containers directly in the environment with no stack objects. Every rule below applies to both.
Existing stacks: a call with 'containers' ALWAYS creates a new stack. When the user names a stack that already exists — built by this tool, the dashboard, or from a git repo — pass stack_id instead and leave 'containers' out (or pass the original list only to link domains). It deploys the stack's latest usable build into the environment; build_id picks a specific build, rebuild:true generates a fresh build from the stack's source first. Containers the stack already has in that environment are left untouched unless 'redeploy' says to update them — rebuild:true plus redeploy:{reimage:true} is how a new image tag or commit is rolled out to a running deployment. preview:true reports the stack's recent builds, the containers the chosen build creates, and which existing ones would be updated.
MUST GET RIGHT — Cycle accepts these and the application is then silently unreachable or insecure. Preview checks them and an error finding blocks the deploy:
- Listen on ::. The private network is IPv6-ONLY (discovery serves AAAA records) and the load balancer reaches backends over it, so a process on 0.0.0.0 or localhost is unreachable from siblings and from the LB. IPv4-default apps need IPv6 enabled explicitly (mongod --ipv6, HOST=::), and so do client libraries (Node's ioredis needs ?family=6 on the URL).
- TLS through the LB needs BOTH "443:<backend port>" and "80:80" in ports. "443:80" is Cycle notation: LB ingress 443 routes to backend 80 with TLS terminated at the LB.
- Secrets never go in env or args — both are stored in plain text in the stack. Sequence: deploy (containers are created stopped) → create scoped variables with manage_scoped_variable and source.secret:true → start with cycle_control_container. First boot reads them (postgres initdb reads POSTGRES_PASSWORD once; redis needs its config file present).
- Stateful containers deploy as a SINGLE instance with their own volume. Model a clustered app as N separate single-instance stateful containers (mongo-0, mongo-1, ...), each with its own volume and unique hostname. Containers reach each other by hostname; the environment must be live for discovery to resolve them.
- Placement is unconstrained by default — a container may land on ANY server whose pool allows it, so cluster members can spread across providers. Pin hardware with constraints.node.tags.all (every tag present) or .any, and confirm the tag exists with list_servers BEFORE deploying; a tag matching no server leaves the container with no deployment target.
- public defaults to "disable". Set it only to expose a container.
Images — each container sets exactly ONE of: image (a Docker Hub target like 'mongo:7'; an existing image source with the same origin is reused), image_source (already on Cycle — prefer it whenever the user has one, it carries their registry credentials and build config), or dockerfile (repo or targz_url for Cycle to build). Registry and git credentials are never accepted directly.
Backups — 'backups' (stateful containers only) has Cycle run 'command' on the cron 'schedule' inside the container and ship its STDOUT to a backup-capable hub integration ('destination', e.g. Backblaze B2); 'restore_command' reads a backup from STDIN. All four are required; the destination must already be enabled on the hub. Commands may reference env vars (mongodump -u $USER) but the tools they call must exist in the image.
Domains — 'domain' exposes a container through the environment load balancer. A DNS zone covering it must already exist; the tool creates a LINKED record pointing at the container and never overwrites an existing one (conflicts are errors to resolve with the user). Forces public:"enable" and requires ports; TLS on the record follows from a '443:<port>' mapping. Cycle creates the environment load balancer only when the environment's services start with a public container present, so after deploying public containers into an environment without one the tool starts the environment's services itself; if that fails the response carries 'load_balancer_missing'.
'application' hint — pass an app name with NO containers to get recommended per-container config, topology, and caveats, then compose the container list from it. Unknown names return the known names instead of an error.
Workflow: 1) call with preview:true — returns the exact spec that would deploy plus checklist findings, and makes NO changes. 2) Confirm with the user, then call again without preview. Never deploy without explicit confirmation.
Asynchronous and RESUMABLE: containers appear only at the last step, so an early empty list_containers means "still working". Every response carries stack/build/job ids, 'phase', and 'recommended_action'. After a transport failure, repeat the call with the same name/deployment_id and follow 'recommended_action' ('wait', 'resume' with stack_id, or 'nothing_to_do'). A stack_id call is idempotent at every phase.