Skip to main content

Instance setup and teardown

A Project stores two ordered lists of named commands: setup steps and teardown steps. Orbit runs the setup list after it activates a new development Instance, and the teardown list before it removes an active development Instance. Setup does not require a Route: an unrouted monorepo default becomes active and runs the same list. Its setup commands can install dependencies in each subproject without application metadata inspection at the repository root. Production Instances run neither list. They use deploy steps. A Project’s development deploy steps are a third, independent list. Setup and teardown do not run that list. Each step is one database row with a name, a command string, a timeout, and a position. Orbit writes no script into the checkout. The lists belong to the Project, and no Instance keeps a copy. The next run uses the lists as they are at that moment.

Record a step

Record the steps on the Project before you create an Instance. --project selects the Project by its numeric ID.
destroy asks for confirmation with No selected. --yes confirms without a prompt and is required for JSON and noninteractive calls. The Gateway checks each change against these limits and stores nothing when one fails. A list holds at most 32 steps. The timeouts of one list add up to at most 540 seconds, so a whole list fits in one API request. The Gateway stores a step only when its timeout is inside that limit, and a later read returns the stored timeout. A list stored before that limit can total more. The Gateway keeps it, and accepts a change only when the change does not raise the list’s total. A lower total becomes the new ceiling, so do not lower a step on its own to make room.

Make room in a full list

To add a step to a full list, or to move time from one step to another, change the other steps in the same write. Repeat --rebalance=NAME=SECONDS for each step whose timeout changes. The Gateway checks the whole list after the change, so the total must stay within 540 seconds, or within the stored total of an older list.
The first command adds a 180-second step and lowers build-assets by 180 seconds. The second gives that time back to build-assets and takes it from install-js. A write that would raise the total stores nothing and fails with The lifecycle list timeout total is too large. Authorized reads return the commands. Activity records no input for the step commands and instance:setup, so it never holds command text or command output.

Checkout access

The Gateway prepares a development checkout as the Node’s managed user. The checkout and the files in it stay owned by that user and group. Every Gateway Git command on the checkout runs as the managed user. Production releases are unchanged. Every Gateway Git call sets core.hooksPath=/dev/null and core.fsmonitor=false, so a hook path or a filesystem monitor in .git/config never runs. A linked worktree is added without a checkout; the branch checkout at resolve writes its files. Caddy can read only the web root and the public storage target; Caddy access describes the walk. A task workspace is a checkout inside its own task VM. There the VM’s managed user orbit owns it and runs the agents, the setup steps, and the checks. The Gateway names the Instance’s Git directory from its records: <seed>/.git for a worktree of a seed, otherwise <checkout>/.git. It never asks Git for it, because Git follows .git/commondir and a .git file in the checkout. The managed user writes task metadata under $(git rev-parse --git-path orbit). In a linked worktree this is its private administration directory, not a directory under the .git pointer file. The Gateway validates both the pointer and its return path, and pins owned directories before publishing metadata.

Credentialed Git

On a task VM none of this runs: the Gateway fetches and pushes in its own mirror and sends no token to the VM. See Git on a task VM. The rest of this section covers development checkouts on other Nodes. A fetch and ls-remote carry a GitHub token. ls-remote runs outside the checkout. A fetch runs in the checkout as the managed user, against the Project repository URL with explicit refspecs, never against a configured remote. It sets credential.helper empty, http.followRedirects to false, and core.alternateRefsCommand to true, besides the hook and fsmonitor overrides above. Only HTTPS may carry the fetch: protocol.allow is never, and each transport is set on the command line, so ssh, ext, file, and git stay off. The token exists only in the environment of that one git command. It travels on the standard input of the SSH process, never in an argument or a file. The checkout’s .git/config is not trusted. The operator writes it, and so does every program that runs as the managed user: setup steps, development Processes, and any agent working in the checkout. Command-line settings outrank that file, so a planted credential helper, hook, url.*.insteadOf, or transport setting cannot run a command or move the fetch off HTTPS while the token is in the environment.

Run setup

A create that fails before activation cleans up its owned resources without running setup or teardown, and keeps the original failure code. If cleanup cannot finish or the process is interrupted, removal accepts the pre-activation states reserved, checkout_prepared, and source_resolved and skips teardown. See pre-activation removal. instance:create runs the setup list after the Instance and its Route are active, and after the database clone or the database on a server when it applies. Setup steps receive two variables: Orbit reads them again before each step. Project steps own the dependency copy, including nested monorepo folders. When the default has not deployed at source preparation, Orbit records an empty seed decision and setup installs from lock files. Retries keep that decision. When Orbit registers an Instance, it records a seed only if the adopted source commit matches the default’s deployed commit. It never pairs an older source with newer seed dependencies. A task VM has no default Instance, so its workspace has no seed. Instead, source preparation can take a workspace template: a prepared checkout of the default branch next to the new checkout, on the same filesystem. Preparation checks that the template is a repository of the managed user with the Project’s origin, and renames it to the checkout instead of cloning. Its ignored files, such as vendor and node_modules, stay. Without a template it clones as usual. A routed Laravel Instance has its .env before setup runs. When Orbit creates that file, it already holds APP_URL, a usable APP_KEY, and the Project’s APP_NAME, so setup does not need key:generate. See Laravel application URL. A routed task VM workspace gets that .env and its PHP packages before the baseline check, and its Route after the check started. Its setup steps must not need the Route. See Start the baseline check before the Route. Activation records failed_step: setup in the same transaction, so a Gateway interruption before or during setup cannot make an identical create retry report success without setup. Orbit clears the marker only after setup completes. instance:create also marks a new Instance as unfinished until its first setup completes, through instance:create or instance:setup. Only that mark lets a create retry finish the Instance’s database and then run setup. A live Instance whose later instance:setup failed also has failed_step: setup, but it lacks the mark: a create retry returns instance.setup_step_failed and changes nothing. Only the request that activated an Instance removes it after a failure. A retry that resumes an unfinished create keeps the Instance when the database or a setup step fails, and its error says the Instance remains. Each command runs with bash -eu at the repository root of the code the Instance serves, even when the Laravel application directory is nested. For example, a setup command for root apps/site/public must use cd apps/site && composer install to install that application’s dependencies. Teardown and task-check commands also keep their repository-root scope. Each setup command runs on the Instance’s Node, as the Node’s managed user. This is the instance:create and instance:setup path. A task workspace does not use it. The task baseline runs the same commands, also as the managed user, inside the task check. Project check describes that run. That root is the checkout. A default Instance deploys in its checkout too, so setup sees the code that runs. Its seed is its own checkout, so its setup gets empty seed variables. Setup, teardown, and a development deployment of the default share the lifecycle lock on the checkout, so they never run at the same time. Setup and teardown export VP_HOME to the Node’s resolved Vite+ store. Project-local vp processes inherit that value even in these non-login shells, rather than using the default ~/.vite-plus. Commands read no input. Orbit discards their stdout and retains only a bounded 1 KiB stderr tail internally; public errors do not include command output. When a step ends, for any reason, Orbit kills its process group, so background processes do not survive the step. Each run holds a lifecycle lock on the Instance: an exclusive flock on its checkout directory, so the lock leaves no file behind. If another operation holds that lock during instance:create, Orbit keeps the active Instance and records error_code: instance.lifecycle_busy. An identical create retry then reports that setup must run; use instance:setup to retry the list. A busy lock never removes the Instance. The first command that exits non-zero or times out stops the list. Then Orbit rolls back the new Instance:
  1. It runs every teardown step.
  2. It removes the Instance with forced removal, which also deletes a dirty checkout.
  3. It returns instance.setup_step_failed with the failed setup step, or instance.setup_step_unavailable with outcome: missing for a missing or non-executable command (exit 127 or 126).
An unavailable setup error keeps its step, Node, and not-found diagnostic and says the Instance was removed. A failed teardown step is named too. A teardown failure during this rollback does not keep the Instance. Rollback never removes another Instance. Orbit keeps the Instance, with its setup marked failed, in three cases: Incomplete or unconfirmed cleanup keeps the setup error’s classification and details. When setup was unavailable, the error also keeps its step, Node, and not-found diagnostic alongside the cleanup annotation. Inspect the Instance before you retry. Registration verifies the source’s own state, not unrelated refs in its shared repository. In-place adoption records the verified destination before it closes the environment file’s permissions. For a nested Laravel root such as apps/site/public, that file is apps/site/.env; checks for cached Laravel configuration also use apps/site/bootstrap/cache/config.php. Git identity keeps its repository-wide scope. A retry after that checkpoint checks the destination’s Git identity rather than the pre-adoption digest, so Orbit’s own permission change does not prevent recovery. Setup still runs only after adoption completes. instance:register runs no setup. instance:register --setup runs the setup list after adoption. instance:setup runs the list again on an active development Instance. Both keep the Instance when a command fails and return instance.setup_step_failed, or instance.setup_step_unavailable when the command is missing or not executable. Every run starts at the first step.
instance:clone runs neither list.

Deadlines

One API request runs a whole list. The request’s remote work ends after 570 seconds, and its forward work stops 20 seconds earlier to leave time for cleanup. Each step’s timeout is cut to the time that remains. instance:create keeps 150 seconds back from its setup list for rollback: up to 60 seconds for the teardown list and 90 seconds for the removal. Nested operations, including TIA artifact retrieval, keep their own local timeout while preserving that hold and the request cleanup reserve. They cannot extend the parent request deadline. instance:setup and instance:register --setup roll nothing back, so their setup list can use the whole request. A step that the request deadline stops, or that has no time left to start, is not a failed command. The request returns command.deadline_exceeded (HTTP 504) with outcome: deadline and the step name. instance:create still rolls back and keeps that code. Lower the step timeouts until the list fits.

Run teardown

instance:destroy of an active development Instance runs the teardown list after the removal checks accept the source. Then Orbit checks the source again and deletes the Route, the source, and the record. In a forced removal of a checkout with worktrees, each member runs its own teardown list. Teardown may delete ignored files. It must keep the checkout, its Git identity, and its worktrees. When teardown changes tracked files, normal removal refuses. Retry with --force to discard them. The first teardown command that exits non-zero or times out stops the removal. The Route, source, and record stay, and the command returns instance.teardown_step_failed with the step name. A missing or non-executable command (exit 127 or 126) returns instance.teardown_step_unavailable instead, with outcome: missing. The message names the step and its Node and says the command was not found or is not executable. Setup uses the same distinction with instance.setup_step_unavailable. Orbit retains a bounded stderr tail internally, without including command output in the public error. Fix or destroy the step, then run instance:destroy again.

Bootstrap the Orbit repository

The Orbit Project can record bin/bootstrap as a setup step, or record its locked dependency installs as separate steps. It installs the locked dependencies, seeds caches, and runs the checks in all five Composer projects. A cold bootstrap can exceed the request deadline.
Task workspaces are not created with instance:create, so this create-time run does not happen for them. The task baseline check runs the Project setup steps before the task check instead. See Project check and Implementation loop.

Beast setup changes after deployment

These are operator steps on shared beast, not task fixtures. Re-read Projects 33 (orbit-website) and 46 (orbit) and their ordered setup and development deploy lists before changing them. Preserve their Instance-specific environment, key and database setup. Finish initializing Orbit’s default Instance 303 without a Route, and deploy the default of each Project successfully before using it as a seed.

Project 33

Record ordered development deploy steps for the website’s locked Composer and Bun installs, migrations, and asset build. Keep installs, migrations, and builds required. Do not put Instance-only environment, key, or database-copy setup in this list. Read the default Instance’s ID from orbit project:show 33 --json, then run orbit instance:deploy ID --json and check its final result before enabling seed copies. Add a first setup step that copies the website’s vendor and node_modules from ORBIT_SEED_PATH when it is nonempty. Use cp -a --reflink=auto into absent destinations. Keep the locked incremental install afterward. An install that deletes the destination first discards the copy.

Project 46

Record ordered development deploy steps for every locked dependency tree: the repository root, apps/cli, apps/gateway, apps/docs, apps/web, packages/php-sdk, and apps/e2e. Run the Gateway migrations and the builds required by the deployed applications after their installs. Keep these steps required. No step runs a test suite: main CI fills the main caches. Preserve Instance-only setup in the setup list. Retry orbit instance:create 46 9 default --json to finish the existing SourceResolved Instance 303 without a Route; confirm its identity and active status, then run orbit instance:deploy 303 --json and check the final result. Add a first setup step that copies present vendor and node_modules folders at the repository root and under apps/cli, apps/gateway, apps/docs, apps/web, packages/php-sdk, and apps/e2e. Include each Composer project’s .orbit-tia. Pint’s vendor/pint.cache and PHPStan’s vendor/phpstan/cache travel with vendor. Keep locked installs for every dependency tree as the empty-seed fallback. Never copy .env, databases, logs, build runtime files or symlinks that point outside the seed.

Cache ownership

Register Orbit’s stable default store with bin/tia-cache register --repository=/fast/apps/orbit/default. The cache worker runs as the managed user that owns the store and imports the main CI artifacts with that user’s gh login, so the login must read nckrtl/orbit and its Actions. Run bin/tia-cache refresh --repository=/fast/apps/orbit/default once, then check that bin/tia-cache status --json --remote lists all five projects in current. Create a disposable new Instance to check nested dependency copies. Test the empty-seed fallback separately. Point any ORBIT_MAIN_CACHE_STORE override at /fast/apps/orbit/default/.git/orbit-tia/v1. Stop the old ext4 cache worker before retiring /home/nckrtl/orbit/.git/orbit-tia/v1/checkout and its separate repository. Preserve unresolved failure logs. Do not remove the old main repository or any unrelated linked worktree. A setup copy step must be retryable: keep an existing destination, stage a missing directory, then rename it into place only when cp succeeds. A copy failure fails that step rather than leaving a partial dependency tree. On ZFS, --reflink=always can enforce shared blocks; --reflink=auto provides the ordinary-copy fallback. Verify shared blocks on beast and write isolation by changing a copied dependency or cache in a disposable Instance and checking that the selected seed stays unchanged. Remove only that disposable Instance after the check.

Configure Orbit’s task policy

Orbit’s task check is explicitly composer check; new Project defaults do not supply it. Record setup steps for each dependency tree that this repository check needs. The baseline runs only those steps. The live Orbit Project is id 46. Re-read it before every deployment, and stop if the id, check, or lists differ from the handoff.

Preflight

These reads change nothing. Run them against the Gateway that will receive the deployment.
Confirm task_check is composer check. Confirm each stored timeout_seconds is an integer from 1 to 540. A new list totals at most 540 seconds. A list stored before that cap may total more, and a later change may not raise its total. To add a step to such a list, rebalance the other steps in the same write. The task baseline runs each setup step with that step’s own timeout, outside the API request deadline. instance:create, instance:setup, and instance:destroy run one whole list inside one request. Remote work ends at 570 seconds, and forward work stops 20 seconds earlier. instance:create holds 150 seconds back: 60 for teardown and 90 for removal. A setup list whose timeouts cannot fit is cut with command.deadline_exceeded. Lower those timeouts before relying on instance:create or instance:setup for this Project. Do not raise the total.

Gateway-owned setup steps

A setup step whose command is @orbit/tia-baseline or @orbit/ci-caches runs a Gateway-owned operation, not a shell command. The Gateway renders its command right before the step runs, within the step’s timeout. Development Instance setup, the task baseline setup list, and a task VM workspace template build run it the same way. Teardown rejects it. Its errors name the step. The check log shows at most the first 4096 characters of a setup command, so an operation’s payload does not fill the log.

Restore a CI TIA baseline

Add an opt-in setup step after dependency installation and asset builds:
@orbit/tia-baseline is a Gateway-owned operation, not a shell command. It runs during development Instance setup and the task baseline setup list. Teardown rejects it. It supports a root-level Pest project with the TIA --baseline option. A monorepo needs its own setup policy. The Gateway uses its GitHub App with Actions: read for this one repository. It reads at most 100 successful runs of tia-baseline.yml on the repository’s current default branch and chooses the newest trusted push or workflow_dispatch run that contains an unexpired pest-tia-baseline artifact. Pull request runs cannot supply a baseline. Configure the workflow to publish only after the complete suite passes. The Gateway requires a graph whose branch and commit match the selected run. Pest may omit its optional complete marker; in that case the baseline must contain recorded test results. An explicit incomplete marker is rejected. The successful full-suite workflow is the source of completeness. Only graph.json and the optional js-module-graph.cache.json travel to the Node. Archives and expanded files are each limited to 4 MiB. The Node receives no GitHub credential and needs no authenticated gh. The Gateway needs PHP’s ZIP extension. A missing App, unaccepted permission, missing baseline, or invalid artifact fails the named setup step; opt in only after the first default-branch baseline exists. The restore runs as the managed user, as do setup and the task candidate check. It asks the installed Pest for its cache directory without running tests. The directory must be inside that user’s home or this checkout, with no symlink components. It stages files under a cache lock and publishes a new directory atomically. A nonempty cache stays unchanged, so retries and later subtasks keep their own results. Remove an obsolete cache explicitly before requesting a fresh import. Pest remains responsible for rejecting an incompatible runtime fingerprint and recording a new local baseline when needed. This step does not provision a VM or change the CI publication policy. It follows the same setup ordering, time budget, and failure cleanup as other steps. The list’s total timeout must still fit the Project setup limits.

Import CI main caches

Add this step after the install steps of a monorepo whose CI publishes main caches the way Orbit’s ci.yml does:
The Gateway uses its GitHub App with Actions: read for this one repository. It reads the newest 20 successful runs of ci.yml on the Project’s default branch from a push, a schedule, or a manual dispatch. It takes the newest run that has unexpired sandbox-tia-<job>-<commit> artifacts, and downloads them all: one archive per Composer project, each at most 16 MiB and 32 MiB together. The archives travel to the checkout inside the step’s command. The checkout receives no GitHub credential. In the checkout, the step runs bin/tia-cache import-ci for each archive. It checks the archive’s manifest, checksums, project, tested commit, and test configuration. It writes the project’s Pest TIA graph to .orbit-tia/graph.json, and the Pint and PHPStan caches into vendor/, where none exists. Before extraction, the step rejects a ZIP with more than 1,024 entries or more than 64 MiB of total expanded data, as declared by the ZIP. Python’s ZIP reader bounds each extracted file by its declared size and checks its CRC. A rejected archive fails the step before extraction or cache import. The step removes its temporary archives and extracted files when it exits. PHPStan’s result cache is PHP code, which PHPStan executes when it reads the cache. Orbit trusts this code because the artifact comes from a successful default-branch run of the repository’s own ci.yml, using the Gateway’s GitHub App access to that repository. It never imports a pull-request run’s cache. The repository’s default branch, workflow, and CI artifact publishers must therefore be trusted to run code in the checkout. Checksums detect damaged bytes; they do not establish that trust. The matching commit, lock file, tool configuration, and PHP minor version checks guard compatibility. An existing graph stays, so a retry or a later baseline keeps the workspace’s own results, with one exception: when CI tested a descendant of the graph’s main commit, the CI graph replaces it. A workspace from a template then gets the graph of today’s main, not of the template’s day, and its check selects fewer tests. The log names each graph it kept or replaced, and why. The step decides between a loud failure and a cold run like this: A cold run only happens in the minutes after a change to a project’s test configuration lands on main, until CI on main publishes new caches. Each baseline runs the step again, so a workspace from a template that stayed cold imports the graph once the caches fit.

Failure codes

These codes name the step that failed. The sections above say whether the Instance stays.

Why it works this way

These reasons explain the design. Check them before you propose a change.

Named rows, not a script

Each command is its own row, so a failure names one step and you can reorder steps without rewriting a script. One script body and a path in the repository were rejected: the failure report could name no step, and the operator records the command on the Project.

Lists on the Project

The commands run for every Instance of a repository, so they live on the Project. Per-Instance copies were rejected because they would drift from what the operator recorded. The command family is instance: because the commands run for an Instance.

A failed setup removes the new Instance

A half-set-up Instance is not a useful result of instance:create. So a confirmed failure tears it down and removes it, and the next create starts clean. Resuming a create after a failed setup was rejected.

Registration skips setup by default

Registration adopts a checkout that is usually set up already. Running setup on every registration was rejected. --setup runs it on request.

Teardown failure stops removal

Teardown is the operator’s cleanup. Removal continues only after that cleanup succeeds.

Task setup runs in the task check

Create-time setup runs as the managed user, before an agent uses the Instance. A task workspace skips that path. Its baseline runs the setup list inside the task check on the task VM, as the VM’s managed user orbit, which also runs the agents. That user has sudo, so host-dependent setup and tests work. One user for every task agent explains why the VM needs no second user. Task teardown runs as the same user before the Gateway destroys the VM.

Gateway-owned baseline delivery

CI artifacts are repository data. The Gateway already owns the GitHub App, so it downloads and validates the files before Node delivery. Personal tokens on disposable machines were rejected because they duplicate credentials and complicate revocation. A fixed setup identifier keeps the existing ordered step contract and prevents Projects from selecting arbitrary artifact URLs or granting a Node App access. Both instance setup and task baseline setup interpret it before any shell execution.