Deploying and running an environment
Deploying is what turns a project's description into running containers on a server. It happens at the environment — the project holds the compose document; the environment is one copy of it, on one server (or a set of servers), with its own hostnames, storage and variables. This chapter covers the whole life of an environment: choosing where it runs, previewing what will be sent, deploying and redeploying, stopping and starting, destroying, reading logs and history, and what every refusal means.
The model
A deploy is a command the control plane sends to the daemon on the target server. The control plane does the thinking — it merges the project compose with the environment's overlay, resolves variables and secrets, schedules services across servers, allocates addresses, and compiles one runtime compose.yaml per participating server. The daemon does the doing — pulls images, checks out and builds source releases, writes the files, and runs Compose. What you see in the app is the command's progress, polled until it reaches a terminal state.
Three consequences shape how the buttons behave:
- Nothing runs until a server is chosen. An environment resolves its server from its own pin or the project's default; without one, Deploy is disabled and points you at the Hosting tab.
- Preview is free; Deploy is a command. Preview shows exactly what would be sent without sending it. Deploy enqueues a command that the app tracks to completion.
- The daemon is the source of truth for status. Container status is what the daemon last reported; the app refreshes it after each tracked command and on Refresh, not on a timer.
Before you begin
- A connected server in the organization (Servers shows Online).
- A saved compose document with no blocking lint findings.
- For any site or Node app in the document, a system user assigned — Deploy stays disabled with No system user — assign one in Bindings until then.
- Manage rights on the organization.
Placement and scheduling
Open the project's Hosting tab. At Project scope, set Default project server; at an environment's scope, set its Server pin (or clear it to inherit the default).
Back on the environment's Overview, the lifecycle bar now shows Preview ▾ and Deploy instead of No server — set one in Hosting.
The pin is the anchor. Services that declare deploy.replicas or deploy.placement.* are placed by the planner across the organization's connected servers, respecting host-port conflicts, co-location and spread constraints and the per-server replica cap; services on more than one server must share a spanning (driver: overlay) network, which TurboFabric carries. Preview ▾ → Prepared compose shows one block per server whenever the plan spans more than one host.
Any connected server is a valid target, including the host a self-hosted control plane runs on. A one-server installation can deploy to itself.
Preview
Preview ▾ on the lifecycle bar opens Compose Preview.
Merged compose (the default) is the project compose with the environment's overlay applied, plus the live placement shown as x-turbopanel.placement for reading only — the stored document never holds it.
Prepared compose (via the caret) fetches what the daemon would receive: the compiled runtime compose.yaml, the generated .env of non-secret variables, the file paths secrets will be materialized at, and one YAML block per server when the plan spans more than one.
Prepared preview runs the same validation Deploy does, so a refusal shows up here first — same code, same message.
Deploy, redeploy, cacheless
Deploy (an environment with nothing deployed) or Redeploy ▾ (one that has containers) opens Confirm Deployment, showing the prepared compose.
Confirm. The command is enqueued and the bar tracks it — queued, sent, acked, running, then succeeded or failed — and container status refreshes when it lands.
Redeploy ▾ → Cacheless redeploy sends the same command with noCache: true, so image pulls and source builds ignore their caches. Use it when a latest tag moved or a build cache is suspect.
A redeploy with no change is a no-op: the platform hashes the compose document together with each source service's resolved commit, and the daemon skips work whose hash it has already applied. Push a commit or change the document, and the same button does real work.
Health-check gates
Each service carries a Health check policy (its settings page): disabled, warn, or required. At deploy, a service with policy required and no healthcheck in its compose refuses the deploy outright (health_check_missing with required: true). A service with policy warn and no health check stops the deploy once with a prompt naming the services; confirming sends the deploy again with the warning acknowledged.
Deploy strategies
An environment has a deploy strategy, which decides how the new version replaces the old one.
| Strategy | What a deploy does | Rolls back on its own? |
|---|---|---|
sequential | Stops the old version, runs any pre-deploy migration, starts the new version, then waits for every service to be healthy. This is the default for every new environment. | Yes, while it is still safe (see below). |
inplace | Brings the new containers up over the old ones and does not wait for health. The strategy an environment created before it existed keeps. | No. |
bluegreen | Starts the new version beside the old one, then switches traffic. Coming in 0.2.x. | — |
A sequential deploy goes in this order:
- Prepare — networks, image pulls and source builds. If this fails, nothing has been stopped and the old version is still serving; the deploy simply fails.
- Stop the old version. Services that keep data on a writable volume (databases and the like) stay running so a migration has something to talk to.
- Migrate — any pre-deploy command runs. From here on the schema may have changed.
- Start the new version, then the health gate: every service must be healthy. A container with a
healthcheckmust report healthy; one without must stay running for a short stable window; a one-shot service must exit cleanly; a container that exits with an error or keeps restarting fails the gate. The gate gives up afterhealthTimeoutSeconds.
How a sequential deploy that does not finish ends is shown in Deployment history instead of a plain Failed:
| Outcome | Meaning | What to do |
|---|---|---|
Rolled back (rolled_back) | The new version failed before any migration ran, so the previous version was started again and is serving. | Fix the cause (the history row gives the reason) and deploy again. |
Needs attention (needs_attention) | The environment was left stopped on purpose, or the previous version could not be restored. The usual cause: a migration had already run, so the old code is never started on a changed schema. | Read the reason, fix forward and deploy again, or restore from a backup. Check the environment before assuming it is serving. |
An environment that declares migrations: breaking is treated as past the point of no return from the start, so a failed health gate there is always Needs attention. A first deploy has no previous version to go back to, so a failure is an ordinary failure.
The strategy is set on the environment's options through the API (there is no screen for it yet), next to the compose overlay:
| Option | Values | Default |
|---|---|---|
deployStrategy | inplace, sequential, bluegreen | sequential for a new environment, inplace for an existing one with none set. bluegreen is accepted but runs as sequential until it is built. |
migrations | none, compatible, breaking, unknown | unknown. Unknown is never treated as none. |
healthTimeoutSeconds | 10 to 3600 | 120 |
drainSeconds | 0 to 3600 | 30 (used by bluegreen) |
rollbackWindowMinutes | 0 to 1440 | 0 (used by bluegreen) |
A project's options may carry the three numbers as defaults for its environments; an environment's own value wins. Send null for a key to clear it. A value out of range, or a key that does not belong (a project cannot set deployStrategy), is refused with deploy_options_invalid and the field is named. Preview (GET /environments/:id/deploy-preview) reports the strategy requested, the one that would really run (effectiveStrategy), the migration status and, when the two differ, the fallbackReasons.
Rolling deploys across servers
An environment whose services land on more than one server (through deploy.replicas or deploy.placement, see Placement and scheduling) is deployed to each of them. A sequential deploy (and bluegreen, which runs as sequential for now) then updates the servers in batches, sized by the Compose deploy.update_config.parallelism of its services:
services:
web:
deploy:
update_config:
parallelism: 2 # two servers at a time| Setting | Effect |
|---|---|
parallelism (whole number, 0 or more) | How many servers update at once. Default 1. 0 means every server at once. A negative or fractional value is refused. |
| Several services set it | The lowest value wins; 0 yields to any other value. |
failure_action: pause or rollback | Accepted. Both stop the rollout when a server fails, exactly as the default does. rollback does not undo servers that already updated. |
delay, monitor, order, max_failure_ratio, failure_action: continue | Refused at deploy with compose_field_unsupported (a warning while you save), so nothing is silently ignored. |
Only the first batch is queued. Each next batch is queued when every server in the batch before has applied the deploy. A single-server deploy is unchanged. The deploy response and the preview report rollout as { parallelism, batches }.
When a batch fails. The first server that fails or times out stops the rollout. Servers that have not started are marked failed in Deployment history with the reason rollout stopped … this server was not started, and their commands are cancelled. Servers that were already running finish. Servers that updated in an earlier batch stay on the new version: nothing rolls them back, so the environment can be left on two versions until you fix the cause and deploy again. Each server's own sequential deploy still rolls back to its previous version when it can (see the outcomes above).
An inplace environment ignores parallelism: it updates every server at once and reports parallelism: 0. A compose document that sets it gets a non-blocking warning from Preview and Deploy (deploy.update_config.parallelism is ignored: this environment deploys inplace); set the environment's deployStrategy to sequential to use it.
Coming in 0.2.x
Blue-green deploys, which keep the old version serving until the new one is healthy and then
switch, are not built yet. Until they are, asking for bluegreen gives a sequential deploy. The
other Swarm update_config settings are refused, as above.
Start, stop, restart, destroy
| Action | Where | What it does | Data volumes |
|---|---|---|---|
| Stop | Lifecycle bar | Stops the environment's containers. Files, images and volumes stay on the server. | Kept |
| Start | Lifecycle bar (shown when deployed but stopped) | Starts the existing containers again. No preview, no rebuild. | Kept |
| Restart | API (action: restart) | Stop then start. | Kept |
| Destroy | Lifecycle bar, two presses | compose down --volumes: removes the containers, their networks, the site and Node releases, the secrets on disk, the hostnames from the host's Caddy — and the data volumes. | Removed |
Stop and Start only touch existing containers, so they stay available even when Deploy is disabled for a missing system user. Destroy is the one that loses data; the app asks twice.
Container status and logs
The environment's Overview lists each service's containers with a status badge from the daemon's last report. Refresh asks again.
Logs on a container opens a live tail of its stdout and stderr, streamed from the host on demand. It is never stored: close the panel and it is gone. For the record of what a deploy did, see the next section.
Deployment history
Deployment history on the environment's Overview lists past deploy attempts with their outcome, who or what started them (a person, the system, or Push to <branch> with the short commit when a git push did) and, for each, the transcript the daemon produced — image pulls, builds, the Compose output — kept for 90 days by default (an operator can change TURBOPANEL_EXECUTION_LOG_RETENTION_DAYS).
A sequential deploy also names the engine it ran (Sequential or In place) and, when it did not finish, Rolled back or Needs attention with the reason; see Deploy strategies.
A deploy that never finished is recorded as stalled, with one of two reasons:
| Outcome | Meaning | Safe to deploy again? |
|---|---|---|
stalled_undelivered | The daemon never acknowledged the command — it was offline or the control plane restarted before delivery. Nothing happened on the host. | Yes. |
stalled | The daemon acknowledged the command but never reported an outcome — a restart mid-run on either side. It may still be running on the host. | Check the host first. |
Reference
POST /environments/:id/deploy
| Field | Type | Meaning |
|---|---|---|
acknowledgeHealthCheckWarnings | boolean | Proceed past the warn health-check prompt. Never bypasses required. |
noCache | boolean | Cacheless redeploy. |
strategy | inplace | sequential | Run this one deploy with that strategy instead of the environment's. bluegreen is refused with 501 deploy_strategy_unsupported. |
migration | string | Refused with 501 deploy_strategy_unsupported for now; declare the migration status on the environment. |
ref | string | Refused with 501 source_ref_unsupported — services deploy their compose-declared branch, and accepting a ref the build would ignore is the one outcome a caller could not detect. Omit it. |
Response: the queued command's id and the URL to poll. GET /environments/:id/deploy-preview returns the prepared shape without enqueueing.
POST /environments/:id/lifecycle
{ "action": "start" | "stop" | "restart" }. Operates on existing containers only.
POST /environments/:id/stop
Destroy. Removes containers and volumes, tears down hostnames and releases.
Command statuses
queued → dispatching → sent → acked → running → one of succeeded, failed, timed_out, cancelled. Commands carry a delivery deadline; one the daemon never picks up becomes timed_out and is recorded in history as stalled with the reason above.
The prepared shape
| Part | Content |
|---|---|
composeFiles[] | One compose.yaml per participating server, role runtime. |
.env | Non-secret variables, ${service__KEY} interpolation resolved. |
secretPlan[] | The file path each secret will be materialized at under /run/turbopanel/deployments/<project>/<environment>/secrets/. Values travel sealed, never in YAML. |
servers[] | Present only when the plan spans more than one host. |
Errors
Every refusal carries a stable error code and, where useful, a message and the offending names. Grouped by what to do about them.
Fix the document
| Code | Status | Meaning |
|---|---|---|
compose_empty | 400 | The merged document has no services. |
compose_merged_invalid | 422 | Project and overlay each saved cleanly but their merge is not valid Compose. Open Preview ▾ → Merged compose. |
compose_field_unsupported | 422 | An unsupported key is present; the message quotes why. Remove it. |
compose_field_requires_org_opt_in | 403 | A host-level feature — a gated key (privileged, cap_add, use_api_socket, …) or a path outside the service's directory — and the organization has not turned them on. An owner enables them with PUT /organizations/:id/compose-privileged-fields. See Host-level features. |
compose_host_access_requires_manager | 403 | Host-level features are on, but only an organization manager or owner can deploy them. |
compose_host_access_requires_approval | 403 | A Git-triggered deploy of host-level content no manager or owner has deployed in this form. Deploy it once from the app. |
invalid_deploy_hosting, invalid_deploy_storage | 400 | A hosting or storage row attached to the environment is malformed; the message names it. |
principal_alias_unknown, principal_required_for_service_kind, site_principal_ambiguous, source_principal_ambiguous, site_cron_unowned, site_managed_directory_unowned | 422 | Ownership of a site or Node app is missing or ambiguous — declare x-turbopanel.principal, or assign a system user on Bindings. |
source_ref_unresolved | 422 | A source service's branch does not exist or the provider refused the lookup. |
variable_unresolved, variable_ref_invalid, variable_secret_interpolation | 422 | A ${KEY} reference has no value at any scope, is malformed, or interpolates a secret into a plain field. |
docker_external_network_unregistered | 422 | external: true names a network not registered under Network → Docker networks. |
datacenter_ip_required | 422 | A hosting row binds to the datacenter scope on a server with no datacenter address. |
Fix the placement or the servers
| Code | Status | Meaning |
|---|---|---|
server_placement_required | 409 | No server resolves for the environment — set the pin or the project default on Hosting. Also the answer when the planner finds no eligible server. |
turbofabric_required | 422 | Services placed on different servers share a network that is not driver: overlay. |
host_port_conflict | 422 | Two services on the same server publish the same host port. |
constraint_unsatisfiable, colocation_conflict, max_replicas_per_node_exceeded | 422 | The planner cannot satisfy deploy.placement or deploy.replicas with the servers available; the message names the constraint. |
resource_limit_exceeded | 409 | A service asks for more CPU or memory than the organization or server ceiling allows. |
fabric_reconcile_pending | 409 | TurboFabric is still converging on a server the plan needs; retry shortly. |
fabric_reconcile_failed | 422 | TurboFabric could not converge; see the server's Network tab. |
storage_location_unavailable | 422 | A storage entry's primary copy lives on a server other than the one scheduled, and its access mode does not allow that; the message names both servers. |
Fix hostnames or TLS
| Code | Status | Meaning |
|---|---|---|
hosting_route_conflict | 409 | Two hosting rows in the environment claim the same hostname and path. |
hosting_hostname_conflict | 409 | A hostname declared in compose is already served by another hosting in the organization. |
hosting_tls_ref_unresolved, hosting_ip_ref_unresolved | 422 | A certificateRef or ipRef names something the organization does not have. |
hosting_tls_mode_unsupported | 422 | A tls.mode the platform does not implement. |
tls_pin_not_found, tls_pin_mismatch, tls_pin_not_ready | 400 | The pinned library certificate is gone, does not cover the hostname, or is not ready yet. |
acme_requires_public_bind, acme_requires_org_opt_in | 400 | Let's Encrypt needs a public bind, and the organization's Let's Encrypt opt-in under Servers → TLS. |
Acknowledge and retry
| Code | Status | Meaning |
|---|---|---|
health_check_missing | 409 | Services with no healthcheck. required: true refuses; required: false is the warn prompt — resend with acknowledgeHealthCheckWarnings: true. |
binding_endpoint_unavailable | 422 | A bound managed database has no listener endpoint yet; wait for its cluster to finish applying. |
source_ref_unsupported | 501 | A ref was sent. Omit it. |
deploy_strategy_unsupported | 501 | strategy: bluegreen or a per-deploy migration was sent. Use inplace or sequential and omit migration. |
Related
- Projects and environments — the environment's pin and overlay.
- Writing compose — the document, the field policy, the linter.
- Deployment logs — how transcripts are stored and retained.
- Container logs — why the live tail is never stored.
- Datacenter networks — spanning networks and TurboFabric.
Last updated on
Writing compose
The compose document as TurboPanel reads it — the Compose and Services tabs, the x-turbopanel extension, service kinds, the field policy, the linter, overlays and merging, releases and rollback, and every refusal code
Hosting — hostnames, ports and TLS
How a service is reached — hostnames and published ports, the three bind scopes, the organization's TLS library, Let's Encrypt behind the organization opt-in, proxy options, and every refusal code on the way