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:
- Selects the tracked and non-ignored files in your local Git work tree.
- Mirrors those files over SSH into a named slot on a host you control.
- Runs the
ops/deploy.shscript 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
sudoaccess 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/orbitand the system grouporbit - Adds the deploy user and the
caddyuser 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
Built Distribution
Filter files by name, interpreter, ABI, and platform.
If you're not sure about the file name format, learn more about wheel file names.
Copy a direct link to the current filters
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
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
f81fa995de609e2d40031dcc3497ffc6e588a1880d62a259c71aa23c907fecaf
|
|
| MD5 |
554e16af20ee2c719f06f87e981e6bf2
|
|
| BLAKE2b-256 |
99466daecc1afc2657c1e46a6f1dbfbd6f302688a1730ce26aadab4d8c50cf6a
|
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
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
cb09585423a60b59efd3633d951b206ca04d93b8233e94ea919bcf6fa0c4818a
|
|
| MD5 |
07742f9f57c6c1beb0c13f046b6a2332
|
|
| BLAKE2b-256 |
fce8ad69e4e855f4715c2e8cd57e0e391c2a296e81fd64045a074b3155e1baf9
|