An SSH deployment orchestrator that uploads local assets and runs shell scripts across multiple remote servers in parallel, with a full-featured terminal UI (or plain log output for CI/CD environments).
┌─[web] ───────────────────────────────────────────────────────────────────────┐
│ Checking connection... │
│ Connection OK │
│ Creating remote base directory... │
│ Uploading scripts... │
│ Running: 01-install.sh │
│ [...] │
│ COMPLETED SUCCESSFULLY │
└──────────────────────────────────────────────────────────────────────────────┘
Switch: [TAB] | Scroll: [SHIFT]+[↑/↓] [PGUP/PGDN] | Quit: [CTRL+C] | ...
When you need to provision a machine, deploy an app, or apply quick updates across a handful of servers, heavyweight configuration management tools often feel like overkill. s.Orchestrator gets out of your way. There is no complex syntax to learn - just write standard bash scripts, point them at a JSON config, and watch everything execute concurrently.
Compared to Ansible:
| s.Orchestrator | Ansible | |
|---|---|---|
| Learning curve | Low - just bash + JSON | Medium - YAML, modules, playbooks |
| Target requirements | SSH only | SSH + Python on target |
| Idempotency | Manual (write your scripts defensively) | Built-in via modules |
| Parallel execution | Always | Configurable (forks) |
| Module ecosystem | None | Large (ansible-galaxy) |
| Live feedback | TUI with per-server log panes | stdout only |
| Config management at scale | Not suitable | Purpose-built |
Use s.Orchestrator when you manage a small cluster of servers, you already know the exact shell commands you want to run, and you want zero tooling overhead.
Use Ansible when you require built-in idempotency, complex configuration templating (Jinja2), or are managing large enterprise infrastructure.
- Parallel deployment - all servers deploy simultaneously; per-server log panes in the TUI
- Host key enforcement - on first contact, fetches all key types offered by the server and prompts you to save them; on subsequent runs, verifies each stored key type against
known_hosts; all mismatches are reported together in a single run - TUI with tab switching - navigate between server log panes, scroll history, forward keystrokes to interactive SSH sessions
- Plain mode (
--ugly) - prefix-based log output suitable for CI/CD pipelines and logging - Dry-run mode - validates config and tests connections without executing any remote commands
- JSON schema export - emit a JSON Schema file for the config so editors can validate it
- Injected environment variables - every script receives a standard set of server variables automatically; additional per-server variables can be added via
envin the config - Optional key file - specify a per-server SSH private key; falls back to the SSH agent/defaults
- Per-server log files - each server's output is saved to
logs/<name>.log - Single-command exec (
--exec) - run one ad-hoc command on all servers instead of the scripts directory (assets and scripts are still uploaded) - Server filter (
--servers) - target a subset of servers by name; all others are skipped
# Install with npm
npm i -g @scolastico-dev/orchestrator
# Then run it
s-orchestrator [options]
# Install with npm
npm i @scolastico-dev/orchestrator
Then add a script to package.json:
{
"scripts": {
"deploy": "s-orchestrator [options]"
}
}
And run it with:
npm run deploy
# Clone and build
git clone https://github.com/scolastico-dev/orchestrator.git
cd orchestrator
pnpm install
pnpm build
# Then run it
node dist/index.js [options]{
"web": {
"ip": "203.0.113.10",
"user": "deploy",
"env": {
"APP_ENV": "production"
}
},
"db": {
"ip": "203.0.113.20",
"port": 2222,
"keyFile": "~/.ssh/db_deploy"
}
}It's important to prefix your scripts with numbers to ensure they run in the correct order, as they are executed in lexicographic order.
scripts/
10-update.sh
20-restart.sh
Ensure all scripts are written in a "upsert" style, meaning they can be run multiple times without causing issues. s.Orchestrator does not enforce idempotency, so your scripts should be designed to handle repeated executions gracefully.
s-orchestratorOn first run, s.Orchestrator will fetch all SSH host key types for each server and ask you to confirm before saving them to config.json. Subsequent runs verify every stored key type and abort if any of them changes (TOFU - Trust On First Use).
See example/ for a complete working example that provisions two Debian servers: apt upgrade, SSH key setup, and Docker installation.
The config file is a JSON object whose keys are server names and whose values are server configs:
| Field | Type | Default | Description |
|---|---|---|---|
ip |
string (required) |
- | IP address or hostname |
port |
number |
22 |
SSH port |
user |
string |
"root" |
SSH username |
hostKeys |
string[] |
- | Stored host public keys for all key types (auto-populated on first run) |
keyFile |
string |
- | Path to SSH private key file |
knownHostsFile |
string |
- | Path to a custom known_hosts file (useful in CI/CD) |
env |
Record<string,string> |
- | Extra environment variables injected into each script (merged on top of the default injected vars) |
importedEnv |
string[] |
[] |
Names of local shell environment variables to read at runtime and forward to each remote script |
s-orchestrator --schema # print schema to stdout
s-orchestrator --schema config.schema.json # write to fileAdd to config.json for editor validation:
{
"$schema": "./config.schema.json",
"web": { "ip": "..." }
}Every script execution receives the following environment variables automatically:
| Variable | Value |
|---|---|
SERVER_NAME |
The server's key in config.json |
SERVER_IP |
The server's ip field |
SERVER_USER |
The SSH username (user, default root) |
SERVER_SSH_PORT |
The SSH port (port, default 22) |
ASSET_DIR |
Absolute remote path where assets were uploaded |
SCRIPT_DIR |
Absolute remote path where scripts were uploaded |
Variables defined in the server's env block are merged on top, so they can override any of the above if needed.
importedEnv lets you forward specific variables from the shell that runs s.Orchestrator to each remote script — without hard-coding their values in config.json. Variables not present in the local environment are silently skipped. The import happens before the env block is applied, so env entries take precedence over imported values.
{
"web": {
"ip": "203.0.113.10",
"importedEnv": ["DB_PASSWORD", "API_KEY"]
}
}This is especially useful with dotenvx, which can decrypt an encrypted .env file and inject the secrets into the local shell before s.Orchestrator runs:
# Decrypt .env.vault, inject secrets locally, then forward listed ones to remote servers
dotenvx run --quiet -- sh -c 's-orchestrator'The lite version works the same way:
dotenvx run --quiet -- sh -c 'bash lite-version.sh'dotenvx makes DB_PASSWORD and API_KEY available in the local environment; importedEnv picks them up and forwards only those two to each remote script. Nothing else from the local environment is exposed.
Usage: s-orchestrator [options]
Options:
-v, --version print version and exit
-c, --config <path> path to config JSON file (default: "config.json")
-y, --skip-confirm skip confirmation after connection tests
-n, --dry-run test connections, do not deploy
--schema [path] export JSON schema for config
--ugly plain log output, no TUI
--log-dir <path> directory for per-server log files (default: "logs")
--assets-dir <path> local assets directory to upload (default: "assets")
--scripts-dir <path> local scripts directory (default: "scripts")
--remote-path <path> remote working directory (default: "/tmp/s-orchestrator")
--exec <command> run one command on each server instead of scripts dir
(assets and scripts are still uploaded)
--servers <names> comma-separated server names to target; others are skipped
-h, --help display help
# Production deploy with a dedicated config
s-orchestrator -c prod.json
# Skip confirmation prompt (for automated pipelines)
s-orchestrator -y
# Dry run - verify config and connections without deploying
s-orchestrator --dry-run --ugly
# CI/CD friendly - ugly mode, skip confirm, custom config
s-orchestrator --ugly -y -c /etc/deploy/config.json
# Export the JSON schema
s-orchestrator --schema > config.schema.json
# Run a single ad-hoc command across all servers
s-orchestrator --exec "systemctl restart nginx" -y
# Deploy only to specific servers
s-orchestrator --servers web,db-primary
# Combine: one command, specific servers, no prompt
s-orchestrator --exec "docker pull myapp:latest" --servers web1,web2 -yFor each run, s.Orchestrator:
- Validates the config file
- Enforces host keys - fetches all key types on first use (with confirmation), verifies each type on subsequent runs; all mismatches are collected and reported together; any newly confirmed keys are saved to the config even if mismatches are found
- Tests connections to all servers in parallel - aborts immediately if any connection fails
- Prompts to start (unless
--skip-confirm) - Deploys to all servers in parallel:
- Creates
<remote-path>/on the remote - Uploads
assets/(if present) - Uploads
scripts/(if present, even when--execis used) - Executes each
.shscript in lexicographic order or the single--execcommand, with injected environment variables - Cleans up
<remote-path>/on the remote
- Creates
- Displays final status - Ctrl+C after this point exits cleanly without an abort prompt
your-project/
├── config.json # server config (managed by s.Orchestrator)
├── assets/ # files uploaded verbatim (optional)
│ └── nginx.conf
└── scripts/ # executed in alphabetical order on each server
├── 01-update.sh
└── 02-restart.sh
For environments where Node.js is unavailable, a self-contained bash implementation is provided in lite-version.sh. It shares the same config format and feature set but deploys servers sequentially and has no TUI.
Requirements: bash, curl, ssh, scp, ssh-keyscan, ssh-keygen, jq
# Run directly (no build or Node.js required)
curl -sSL https://raw.githubusercontent.com/scolastico-dev/orchestrator/main/lite-version.sh | bash -s -- [options]The lite version supports all the same options as the full version except --ugly and --schema. It also self-updates: on each run, if it is installed as a file on disk and curl/md5sum are available, it checks the latest version from GitHub, compares checksums, and prompts you to update in place.
Options:
-c, --config <path> path to config JSON file (default: config.json)
-y, --skip-confirm skip confirmation after connection tests
-n, --dry-run test connections, do not deploy
--log-dir <path> directory for per-server log files (default: logs)
--assets-dir <path> local assets directory to upload (default: assets)
--scripts-dir <path> local scripts directory (default: scripts)
--remote-path <path> remote working directory (default: /tmp/s-orchestrator)
--exec <command> run one command instead of the scripts dir
--servers <names> comma-separated list of server names to target
| Feature | Full version | Lite version |
|---|---|---|
| Parallel deployment | yes | no - sequential |
| TUI / plain mode | yes | plain only |
| Host key enforcement (TOFU) | yes | yes |
| Env injection | yes | yes |
| Dry-run mode | yes | yes |
| Per-server log files | yes | yes |
keyFile / knownHostsFile |
yes | yes |
--exec / --servers |
yes | yes |
| Self-update check | no | yes |
| JSON schema export | yes | no |
| Node.js required | yes | no |
pnpm install
# Type check
pnpm typecheck
# Run tests (unit tests run always; Docker integration tests require Docker)
pnpm test
pnpm test:watch
# Build (Parcel compiles TypeScript → dist/index.js)
pnpm build
# Run from source (no build needed)
pnpm dev -- --helpsrc/
├── index.ts entry point, main()
├── cli.ts CLI argument parsing (commander)
├── types.ts shared TypeScript types
├── config.ts Zod schema, load / save / export-schema
├── logger.ts per-server file logger
├── ssh/
│ ├── executor.ts execRemoteStreaming, execRemoteSimple, scpUpload
│ ├── host-keys.ts TOFU host key enforcement
│ ├── connection.ts connection health check
│ └── index.ts barrel export
├── deploy/
│ ├── steps.ts single-server deploy steps (env injection, script execution)
│ └── index.ts multi-server orchestration
└── ui/
├── types.ts OrchestratorUI interface
├── tui.ts blessed TUI implementation
└── plain.ts plain stdout implementation
tests/
├── fixtures/sshd/ Alpine+OpenSSH Docker image for integration tests
├── helpers/docker.ts Docker container lifecycle management
├── config.test.ts config validation unit tests
├── ssh.test.ts SSH executor integration tests (requires Docker)
├── deploy.test.ts deployment integration tests (requires Docker)
└── lite-version.test.ts lite-version.sh integration tests (requires Docker)
# All tests (unit + Docker integration)
pnpm test
# Unit tests only (no Docker required)
pnpm test tests/config.test.tsIntegration tests spin up Alpine+OpenSSH Docker containers automatically, run the deployment against them, and clean up on completion. They are skipped gracefully when Docker is unavailable.
- Host key verification uses SSH's
StrictHostKeyChecking=yes- connections to unknown hosts are rejected - TOFU (Trust On First Use) - the first time a server is seen, the user must explicitly approve all offered key types; they are then stored as an array in
config.json - Key mismatch = hard abort - if any stored key type doesn't match
~/.ssh/known_hosts, s.Orchestrator exits with an error listing all conflicting entries and thessh-keygen -Rcommands needed to resolve them; any newly confirmed keys are saved before aborting so they don't need to be re-confirmed after the conflict is resolved - Scripts run with the permissions of the configured SSH user - use a least-privilege deploy user where possible
This project is licensed under the MIT License.
MIT
A short and simple permissive license with conditions only requiring preservation of copyright and license notices. Licensed works, modifications, and larger works may be distributed under different terms and without source code.
| Permissions | Conditions | Limitations |
|---|---|---|
🟢 Commercial useThe licensed material and derivatives may be used for commercial purposes. |
🔵 License and copyright noticeA copy of the license and copyright notice must be included with the licensed material. |
🔴 LiabilityThis license includes a limitation of liability. |
🟢 DistributionThe licensed material may be distributed. |
🔴 WarrantyThis license explicitly states that it does NOT provide any warranty. |
|
🟢 ModificationThe licensed material may be modified. |
||
🟢 Private useThe licensed material may be used and modified in private. |
Information provided by https://choosealicense.com/licenses/mit/
This information is provided for general understanding and is not legal advice.