Skip to main content

Instance cloning

Every production Instance starts as a clone. The Gateway copies a candidate Instance’s committed source, stored environment, and optionally one SQLite database to a new Instance on an app-prod Node. The candidate keeps running. The clone ends with a prepared production home and no release. Its first deployment selects code. Provisioning creates the production PHP runtime through the native provisioner and projects its Route through the shared native Route projector. instance:create on an app-prod Node returns instance.candidate_required. The app-prod role decides this, not APP_ENV.

Prepare the Node

The destination Node needs its own TLD for the preview domain. Set it when you add the Node:

Clone

Name the candidate, the destination Node, the new Instance name, and a preview name:
The CLI calls POST /api/v1/instances/{candidate}/clone. The request accepts no Project, commit, Unix user, or path. The candidate decides the Project. Laravel-package Projects use their declared Project type for source classification; they do not need an artisan application to clone. The caller needs an access grant to both Nodes. The result names the new Instance, its branch, its preview domain, and its selected release, which is empty.

Candidate rules

The candidate is an active development Instance with its checkout, or an active production Instance with a selected release. A development candidate must have its recorded branch checked out, not another branch or a detached HEAD. Its source must have no staged, unstaged, untracked, or submodule change. Its current commit must be in the Project repository. The Gateway checks this on the candidate’s Node using its recorded source identity. Every Git invocation in the candidate inspection, including calls through sudo and checks inside submodules, passes -c core.hooksPath=/dev/null and -c core.fsmonitor=false. Development candidate checks that inspect file contents run as the Node’s managed user without a credential environment. instance.clone_candidate_unavailable carries the inspection’s stderr in details.stderr, at most 1,000 characters, with URL credentials and token-shaped words replaced by [REDACTED]. A checkout’s hooks and custom filesystem monitor do not run during that check. These overrides leave the candidate’s stored Git configuration unchanged; they do not relax the clean-source rules.

Destination checks

The destination must be an active Linux Node with an active app-prod role. The Project can have one production Instance per Node. In an active Cluster, the Cluster needs an active Router. Orbit inspects the destination checkout as its production user from a directory that user can access. A private SSH account home does not prevent source classification. For a nested Laravel app, classification uses composer.json and artisan in the application directory derived from the effective web root; Git identity checks still use the complete repository. Foreign file ownership, unsafe source metadata, and failed directory scans still stop the clone.

What the clone gets

The new Instance gets its own copy of each part below. Before the clone completes, Orbit renders the new Instance’s stored values and writes its .env in the production home. The first deployment links that file from the release’s application directory, including a nested path such as apps/site/.env. It copies no .env file from the candidate, and no cached configuration, dependencies, logs, caches, or PHP-FPM tuning. Stored values such as APP_KEY copy as they are. References such as {{instance.domain}} resolve against the new Instance.

Preview domain

The preview domain is <preview-name>.<node-tld>. For example, shop.com on a Node with TLD prod.orbit gives shop.com.prod.orbit. Orbit never uses the Cluster TLD here. Orbit stores the preview as an explicit private Route, so a later TLD change does not rename it. On a standalone Node the Route has Node scope. In an active Cluster it has Cluster scope, and private DNS points to the Router. Cloning publishes no public listener. Replace the preview with the production domain through a separate Route change. A missing Node TLD returns route.tld_required. A domain that another Route owns returns route.domain_conflict.

SQLite seed

sqlite_source_path must be inside the candidate’s checkout or selected release, and it must be a readable SQLite database. Orbit takes a consistent snapshot while the candidate keeps writing, checks its integrity, and installs it as <home>/database.sqlite, owned by the new Instance’s user. The Gateway checks the path and disk space on both Nodes before it replaces anything. Without the option, the clone gets no database. Cloning seeds a different production Instance at the fixed <home>/database.sqlite path. Instance transfer instead keeps the Instance ID between development Nodes and installs the snapshot at the selected file’s relative path in the destination checkout. Each transfer attempt has its own snapshot record. Transfer rollback abandons that seed and removes its owned temporary files before closure. An identical retry after restoration takes a fresh snapshot rather than reusing data captured before the source restarted. These transfer rules do not change the clone’s destination or its identical-request retry. The snapshot holds whatever the candidate committed at that moment, queued jobs included. Remove copied queue rows or other data on the new Instance before you start its workers. Other databases and external storage need their own preparation.

Candidate stays live

Cloning never stops the candidate’s Processes or Schedules, pauses queues, or changes its source, environment, or database.

Retry

The Gateway records the clone request and each finished step. An identical request resumes an interrupted clone, even when the candidate has moved to a newer commit. A request that changes the candidate, Node, name, preview, branch, or SQLite path returns instance.clone_retry_conflict. After completion, an identical request returns the same Instance and changes nothing. instance:create does not create or resume a production Instance. While a clone is incomplete, removal of its candidate returns instance.clone_in_progress.

Errors

The Gateway returns these codes for a clone. A failure after reservation records its code on the new Instance, and the identical request resumes it.

Next steps

  1. Update environment values that must differ from the candidate, then synchronize to rewrite .env.
  2. Clean copied application data, such as queue rows.
  3. Record deploy steps.
  4. Run instance:deploy to create and select the first release.
  5. Start the copied Processes and Schedules.

Why it works this way

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

Clone from a candidate

A repository does not hold an Instance’s environment or data. A candidate does. So Orbit creates production from an existing Instance. Direct production creation from a repository was rejected. Any eligible development or production Instance can be the candidate.

Rebuild source from the repository

Copying the candidate directory would carry dependencies, logs, caches, and Node-specific configuration. So the clone checks out committed source from the repository, and a dirty candidate is refused.

Keep the candidate running

Stopping workers or clearing queues on the candidate would disturb a live application. So Orbit takes a live SQLite snapshot and leaves target cleanup to you.

No automatic deployment

You need to check the environment and deploy steps before the first release. So cloning and deployment are separate requests.