---
title: "Your first deploy"
summary: "The full path (scaffold ob.yml, inspect what Onebox understood, plan, approve, deploy) with the plan and local-confirmation artifacts explained."
description: "From an existing Compose file to a released application, in one pass."
status: shipped
read_when:
  - "Deploying with Onebox for the first time"
  - "Understanding what a plan and an approval actually are"
---

## Before you start

You need:

- Onebox installed on your machine
- a repository containing the Compose file you already run
- one Linux server reachable over SSH, with Docker Engine, Compose, and Buildx
  available to that SSH account
- the server's host key already recorded in `known_hosts`

If the server has no public SSH port, name the bastion it is reached through; see [Deploy through a jump host](/docs/guides/jump-host).

The first three steps are local; later steps state when they contact or change
the server.

## Deploy once

1. **Scaffold a project from the Compose file you already have.**

   ```sh
   ob init
   ```

   This writes `ob.yml` and puts the published schema reference on its first
   line, so your editor offers completion, hover documentation and inline errors
   that match what `ob validate` will say.

   > **Caution**
   >
   > Review the scaffolded `ob init` output before running a plan: workload types,
   > persistence semantics, readiness, job data effects, and the environment server.

2. **Check the contract. Nothing is contacted.**

   ```sh
   ob validate
   ```

   Every failure carries a typed code, the path that produced it, and where
   possible the line and the command that resolves it:

   ```
   ✗ ob: "replicaz" is not a field of this contract (line 10); did you mean "replicas"?
     at: workloads.web.replicaz
     code: unknown_field
   ```

3. **Read back what Onebox understood.**

   ```sh
   ob canonical
   ob preview
   ```

   `ob canonical` marks each value with where it came from: `# default` for one
   Onebox chose, `# shorthand` for a top-level key it moved into a workload,
   `# override` for one an environment supplied. `ob preview` prints the Compose
   runtime it will generate. Read both before the first deploy; see
   [origin annotations](/docs/get-started/reading-it-back#ob-canonical-is-the-one-to-read-in-review).

4. **Prepare the host.** This contacts and changes the server.

   ```sh
   ob bootstrap
   ```

5. **Check host readiness.** This contacts the provisioned server but changes nothing and contacts no registry.

   ```sh
   ob preflight
   ```

   Among the host, ownership, path, and collision checks, preflight verifies
   that Docker Buildx can format image-manifest digests. The capability probe
   reads local help output only, so it does not resolve an image or consume
   registry quota.

6. **Review production without changing it.**

   ```sh
   ob plan --out ob-plan.json
   ```

   The plan is a mode-`0600`, digest-protected executable envelope containing the
   typed operation graph and the exact config, Compose, host-state, image,
   rendered-Compose and payload bindings. **It expires after 15 minutes**, and
   any drift or local payload change requires a new plan.

   Each long-running workload has its own action in that graph:

   - `retain` leaves every healthy replica running when its complete effective
     runtime is unchanged;
   - `rolling` or `recreate` applies the workload's configured strategy when it
     changed;
   - an uncertain comparison uses the configured strategy and names the reason,
     such as a release-staged env file, secret, bind-mounted payload, adopted
     Compose service, missing legacy revision, replica drift or unhealthy state.

   Retention is automatic and adds no configuration or selection flag. A
   retained workload receives no pull, signal, stop, rename or Compose command.
   `ob deploy --redeploy` remains the explicit exception: on a whole-application
   no-op it performs the fresh roll shown in the sealed plan.

7. **Record a short-lived local confirmation for that exact plan.**

   ```sh
   ob approve --plan ob-plan.json --out ob-approval.json
   ```

   The confirmation is mode-`0600` and digest-bound to the plan, server, inputs,
   risk, operator label, and expiry. A changed or expired plan needs a new
   confirmation. It is tamper-evident local ceremony, not authenticated identity.

8. **Deploy with both artifacts.**

   ```sh
   ob deploy --plan ob-plan.json --approval ob-approval.json
   ```

   Completed steps stay in the terminal with their durations. This condensed
   example shows a successful deploy, ending with its release and predecessor:

   ```console
   ✓ transfer  0.7s
   ✓ web rolling ×2  4.1s
   ✓ worker retain  0.0s
   ✓ verify  0.6s
   ✓ activate  0.2s
   ✓ deployed r-0042 in 6.8s (prev r-0041 — `ob rollback`)
   ```

   Read the observed state back once more:

   ```sh
   ob status
   ```

   Status exits non-zero if the host diverges from the active release.

## Reading deployment progress

In an interactive terminal, a cyan rotating block marks the active step. The
label stays readable while elapsed time and supporting details are quieter.
Rolling workloads also show measured replica progress; healthchecks and drain
waits share that same live line:

```console
▘ web rolling · waiting for healthcheck  ■□ 1/2  3.2s
```

The count advances as replacements converge, including retirement of the old
replicas. The last segment fills after replica slot assignment succeeds. It is
a count of replica work, not a percentage of the entire deployment or an
estimate of time remaining. Transfers and other waits use a spinner because
their total work is not measured by the CLI.

On narrow terminals, the bar gives way to the count and the label shortens to
keep the live line from wrapping. Completed steps remain in scrollback.

Piped output, CI, and `TERM=dumb` use static progress lines. Any non-empty `CI`
value selects static output, except the exact value `CI=false`; this includes
`CI=0` and `CI=FALSE`. Set `ONEBOX_NO_ANIMATION=1` for static output in an
interactive terminal. `NO_COLOR`
disables colors independently of animation. JSON and NDJSON keep their
structured output contracts.

## What you now have on the host

- /var/lib/onebox/app/
  - releases/
    - 20260808T101500Z/ the release just deployed
    - 20260807T093000Z/ retained for rollback
  - current the symlink the runtime follows
  - journal append-only operation record
  - services/ supporting services, outside every release

Container and volume names are derived and stable. Workload containers are
`shop-web-1` and `shop-web-2`; the managed database is `onebox-postgres` and the
host proxy is `onebox-proxy`. Persistent and provider resources include
`onebox_postgres_data`, `onebox_services` for the service network, and `onebox-ingress` for
the host proxy network. Once a volume exists, its name cannot change without
moving data.

## Why plan and local confirmation are separate

The split creates a review ceremony and binds the answer to one exact
sealed plan. `ob approve` records a short-lived local confirmation whose digest
detects modification; it is not authenticated identity or proof that the actor
requesting the change was technically unable to create it. Environments that
need independent authorization require a future external provider, not a local
checksum artifact.

For automation, plan and deploy accept structured output, and `ob approve` sits
between them:

```sh
ob plan --output json --out ob-plan.json
echo y | ob approve --plan ob-plan.json --out ob-approval.json
ob deploy --output ndjson --plan ob-plan.json --approval ob-approval.json
```

`ob approve` always prompts and there is no flag to skip it. A routine plan asks
yes or no; one that touches data (a migration, a destructive job, or an
unknown data effect) asks for the plan's identity to be typed back: the release
ID for a deploy, the job name for a job run. It reads the answer from stdin
without checking for a terminal, so a pipeline supplies `y`, or that identity
for the stronger ceremony. Anything else records nothing and exits cancelled.

A JSON deploy buffers ordered operation events and its result into one envelope.
NDJSON streams event records and a terminal result or error record. Diagnostics
stay on stderr, so the structured stream is never polluted.

## If something goes wrong

```sh
ob status     # non-zero on divergence
ob resume     # finish an interrupted deploy
ob abort      # revert an interrupted deploy
ob rollback   # activate the previous release
```

See [Roll back a release](/docs/guides/rollback).