@suble/cli
v0.3.0
Published
Suble CLI — deploy and manage cloud infrastructure from your terminal.
Downloads
266
Maintainers
Readme
suble
The Suble CLI — deploy and manage cloud infrastructure from your terminal.
# run without installing
npx @suble/cli deploy
# or install once, then use the short command
npm i -g @suble/cli
suble deploydeploy walks you through it interactively (sign in via your browser — OAuth
2.1, the same login as the dashboard and MCP server), then writes a
suble.yaml describing what you launched. After that, suble update (in CI)
reconciles that file with no prompts — rebuild, redeploy, re-assert everything.
suble.yaml
One file per project folder describes a single instance. The source key you
set picks the type — there's no kind: field:
| key | what it deploys |
|---|---|
| build: | a Docker container built from your local Dockerfile, shipped over SSH |
| image: | a published Docker image |
| app: | a managed 1-click app or database (postgresql, mysql, …) |
| os: | a plain OS image |
| template: | an instance from a saved template |
# a built container, exposed at a domain with automatic SSL
name: web-01
plan: BXS.s1
build:
dockerfile: Dockerfile
tag: web:latest
container: web
ports:
- "80:8080"
env: # ${VAR} = local/CI env · %{project.NAME}% = a shared Suble variable
NODE_ENV: production
volumes: # persistent named volumes (survive container recreate)
- "data:/var/lib/app"
expose:
domain: app.example.com
port: 80
loadbalancer: # optional: front the app with a managed load balancer
size: lb.xs # lb.xs | lb.s | lb.m | lb.l
ha: false # true = HA pair with a floating IP
# protocol: https # https (default, auto-TLS on expose.domain) | http | tcp
firewall: smart # off | smart | strict
backup: basic # none | basic | extendedloadbalancer: requires expose: (the listener + backend target are derived
from it). On suble deploy the CLI creates a managed LB named <name>-lb if it
doesn't exist, then reconciles its listener + this instance as a target —
idempotent, so redeploys never duplicate. loadbalancer: true uses all defaults.
It's a managed entrypoint (stable IP, auto-TLS, health checks) in front of the
in-instance ingress; removing the block does not delete the LB.
Block or inline flow style both parse, so ports: ["80:80"] and
firewall: { mode: strict, allow: ["10.0.0.0/8"] } are fine too.
# a managed Postgres, locked to a private network
name: db-main
plan: CRA.m1
app: postgresql
password: ${ROOT_PASSWORD} # OS root login — env-interpolated, never a literal
databases:
- app_production # created on first deploy (db.create_database)
users:
- name: app_user
password: ${APP_DB_PASSWORD}
database: app_production
networks:
- my-private-net # attach by name or uid
firewall:
mode: strict
allow: # source CIDRs (required for strict)
- 10.20.0.0/16# a published image with direct port publishing
name: edge
plan: BXS.s1
image: nginx:1.27
ports: ["80:80", "443:443"]Firewall is off / smart / strict, or a block. With off you can list
explicit rules; with strict you must give an allow: list of source CIDRs so a
headless run can't lock you out:
firewall:
mode: off
rules:
- direction: in
action: ACCEPT
proto: tcp
dport: "443"
- direction: in
action: ACCEPT
proto: tcp
dport: "22"
source: 203.0.113.0/24
comment: ssh from officeWith smart (or off) SSH stays reachable, so suble instance ssh just works.
Under strict, SSH is locked to the allow: list plus a guard IP — so
suble instance ssh checks your current IP and, if it isn't covered, offers to
arm SSH access for it before connecting (it never widens the DB-port allowlist).
Secrets
${VAR} is expanded from the environment when the file is read — never commit
literal passwords (the CLI warns if you do). A database's root credentials are
generated server-side; fetch them anytime with suble app info. Secret values are
masked unless you're on an interactive terminal, or pass --show-credentials.
Shared variables. For a secret several services share, reference a project- or
cluster-scoped Suble variable inside any env value: API_KEY: "%{project.stripeKey}%".
Unlike ${VAR} (your local/CI env, expanded here), %{project.NAME}% and
%{cluster.NAME}% are resolved server-side at deploy — the value lives in Suble,
never in your suble.yml, and rotating it updates every consumer on the next deploy.
Manage them in the dashboard under Project → Settings → Shared variables, or on a
cluster's page.
How reconcile works
deploy (interactive, first run writes the file) and update (CI, no prompts)
share one reconciler:
- Find or create the instance by
name— never recreated once it exists. - Containers are rebuilt/re-pulled and recreated to deploy the latest version
(named volumes persist; keep stateful data in a managed
appdatabase). - Firewall, expose (ingress + SSL), and private networks are re-asserted every run — networks are attach-if-missing and never auto-detached.
- Databases/users are provisioned on first create.
- Plan changes are warned about, not auto-resized.
Commands
# Deploy lifecycle (driven by ./suble.yaml)
suble init Scaffold a commented suble.yaml (no deploy)
suble deploy Reconcile ./suble.yaml (first run: interactive, writes it)
suble update Reconcile ./suble.yaml non-interactively (CI; needs SUBLE_API_KEY)
suble plan Preview what deploy/update would change (also: update --dry-run)
suble destroy [name] Delete the instance (from suble.yaml or named); --yes to skip confirm
# Instance ops
suble status <uid|name> State, job progress, IPs, firewall, networks, app health
suble logs <uid|name> [container] Container logs (--tail N, --follow)
suble app info <uid|name> App/db connection details (--show-credentials)
suble resize <uid|name> <plan> Change plan (reboots; --yes to skip confirm)
suble backup ls|create|restore <uid|name> [backup-uid]
suble db create <uid|name> <db> Create a database (managed DB apps)
suble db user-add <uid|name> <user> Create a DB user (--password … --database …)
# Networks
suble network ls | create <name> | show <ref> | rm <ref>
suble network connect <inst> <net> [--ip <ipv4>]
suble network disconnect <inst> <net>
# Instances / account
suble instance list | create | info <ref> | delete <ref> | power <ref> <action> | ssh <ref>
suble login | logout | projects | apps | plansinstance is the canonical name (vm still works as an alias). Every <ref>
accepts an instance name or uid — names are unique within a project. Add
--project <uid> (or set SUBLE_PROJECT) to skip the picker.
CI / preview environments
# .github/workflows — on push: rebuild + redeploy; on PR close: tear down
- run: npx @suble/cli update # idempotent; rebuilds container, re-asserts everything
env: { SUBLE_API_KEY: ${{ secrets.SUBLE_API_KEY }} }
# teardown step:
- run: npx @suble/cli destroy --yes
env: { SUBLE_API_KEY: ${{ secrets.SUBLE_API_KEY }} }Run suble update --dry-run (or suble plan) first to preview changes — it
flags when a container would be recreated before it happens.
Authentication
Interactive commands open your browser and sign you in with OAuth 2.1 (PKCE);
the token is cached in ~/.config/suble/credentials.json and refreshed
automatically.
For CI / non-interactive use, create a project API key (dashboard → Project →
API keys). suble update never opens a browser — it requires a key:
export SUBLE_API_KEY=sk_proj_…
suble update| Env | |
|---|---|
| SUBLE_API_KEY | a sk_proj_… project key (skips the browser login) |
| SUBLE_PROJECT | default project uid |
| SUBLE_API_URL | API base (default https://api.v3.suble.io) |
Develop
npm install
npm run build # tsc → dist/
npm test # build + parser/spec round-trip tests
node dist/cli.js --helpZero runtime dependencies — just Node ≥ 18.
