Guide

Orbit deploys a project from your local Git work tree to a server that you control. It synchronizes the project over SSH, then runs a deployment script from the repository on that server.

The three parts

Orbit uses three terms throughout this manual:

  • The project is the application repository. It owns the scripts that know how to build, start, check, and stop the application.

  • The host is the remote machine. Orbit connects to it through SSH and uploads the project.

  • A slot is one place on that host where the application runs. It contains the uploaded source and separate locations for configuration, durable data, and runtime files.

A hook is one of the application-owned scripts under ops/. The required ops/deploy.sh hook tells the remote machine how this project runs.

The same project can run in more than one slot:

text
weather project
  ops/deploy.sh
        |
        | Orbit uploads the same project
        |
        +--> production slot  + production configuration
        +--> staging slot     + staging configuration
        +--> demo slot        + temporary configuration and expiry

The source and hooks stay the same. Each slot supplies its own environment values and secrets. A slot can be permanent or temporary; the application does not need a separate deployment model for each case.

Orbit owns the upload, slot structure, and hook calls. The project owns the application-specific work.

One deployment

After you create a slot and set its environment, orbit deploy:

  1. Uses Git to select tracked files, local changes, and non-ignored untracked files.

  2. Makes the slot's remote src directory match that selection.

  3. Runs ops/deploy.sh from the remote src directory.

  4. Streams the hook output and returns its exit status.

The project hook can build, start, check, switch, and reload the application. Orbit does not translate those steps into its own configuration format. It calls the hook and returns the result.

Slot lifecycle

Run orbit setup once for a host. Each slot then moves through the same small lifecycle:

  1. orbit create creates its directories and optional expiry timer.

  2. orbit env writes application values outside the synchronized source.

  3. orbit deploy synchronizes source and runs ops/deploy.sh.

  4. orbit status runs ops/status.sh when the project provides it.

  5. orbit remove runs ops/down.sh, then removes source and runtime files.

A normal removal keeps the slot's durable data and environment file. You can create the same explicit slot again and reuse them. orbit remove --purge and automatic expiry delete the complete slot.

The deploy hook also receives an ID for the current deployment, the local Git revision, and paths to every part of the slot. The Hooks page defines each value.

Why a convention

The application knows more about its deployment than a general configuration format does. It knows what to build, which migrations to run, how to start, what health means, and how to stop.

Orbit keeps that knowledge close to the application in repository scripts. It only standardizes the common outer structure: source upload, slots, paths, external configuration, hook names, status, and removal.

This gives different projects the same recognizable shape without forcing their internal deployment steps to match. Moving between projects stays simple: find ops/deploy.sh, inspect the slot configuration, and follow the same lifecycle.

Application support

The Orbit command uses Python, and its remote actions and hooks use Bash. The application can use any language or runtime. For example, ops/deploy.sh can:

  • build and replace a Podman or Docker container

  • compile a Go binary and restart a systemd service

  • install a Python application into a virtual environment

  • copy static files into place

  • run migrations and health checks

The deployment decisions remain in readable scripts beside the application.

Before you start

The local machine needs:

  • Python 3.14 or newer

  • Git

  • OpenSSH

  • rsync

The current host setup supports Ubuntu and needs:

  • Bash

  • OpenSSH

  • rsync

  • Caddy with a system service

  • sudo access during setup

  • a user systemd manager for optional slot expiry

1. Install Orbit

The Python distribution is named stario-orbit. The command is orbit. The unrelated orbit package on PyPI is not this project.

bash
uv tool install stario-orbit
# or: uv add --dev stario-orbit
# or: pip install stario-orbit
orbit --help

For development from a checkout:

bash
uv sync
uv run orbit --help

2. Name the SSH host

Orbit passes the host value to ssh and rsync. An OpenSSH Host alias keeps the user and address out of deployment commands:

text
Host ship
    HostName host.example.com
    User deploy

The rest of this guide uses ship for the host. You can also pass user@hostname, an address, or a hostname directly.

Confirm that the connection works before you continue:

bash
ssh ship

3. Prepare the host

Orbit setup changes the system Caddy service so that it reads /srv/orbit/Caddyfile instead of /etc/caddy/Caddyfile. Existing Caddy sites on that host stop unless you move them into Orbit slots. Start with a dedicated or disposable Ubuntu host.

Run setup once for the host:

bash
orbit setup ship --email you@example.com

Setup creates /srv/orbit, prepares the slot permissions, configures Caddy, enables user lingering for optional expiry timers, and checks that the deploy user can reload Caddy.

Setup uses an interactive SSH session so sudo can prompt. Open a new SSH session after setup so the shell receives the new orbit group membership.

4. Add the deployment hook

At minimum, add an executable deployment hook:

text
your-project/
  ops/
    deploy.sh

Orbit runs the hook from the synchronized project directory. The hook receives paths such as $SLOT_SRC, $SLOT_DATA, $SLOT_ENV, and $SLOT_RUN.

The shape of a deployment hook is:

bash
#!/bin/bash
set -euo pipefail
 
# Build or install from "$SLOT_SRC".
# Start the new process or container.
# Check that it is healthy.
# Publish it or reload the service.

Make the hook idempotent. It must exit with 0 when deployment succeeds and a non-zero status when deployment fails.

If you want a working starting point, follow the Podman recipe. It shows the complete build, start, health check, traffic switch, status, and removal sequence. The Hooks page is the full hook contract. Other runtimes sit under Recipes.

5. Create the slot and deploy

The example uses app.example.com as both the slot name and the public hostname. Create it:

bash
orbit create ship app.example.com

Provide the environment values and secrets for this instance of the application before the first deploy:

bash
orbit env ship app.example.com --from .env.production

Now deploy and check the result:

bash
orbit deploy ship app.example.com
orbit status ship app.example.com

Do not keep application secrets in the synchronized repository. A local .env file that Git does not ignore is uploaded with the source. Use orbit env for slot environment values. The project hook decides how to pass $SLOT_ENV to the application.

Orbit streams the deployment hook output. A successful hook exits with 0. If the project has ops/status.sh, the status command prints the short status that the hook returns.

Later deployments

After a successful create or deploy, Orbit remembers the Git remote and branch to host and slot mapping. Later deployments from the same branch can use:

bash
orbit deploy

Temporary previews

A temporary preview uses the same project and hook as production. Use {} in a slot name to create a readable generated name. Add --ttl to purge the slot automatically:

bash
orbit create ship {}.example.com --ttl 6h
# created ship calm-otter.example.com
orbit deploy ship calm-otter.example.com

The maximum lifetime is 30 days. Expiry uses a transient user systemd timer, which does not survive a host reboot.

Next steps

  • Hooks explains the lifecycle, slot paths, hook variables, Caddy integration, status, and removal.

  • Recipes collects runtime implementations. Start with the Podman example.

  • CLI reference lists every command and target rule.

Theme

Made by Adam with Stario