CLI
suble.yml reference
One declarative file describes a whole instance. suble deploy / suble update reconcile it idempotently. Every field is below.
name: web # instance hostname (lowercase, digits, hyphens)
plan: BXS.s1 # plan code — see the dashboard catalog
# --- source: pick exactly one -------------------------------------------------
image: ghcr.io/me/app:1.4.2 # build: | image: | app: | os: | template:
# --- container ----------------------------------------------------------------
env:
NODE_ENV: production
DATABASE_URL: ${DATABASE_URL} # ${VAR} expands from your shell/CI at deploy
STRIPE_KEY: "%{project.stripeKey}%" # %{project.NAME}% = a shared Suble variable
volumes:
- "data:/var/lib/app" # named volumes persist across redeploys
resources:
cpus: "0.5" # CPU share (e.g. 0.5, 2)
memory: 512m # memory limit (e.g. 512m, 1g)
expose:
domain: app.example.com # ingress + automatic Let's Encrypt TLS
port: 8080
# --- deploy: how re-deploys roll out -----------------------------------------
deploy:
strategy: rolling # rolling (default) | canary | blue-green | immediate
replicas: 3 # load-balanced copies (requires expose:)
max_unavailable: 1 # replicas cycled per batch (rolling/canary) — 1 or "25%"
drain: 10 # seconds to let an old container finish before it's stopped
keep: 3 # image versions kept on the VM for `suble rollback` (older pruned)
healthcheck:
type: http # docker (default) | http | tcp
path: /healthz
port: 8080 # inferred from expose:/ports: if omitted
status: 200
initial_delay: 5 # wait before the first probe (slow boot)
interval: 3 # seconds between probes
retries: 10 # failed probes tolerated before aborting
timeout: 60 # overall deadline to become healthy
firewall: smart # off | smart | strict
networks: ["my-net"]
backup: basic # none | basic | extendedInstance
Top-level fields that identify the instance. name and plan are required; everything else is optional.
Fields
namerequired | string | Instance hostname — lowercase letters, digits and hyphens. Used to find-or-create the instance (it's never recreated once it exists). |
planrequired | string | Plan code from the dashboard catalog (e.g. BXS.s1). Plan changes are warned about, not auto-resized. |
project | string | Project uid or name to deploy into. Defaults to your only project, or prompts when interactive. |
password | string | OS root login. Use ${ENV_VAR} — never a literal. Only applied on first create. |
backup | none | basic | extended | Automatic backup tier for the instance. |
Source — pick exactly one
There's no kind: field — the source key you set picks the type. build/image run a Docker container; app is a managed 1-click app or database; os/template provision a VM.
Fields
build | block | Build a local Dockerfile and push it to your private registry.suble.io; the instance pulls it automatically (no docker login on the server). Sub-fields: dockerfile (default Dockerfile), context (default .), tag, container. |
image | string | Run a published registry image, e.g. nginx:1.27 or ghcr.io/you/app:1.4.2. Pin a version for reproducible deploys. An image in your own registry.suble.io/<project>/… namespace is pulled with automatic, pull-only auth — no docker login needed (see the Container Registry docs). |
app | string | A managed 1-click app or database: postgresql, mysql, mariadb, mongodb, redis, clickhouse, coolify, docker, … |
os | string | A plain OS image (e.g. ubuntu-24.04). |
template | string | Provision from one of your saved templates (tpl_…). |
build:
dockerfile: Dockerfile # default
context: . # default
tag: web:latest
container: webContainer
Applies to build:/image: sources. Ports, environment and volumes for the container.
Fields
ports | string[] | Host:container port mappings, e.g. ["80:8080"]. Not used with deploy.replicas > 1 (they'd collide — use expose:). |
env | map | string[] | Environment variables, as a mapping (KEY: value) or a list of "KEY=VALUE". Two interpolations: ${VAR} expands from your local/CI environment; %{project.NAME}% / %{cluster.NAME}% pull a shared Suble variable, resolved server-side at deploy so no secret is committed. |
volumes | string[] | Volume mounts host:container or name:container, e.g. ["data:/var/lib/app"]. Named volumes persist across redeploys. |
Shared variables
Reference a project- or cluster-scoped shared variable inside any env value. Unlike ${VAR} (expanded from your local/CI environment by the CLI at deploy), these are resolved server-side — so the secret lives in Suble, never in your committed suble.yml. A missing reference fails the deploy loudly. Create them under Project → Settings → Shared variables, or on a cluster's page.
Fields
%{project.NAME}% | reference | A project variable — usable by every instance and cluster in the project. |
%{cluster.NAME}% | reference | A cluster variable — only that cluster's services can reference it. |
env:
NODE_ENV: production
# a project secret, resolved server-side at deploy
STRIPE_KEY: "%{project.stripeKey}%"
# embed a cluster secret inside a larger value
DATABASE_URL: "postgres://app:%{cluster.dbPassword}%@db:5432/app"resources — CPU & memory limits
Cap what the container can consume so one container (or replica) can't starve the others on the instance. Applies to build:/image: sources.
Fields
resources.cpus | number | CPU share — fractional cores, e.g. "0.5" or "2" (Docker --cpus). |
resources.memory | size | Hard memory limit, e.g. "512m" or "1g" (Docker --memory). |
expose — ingress + automatic SSL
Serve the container at a hostname behind the managed Caddy reverse proxy, with a Let's Encrypt certificate issued automatically once DNS points at the instance. Required for deploy.replicas > 1 and for zero-downtime blue/green.
Fields
expose.domainrequired | string | Full domain, e.g. app.example.com. Point its A record at the instance for the cert to issue. |
expose.port | number | Container port Caddy proxies to (default 80). |
loadbalancer — front a single instance with a managed LB
Put a dedicated managed load balancer (its own Caddy/HAProxy VM, or an HA pair with a floating VIP) in front of this instance, instead of the in-VM ingress — for a stable ingress IP, HA at the edge, or terminating TLS off the app VM. Requires expose: (the listener + backend target are derived from it). Point the domain's DNS at the LB IP printed after deploy. Declarative + idempotent — the LB is reused across deploys by name. (For load-balancing across many VMs, use a cluster instead.)
Fields
loadbalancer.size | lb.xs | lb.s | lb.m | lb.l | LB VM size. Default lb.xs. |
loadbalancer.ha | boolean | Provision an HA pair with a floating VIP instead of a single LB VM. Default false. |
loadbalancer.protocol | http | https | tcp | Listener protocol. https (default) ⇒ automatic TLS on expose.domain (so https needs expose.domain). |
loadbalancer.network | string | Private network (name/uid) to reach the backend over, instead of its public IP. |
loadbalancer.name | string | LB name; defaults to <instance>-lb. Reused across deploys to stay idempotent. |
# A single instance fronted by a MANAGED load balancer (dedicated Caddy/HAProxy VM, or an
# HA pair) instead of the in-VM ingress. The listener + backend are derived from expose:.
name: web
plan: BXS.s1
image: ghcr.io/acme/web:1.4.0
expose:
domain: app.example.com # the LB serves this host
port: 8080
loadbalancer:
size: lb.xs # lb.xs | lb.s | lb.m | lb.l
ha: false # true → HA pair with a floating VIP
protocol: https # https ⇒ auto-TLS on expose.domain
deploy:
strategy: rolling
replicas: 2deploy — strategy, replicas & health checks
How re-deploys roll out. Omit it for the default (rolling). All strategies except immediate health-check the new container(s) before sending them traffic; replicas runs several load-balanced copies. Applies to build:/image: containers.
Fields
deploy.strategy | rolling | canary | blue-green | immediate | rolling (default): replace replicas in batches of max_unavailable, surging new before dropping old — zero-downtime. canary: verify ONE new container first, then roll the rest. blue-green: bring up the full new set, health-check all, switch traffic atomically (instant rollback; 2× resources). immediate: replace all at once with no health checks (brief downtime). With 1 replica behind expose:, the gated strategies all do a zero-downtime swap. |
deploy.replicas | number | Number of load-balanced copies to run (default 1, max 20). Requires expose: and no host ports:. The ingress round-robins across them; the strategy rolls the whole set. |
deploy.max_unavailable | int | "N%" | Replicas cycled per batch for rolling/canary (default 1). E.g. 1 or "25%". Larger = faster rollout, more mixed-version overlap. |
deploy.drain | number | Seconds an old container gets to finish in-flight requests (SIGTERM) before it's stopped during cut-over. Default 10; 0 to kill immediately. Ignored by immediate. |
deploy.keep | number | How many image versions to keep on the VM after a successful deploy (default 3, 1–50); older ones are pruned to reclaim disk. These kept versions are what `suble rollback` reverts to — the previous one is already on the instance, so rollback needs no rebuild or pull. build: sources are tagged per revision so each deploy is a distinct, rollback-able image. |
deploy.healthcheck.type | docker | http | tcp | docker (default): container running + stable + image HEALTHCHECK if present. http: GET the path and expect status. tcp: port accepts a connection. http/tcp probe the container's network IP, so no published port is needed. |
deploy.healthcheck.path | string | HTTP path to probe (http only), e.g. /healthz. Default /. |
deploy.healthcheck.port | number | Container port to probe (http/tcp). Inferred from expose:/ports: when omitted. |
deploy.healthcheck.status | number | Expected HTTP status (http only). Default 200. |
deploy.healthcheck.timeout | number | Overall seconds to become healthy before the deploy aborts. Default 60. |
deploy.healthcheck.interval | number | Seconds between probes. Default 3. |
deploy.healthcheck.initial_delay | number | Seconds to wait before the FIRST probe — give a slow-booting app time to start listening. Default 0. |
deploy.healthcheck.retries | number | Failed probes tolerated before the deploy aborts. Default ≈ timeout ÷ interval. Whichever of retries or timeout hits first stops the deploy. |
deploy:
strategy: canary # verify 1 new container, then roll the rest
replicas: 4 # 4 copies, load-balanced by the ingress
max_unavailable: 2 # after the canary, replace 2 at a time
healthcheck:
type: http
path: /healthz
status: 200
timeout: 60firewall
A string shorthand (off | smart | strict) or a block. smart keeps management ports reachable; strict is default-deny and requires an allow: list of source CIDRs so a headless run can't lock you out. Manual rules: are only valid with mode: off.
Fields
firewall.moderequired | off | smart | strict | off: default-allow (+ optional rules:). smart: sensible managed defaults. strict: default-deny (+ required allow:). |
firewall.allow | string[] | Source CIDRs allowed under strict (required for strict). |
firewall.rules | rule[] | Explicit rules (mode: off only): direction (in|out), action (ACCEPT|DROP|REJECT), and optional proto/source/dest/sport/dport/comment. |
networks
Private networks to attach the instance to, by name or uid. Attach-if-missing — never auto-detached.
Fields
networks | string[] | Private network names or uids, e.g. ["my-private-net"]. |
cluster — a container cluster (infra)
A cluster is shared infrastructure — N member VMs + a managed load balancer + a private network — that runs one or more container services (see service: below). A cluster: block defines that infrastructure; deploy it with suble deploy, or create it imperatively with suble cluster create. To run a workload, deploy a service into it.
Fields
cluster.sizerequired | string | Member VM plan code (e.g. BXS.s1). Every member runs this plan. |
cluster.replicas | number | Number of member VMs (default 2, max 9). This is the VM count — a service's own replicas set how many container copies run across them. |
cluster.ha | boolean | Provision an HA load-balancer pair with a floating VIP instead of a single LB. Default false. |
cluster.network | string | Private network (name or uid) the members + LB share. Auto-created if omitted. |
cluster.image / cluster.port / cluster.domain | shorthand | Optional single-service shorthand: set an image (+ port, optional domain) to seed one "web" service at create time, instead of adding services separately. |
cluster.engine | coming soon | Database clusters (postgres/mysql/mongodb/redis) are not available yet — app/container clusters only for now. |
# suble.yml — a cluster's INFRASTRUCTURE: member VMs + a load balancer +
# a private network. Services (below) are deployed into it separately.
name: shop
cluster:
size: BXS.s1 # member VM plan
replicas: 3 # number of member VMs
ha: true # HA load-balancer pair (floating VIP) instead of a single LB
# network: shop-net # optional existing private network (auto-created if omitted)service — deploy one service into a cluster
The per-repo model: each repo has its own suble.yml with a service: block naming the cluster it belongs to. suble deploy builds + pushes (if build:) and create-or-updates ONLY this service — it never touches the cluster's other services, so many repos with their own CI/CD can share one cluster. If the target cluster doesn't exist, the optional cluster* fields bootstrap it (and are ignored if it already does).
Fields
service.clusterrequired | string | Target cluster (name or uid). Must exist, or be bootstrapped via the cluster* fields below. |
service.build | service.imagerequired | block | string | Source: build a local Dockerfile (pushed to your registry.suble.io namespace, pulled automatically), or run a prebuilt image. Exactly one. |
service.portrequired | number | Container port the service listens on. |
service.replicas | number | Container copies, spread round-robin across the cluster's members (default 1, max 9). |
service.domain | string | Public hostname → exposed via the cluster LB with https + auto-TLS (point its DNS at the LB). Omit to serve on the LB's IP over http (one no-domain service per cluster), or leave internal. |
service.env | map | string[] | Environment variables (KEY: value or "KEY=VALUE"). ${VAR} expands from the environment; %{project.NAME}% / %{cluster.NAME}% reference a shared Suble variable, resolved at deploy. Merged over the cluster's env. |
service.cpus / service.memory | number / size | Per-container limits, e.g. cpus: "1", memory: 512m. |
service.clusterSize / clusterInstances / clusterHa / clusterNetwork | bootstrap | Used ONLY to create the cluster if it's missing (member plan / count / HA / network). Ignored when the cluster already exists — so repos never fight over the member count. |
# suble.yml in a repo — deploys THIS repo as ONE service into a shared cluster.
# Each repo has its own suble.yml + CI/CD; they all share the same cluster.
name: web
service:
cluster: shop # target cluster (bootstrapped if missing — see cluster* below)
build: # build this repo's Dockerfile + push to your registry…
dockerfile: Dockerfile
# image: registry.suble.io/acme/web:1.4.0 # …or run a prebuilt image instead
port: 3000 # container port
replicas: 3 # copies, spread across the cluster's members
domain: app.example.com # exposed via the cluster LB (https + auto-TLS)
# omit → served on the LB's IP over http
env:
NODE_ENV: production
cpus: "1" # optional per-container limits
memory: 512m
# --- optional: create the cluster if it doesn't exist yet (IGNORED if it does,
# so two repos never fight over the member count) ---
clusterSize: BXS.s1
clusterInstances: 3
clusterHa: trueExamples & CI/CD
Copy-paste starting points. The single-instance and cluster/service files are all just suble.yml — suble deploy (interactive) or suble update (CI, with a project API key) reconciles whichever one is in the repo.
name: web # instance hostname (lowercase, digits, hyphens)
plan: BXS.s1 # plan code — see the dashboard catalog
# --- source: pick exactly one -------------------------------------------------
image: ghcr.io/me/app:1.4.2 # build: | image: | app: | os: | template:
# --- container ----------------------------------------------------------------
env:
NODE_ENV: production
DATABASE_URL: ${DATABASE_URL} # ${VAR} expands from your shell/CI at deploy
STRIPE_KEY: "%{project.stripeKey}%" # %{project.NAME}% = a shared Suble variable
volumes:
- "data:/var/lib/app" # named volumes persist across redeploys
resources:
cpus: "0.5" # CPU share (e.g. 0.5, 2)
memory: 512m # memory limit (e.g. 512m, 1g)
expose:
domain: app.example.com # ingress + automatic Let's Encrypt TLS
port: 8080
# --- deploy: how re-deploys roll out -----------------------------------------
deploy:
strategy: rolling # rolling (default) | canary | blue-green | immediate
replicas: 3 # load-balanced copies (requires expose:)
max_unavailable: 1 # replicas cycled per batch (rolling/canary) — 1 or "25%"
drain: 10 # seconds to let an old container finish before it's stopped
keep: 3 # image versions kept on the VM for `suble rollback` (older pruned)
healthcheck:
type: http # docker (default) | http | tcp
path: /healthz
port: 8080 # inferred from expose:/ports: if omitted
status: 200
initial_delay: 5 # wait before the first probe (slow boot)
interval: 3 # seconds between probes
retries: 10 # failed probes tolerated before aborting
timeout: 60 # overall deadline to become healthy
firewall: smart # off | smart | strict
networks: ["my-net"]
backup: basic # none | basic | extended# a DIFFERENT repo — same cluster "shop", its own service, its own pipeline.
name: api
service:
cluster: shop
image: registry.suble.io/acme/api:2.1.0
port: 8080
replicas: 2
domain: api.example.com# an internal worker — no domain, so it isn't exposed by the LB
# (still reachable from the other services inside the cluster).
name: worker
service:
cluster: shop
image: registry.suble.io/acme/worker:2.1.0
port: 9000
replicas: 2# a raw-TCP service — a non-HTTP protocol published straight through the LB.
# protocol: tcp + listenPort ⇒ HAProxy forwards raw TCP to your containers (no domain, no TLS
# termination — your app does its own TLS). For game servers, brokers, custom protocols, etc.
name: game
service:
cluster: shop
image: registry.suble.io/acme/game-server:1.0
port: 25565 # the container's port
replicas: 3
protocol: tcp
listenPort: 25565 # the external LB port clients connect to (not 80/443)# .github/workflows/deploy.yml — deploy this repo's service on every push to main.
name: Deploy to Suble
on:
push:
branches: [main]
jobs:
deploy:
runs-on: ubuntu-latest # runners have Docker, so `build:` works
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version: 20
# A project API key authenticates the CLI (no browser). `update` is non-interactive:
# it builds + pushes (if build:) and reconciles the service into the cluster.
- run: npx @suble/cli@latest update
env:
SUBLE_API_KEY: ${{ secrets.SUBLE_API_KEY }}
SUBLE_PROJECT: ${{ vars.SUBLE_PROJECT }} # or set project: in suble.yml# .gitlab-ci.yml
deploy:
image: node:20
services: [docker:dind] # build runs against Docker-in-Docker
variables:
DOCKER_HOST: tcp://docker:2375
script:
- npx @suble/cli@latest update
# SUBLE_API_KEY + SUBLE_PROJECT set as masked CI/CD variables
rules:
- if: $CI_COMMIT_BRANCH == "main"# No file needed — manage a cluster's services straight from the CLI:
suble cluster create shop --size BXS.s1 --replicas 3 --ha
suble cluster service add shop web --image nginx:1.27 --port 80 --replicas 3 --domain app.example.com
suble cluster service add shop api --image ghcr.io/acme/api:2.1 --port 8080 --replicas 2
suble cluster service scale shop api --replicas 4
suble cluster service deploy shop web --image nginx:1.29 # rolling, one at a time
suble cluster show shop
suble cluster service rm shop workerDatabases & users (managed DB apps)
For app: postgresql/mysql/… — databases and users to provision on first deploy. The engine's root credentials are generated server-side; fetch them with suble app info.
Fields
databases | string[] | Databases to create on first deploy. |
users | user[] | DB users to create on first deploy: name, password (use ${VAR}), and optional database. |