Skip to main content

Orbit

Orbit

Orbit is a small remote deployment runner. It adds a repeatable structure to the familiar rsync plus remote shell script approach.

Guides and reference: stario.dev/orbit

When you run orbit deploy, Orbit:

  1. Selects the tracked and non-ignored files in your local Git work tree.
  2. Mirrors those files over SSH into a named slot on a host you control.
  3. Runs the ops/deploy.sh script from your repository on that host.

That is the deployment contract. Orbit does not decide how to build, run, or reload your application. Your script contains those steps.

The Orbit CLI is written in Python, but the application does not have to be. The remote contract uses Bash. You can deploy a Python service, a Go binary, a static site, or another application. You can run it with Podman, Docker, systemd, or your own process manager.

Orbit provides the convention around those steps:

  • named deployment slots on one or more SSH hosts
  • an exact synchronized source tree for each slot
  • separate paths for source, durable data, environment values, and runtime files
  • repository-owned deploy, stop, and status hooks
  • environment management, status checks, removal, and optional preview expiry

You keep the deployment decisions in short scripts beside the application. There is no remote Orbit daemon, hidden build pipeline, telemetry service, or MCP service to understand.

A slot is one named deployment target under /srv/orbit/slots/. It keeps the synchronized source, environment values, durable data, and runtime files in separate paths.

orbit setup ship --email you@example.com
orbit create ship app.example.com
orbit env ship app.example.com --from .env.production
orbit deploy ship app.example.com
orbit status ship

ship is the SSH destination. In these examples it is a Host alias from ~/.ssh/config. Orbit passes that value to ssh and rsync. You can also pass user@hostname or a hostname. See Commands for the shapes.

After a successful create or deploy, Orbit stores the git remote + branch → host + slot mapping in ~/.config/orbit/config.json (or $XDG_CONFIG_HOME). Later you can run orbit deploy with no host/slot and Orbit uses that mapping.

Requirements

Local:

  • Python 3.14 or newer
  • Git
  • OpenSSH
  • rsync

Ubuntu host:

  • bash
  • OpenSSH
  • rsync
  • a user systemd manager (for optional TTL expiry)
  • Caddy with a system service
  • sudo access during setup

Install

The PyPI name is stario-orbit. The command and the Python import stay orbit. pip install orbit installs a different, unrelated package.

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

From a checkout:

uv sync
uv run orbit --help

Host setup

orbit setup opens an SSH session and runs a script that uses sudo. Use a dedicated Ubuntu host, or a disposable one. Do not run setup on a machine that already serves sites you need to keep.

The current host setup is opinionated about the slot layout and Caddy integration. It is not opinionated about the application runtime. Deployment hooks can use the prepared Caddy paths, or ignore them and manage the application with another runtime or service configuration.

Setup does this:

  • Creates /srv/orbit and the system group orbit
  • Adds the deploy user and the caddy user to that group
  • Writes /srv/orbit/Caddyfile (ACME email + slot imports)
  • Installs a systemd drop-in so the system Caddy service loads only that file — not /etc/caddy/Caddyfile
  • Restarts Caddy
  • Enables user lingering for TTL expiry
  • Checks that the deploy user can reload Caddy

Existing Caddy sites on that host stop unless you move them into Orbit slots. Re-running setup repairs permissions from older Orbit versions and writes the drop-in again.

Pass --email, or setup prompts for it. Setup uses a TTY so sudo can prompt. Open a new SSH session after setup so your shell sees group orbit. Later SSH and rsync calls use BatchMode=yes.

Anyone who can SSH as the deploy user can read slot environment files and run hooks. See SECURITY.md.

Commands

orbit setup <host> [--email you@example.com]
orbit create [<host> <slot>] [--ttl 6h]
orbit create <host> {}.example.com [--ttl 6h]
orbit deploy [<host> <slot>] [--dry-run]
orbit status [<host> [<slot>]]
orbit env [<host> <slot>] [--set KEY=VALUE | --from FILE | --edit]
orbit remove [<host> <slot>] [--purge] [--yes]

Provide both host and slot, or neither. Neither means: read the mapping for this repository's origin remote and current branch from your user config. A single argument is only valid for status <host> (list every slot on that host).

<host> is the SSH destination. Orbit does not parse it further. Typical shapes:

ship                      Host alias in ~/.ssh/config (recommended)
deploy@host.example.com   user and hostname
deploy@203.0.113.10       user and address
host.example.com          hostname (OpenSSH default user)

A Host alias keeps the real address and user in SSH config:

Host ship
    HostName host.example.com
    User deploy

The value must not contain / or begin with -. The remote /srv/orbit tree owns the slots. Host aliases are personal, so the mapping lives in your user config — not in the repository.

create accepts one {} in the slot name. Orbit replaces it with a short readable token such as calm-otter. Generated names treat any remnant slot tree as taken, so a non-purge remove does not hand durable data or env to the next preview. An explicit create of the same named slot may reuse retained data and env when src is gone.

--ttl schedules an automatic purge on the host after a duration such as 30m, 6h, or 2d (maximum 30d). Manual remove cancels that timer. Transient TTL timers do not survive a host reboot. Orbit exports SLOT_TTL to hooks; preview basic auth is optional and belongs in ops/deploy.sh (see examples/podman-slot).

First deployment

orbit deploy uploads every Git-selected file: tracked files, local modifications, and untracked files that Git does not ignore. A local .env that is not in .gitignore goes into the slot src/ directory. Put application secrets in the slot environment with orbit env, not in the repository tree.

orbit setup ship --email you@example.com
orbit create ship app.example.com
orbit env ship app.example.com --from .env.production
orbit deploy ship app.example.com
orbit status ship
# later, on the same branch:
orbit deploy

Preview with a generated name and automatic cleanup:

orbit create --ttl 6h ship {}.example.com
# created ship calm-otter.example.com
# expires after 6h
orbit deploy ship calm-otter.example.com
# example deploy.sh may print preview basic auth on first deploy

User config

Path: ~/.config/orbit/config.json (or $XDG_CONFIG_HOME/orbit/config.json).

Orbit keys entries by a normalized git remote URL and branch name:

{
  "version": 1,
  "remotes": {
    "github.com/acme/app": {
      "branches": {
        "main": {
          "host": "ship",
          "slot": "app.example.com"
        },
        "preview": {
          "host": "ship",
          "slot": "calm-otter.example.com",
          "expires_at": 1773500000
        }
      }
    }
  }
}

Successful create and deploy create or update that entry and print a short note. TTL creates also store expires_at; Orbit removes those entries once the time passes (on the next config read). Detached HEAD has no branch mapping — pass host and slot explicitly.

deploy needs a resolved host and slot. It works with a detached HEAD when you pass them explicitly. It still requires a Git work tree because Orbit gets the upload list from Git. Orbit always copies the complete selected Git work tree into the slot src/ directory, including tracked modifications and non-ignored untracked files.

On a TTY, Orbit prints local status in cyan and streams remote hook output as dim lines. When deploy finishes, it reports success or failure with the elapsed time.

Host layout

/srv/orbit/
  Caddyfile
  slots/                 group orbit, mode 2750
  slots/<slot>/
    src/      synchronized repository, deploy user only
    data/     durable application data, deploy user only
    env       application environment, mode 0600
    run/      group orbit, sockets and generated Caddy configuration

Group orbit is shared by the deploy user and Caddy only for path traversal and the run/ tree. Caddy cannot read or change synchronized source, durable data, or the environment file. Default ACLs on run/ keep new listen sockets reachable by Caddy.

The main Caddyfile is only:

{
	email you@example.com
}
import /srv/orbit/slots/*/run/Caddyfile

Environment changes use an atomic same-directory replacement and set mode 0600. Removing a slot keeps its data and environment. remove --purge deletes the whole slot tree.

Synchronization

Orbit finds the Git root and selects tracked files and non-ignored untracked files with git ls-files. It builds an exact local staging tree and mirrors it into $SLOT_SRC with rsync. Files that are absent from the local Git selection are deleted from the remote source tree.

The rsync operation uses delayed updates, but it is not a transactional release swap. A failed or interrupted transfer can leave a mixed source tree. Do not start concurrent deploy or remove operations for one slot.

Persistent data, secrets, and runtime files are outside the synchronized tree.

Repository hooks

Hooks run from $SLOT_SRC. They are non-interactive and receive no arguments.

ops/deploy.sh     required and executable
ops/down.sh       optional
ops/status.sh     optional

A ready-to-adapt Podman blue/green example lives in examples/podman-slot.

Orbit does not inspect these scripts or infer deployment steps from the application language. Write the commands that your application needs. A deploy.sh can, for example:

  • build and replace a Podman or Docker container
  • compile a Go binary and restart a systemd service
  • create a Python virtual environment, install dependencies, and reload a process
  • copy static files into place
  • run database migrations and health checks

The script must exit with 0 when deployment succeeds and with a non-zero status when it fails. Make it idempotent so the same source and environment can be deployed again safely.

Orbit supplies:

ORBIT_ROOT
ORBIT_CADDY
ORBIT_PURGE

SLOT_NAME
SLOT_ROOT
SLOT_SRC
SLOT_DATA
SLOT_ENV
SLOT_RUN
SLOT_TTL
SLOT_DEPLOY_ID
SLOT_GIT_REVISION

SLOT_NAME is the concrete slot address after {} expansion. SLOT_TTL is the configured lifetime in seconds, or 0 when the slot has no expiry. SLOT_DEPLOY_ID uniquely identifies this deploy attempt. SLOT_GIT_REVISION is the local HEAD commit and is informational when the work tree is dirty.

ORBIT_PURGE is set only for down.sh during remove. It is 1 when Orbit will delete durable data and env (remove --purge or TTL expiry). It is 0 when Orbit keeps those paths and only deletes src and run. Use it when the hook must decide whether to wipe application-owned durable state before Orbit deletes the files.

Orbit streams the output from deploy.sh and returns its exit status. For an HTTP application that uses the prepared Caddy service, the hook can write $SLOT_RUN/Caddyfile, switch a stable socket, and reload Caddy. These are deployment-script choices, not requirements of the runner.

down.sh stops the application and removes its runtime files. A failure stops slot removal. When ops/down.sh is missing, Orbit still deletes src and run (and the whole tree on purge), but prints warnings that containers, systemd units, or Caddy routes may remain active.

status.sh prints one short status line. Exit 0 means healthy. Exit 1 means stopped or unhealthy. Exit 2 or greater means that the check failed. A missing script reports unknown.

Short actions such as create, status, list, environment updates, deploy, and remove run while SSH remains connected. TTL expiry uses a transient user systemd timer. Multiple slots can have independent timers.

What Orbit is not

Orbit is not a Kubernetes platform, a PaaS, a CI service, or a daemon on the host. It does not detect the application stack, generate a deployment plan, observe production, or expose MCP.

The contract is the synchronized slot, the supplied environment variables, and your shell hooks. A Unix socket and Caddy are one supported pattern, not the application contract. The Podman example uses that pattern and defaults to Stario, but Orbit itself does not require Stario or Python applications.

Disposable Ubuntu integration test

The integration test changes Caddy service configuration and creates /srv/orbit. Use only a disposable Ubuntu host where this is acceptable. The test creates a unique disposable slot and removes it with --purge.

The test has no default host. Both environment variables are required:

ORBIT_INTEGRATION_SSH=user@disposable-host \
ORBIT_INTEGRATION_DISPOSABLE=YES \
uv run pytest tests/test_integration_ubuntu.py -v -s

The SSH user must have interactive sudo access for setup. Normal test runs skip this test.

Releases

Version history lives in CHANGELOG.md.

Contributing

From this directory:

uv sync
uv run ruff check .
uv run ruff format --check .
uv run pyright
uv run pytest

Before committing:

uv run ruff check . --fix
uv run ruff format .

Download files

Download the file for your platform. If you're not sure which to choose, learn more about installing packages.

Source Distribution

stario_orbit-0.1.0.tar.gz (22.9 kB view details)

Uploaded Source

Built Distribution

If you're not sure about the file name format, learn more about wheel file names.

stario_orbit-0.1.0-py3-none-any.whl (29.4 kB view details)

Uploaded Python 3

File details

Details for the file stario_orbit-0.1.0.tar.gz.

File metadata

  • Download URL: stario_orbit-0.1.0.tar.gz
  • Upload date:
  • Size: 22.9 kB
  • Tags: Source
  • Uploaded using Trusted Publishing? No
  • Uploaded via: uv/0.9.24 {"installer":{"name":"uv","version":"0.9.24","subcommand":["publish"]},"python":null,"implementation":{"name":null,"version":null},"distro":{"name":"macOS","version":null,"id":null,"libc":null},"system":{"name":null,"release":null},"cpu":null,"openssl_version":null,"setuptools_version":null,"rustc_version":null,"ci":null}

File hashes

Hashes for stario_orbit-0.1.0.tar.gz
Algorithm Hash digest
SHA256 f81fa995de609e2d40031dcc3497ffc6e588a1880d62a259c71aa23c907fecaf
MD5 554e16af20ee2c719f06f87e981e6bf2
BLAKE2b-256 99466daecc1afc2657c1e46a6f1dbfbd6f302688a1730ce26aadab4d8c50cf6a

See more details on using hashes here.

File details

Details for the file stario_orbit-0.1.0-py3-none-any.whl.

File metadata

  • Download URL: stario_orbit-0.1.0-py3-none-any.whl
  • Upload date:
  • Size: 29.4 kB
  • Tags: Python 3
  • Uploaded using Trusted Publishing? No
  • Uploaded via: uv/0.9.24 {"installer":{"name":"uv","version":"0.9.24","subcommand":["publish"]},"python":null,"implementation":{"name":null,"version":null},"distro":{"name":"macOS","version":null,"id":null,"libc":null},"system":{"name":null,"release":null},"cpu":null,"openssl_version":null,"setuptools_version":null,"rustc_version":null,"ci":null}

File hashes

Hashes for stario_orbit-0.1.0-py3-none-any.whl
Algorithm Hash digest
SHA256 cb09585423a60b59efd3633d951b206ca04d93b8233e94ea919bcf6fa0c4818a
MD5 07742f9f57c6c1beb0c13f046b6a2332
BLAKE2b-256 fce8ad69e4e855f4715c2e8cd57e0e391c2a296e81fd64045a074b3155e1baf9

See more details on using hashes here.

Release history Release notifications | RSS feed

This release

0.1.0 This release

2 files

Supported by

AWS Cloud computing and Security Sponsor Datadog Monitoring Depot Continuous Integration Fastly CDN Google Download Analytics Sentry Error logging StatusPage Status page