Deploying
One command builds your images, provisions infrastructure, runs migrations and cuts over DNS.
Deploy
The deploy command performs these steps automatically:
- Validates your
sp00ky.ymlconfiguration - Builds Docker images for each app (linux/amd64)
- Pushes images to your team’s private build registry on Sp00ky Cloud: only the layers it does not have yet, so an unchanged image costs seconds. Apps on a machine pool or a dedicated machine upload an image archive instead, which their machines download.
- Provisions infrastructure (SurrealDB, Scheduler, SSP instances)
- Runs migrations against the cloud database
- Deploys your backend and frontend containers
- Configures DNS and SSL certificates
You’ll see real-time progress as each step completes. These are the steps of a VM project; a Cloudflare project has no containers, see Cloudflare projects.
Run spky lint before deploying to catch config issues early.
Flags
| Flag | Effect |
|---|---|
--upgrade | Also move the SSP and Scheduler to the newest published build. Only for a project whose version is any or unset: a pinned version stays where sp00ky.yml put it, and newest already rolls on every deploy. The swap is blue/green, see Zero-downtime upgrades. On pay as you go it also moves a SurrealDB 3 database to SurrealDB 4 after a backup, whatever version says, see SurrealDB version. |
--only api,web | Build and deploy just these apps. Everything else keeps running untouched. |
--clean | Wipe the scheduler’s persistent volume first. Last resort: the scheduler self-heals stale snapshot state on startup, and spky verify --fix (or POST /admin/resync on the scheduler) repairs it without a wipe. Your SurrealDB data is not touched either way. |
--force-schema | Re-apply the internal schema and remote functions even when the hash says nothing changed. Drift recovery. |
--cache-bust | Clients reloading onto this version clear service-worker caches first. |
--mandatory | Clients reload immediately instead of being offered a notification. Broken-build kill switch. |
--only is the one to reach for during normal iteration: a frontend-only change doesn’t need the
backend rebuilt.
A backend on a dedicated machine shows up in the deploy table as
machine/<name> with its public address, moving through creating, booting and serving. A
deploy of such a backend takes a few minutes longer: a fresh VM boots beside the old one and only
takes over once it answers its healthcheck.
Zero-downtime upgrades
Upgrading or restarting the Scheduler and SSP never takes sync down. Sp00ky Cloud starts the new container first and stops the old one only after the new one has taken over:
- Scheduler. The new scheduler starts on the same volume and asks the old one to hand over. Requests that arrive during the swap wait a moment (typically well under a second) instead of failing, writes keep succeeding, and SSPs keep their registration.
- SSP. The new SSP loads its rows from the old one’s checkpoints and follows every change as a standby, without writing anything. Once it has caught up it takes over the views; only views whose results differ are republished. Then the old SSP is stopped.
If the new container never becomes ready, the old one keeps serving and the
restart or deploy reports the failure. --clean (which wipes the scheduler
volume) and SurrealDB upgrades still stop the old container first.
Failed deploys and rollbacks
A deploy that fails does not take the project down: what serves keeps serving,
custom domains included, until new containers are up. When a deploy fails
before it ever served (its migrations failed, it never left the queue, its apps
never came up healthy), Sp00ky Cloud falls back by itself within seconds: it
deploys again what served last, as a new version marked auto rollback in the
history. A failed rollback is not rolled back again, and a deployment that
served and failed later is left to autoheal.
To go back by hand, open Deployments in the console
and choose Roll back on a version that served. Any of the newest ten
deployments can be brought back; the images of older ones may be gone. A
rollback deploys that version’s apps (backends and frontends, their images and
settings) as a new version. The scheduler, the SSPs and SurrealDB stay on the
versions that run now, and no migrations run, since migrations only go forward.
Agents can do the same with the deploy_rollback tool of the
Sp00ky Cloud MCP server, which takes the version.
Cloudflare projects
A Cloudflare project (plan payg, pay as you go) has no VMs and no containers of its own except its
database. spky deploy puts each part of it here:
| Part | Where it runs |
|---|---|
| Web app | Static assets on Workers, built from deploy.static. No Docker image. |
| SSP | A Durable Object in the project’s SSP Worker. It also runs your outbox jobs; there is no separate scheduler. |
| Worker backends | Dynamic Workers inside the SSP Worker. spky deploy bundles each worker.entry with esbuild and uploads it with the SSP Worker. Their expose paths are served on https://<slug>.<domain>. |
| Database | With surrealdb.hosting: cloud, a SurrealDB of the project’s own (1 GiB), run by Sp00ky Cloud at {slug}-db.<domain> like a VM project’s; spky deploy bakes its endpoint into the web app. With hosting: external your own database. |
| Schema | Pushed to the SSP after the deploy, with your schedules, workflows and job policies. spky push retries it. |
Jobs, schedules and workflows on Cloudflare
They behave as on a VM: the same outbox, retries, leases, schedules and workflows. The SSP’s Durable Object runs them on its alarm, and only sets one when something is due, so an idle project costs nothing and the object is evicted between jobs.
- Wakes follow what is due. A delayed job, a
step.sleep, a retry’s backoff and a schedule’s next fire each wake the SSP at that moment (a job about a second late), not on a polling cadence. Adelayof one day is one wake, not one a minute. - Long waits live in the database. A delay or backoff longer than 30 seconds is not a timer in memory: the job row waits, and an eviction cannot lose it. Shorter ones wait in memory.
- Background work does not load your views. A cron fire or a job wake runs on the SSP without rebuilding its live queries first, and keeps running while the SSP is still starting (or failing to start) after a deploy. A job created during that time runs at once.
- At least once. A job in flight when the object is evicted runs again when its lease runs out. Make job routes safe to repeat.
Docker backends do not run on a Cloudflare project. Make the backend a
Worker backend, or host it yourself (hosting: external) at a public
baseUrl the SSP Worker can reach. --only works as on a VM: spky deploy --only api bundles and
ships just that Worker backend, and the others keep their last version.
A Cloudflare project has no migrations: every deploy pushes the whole schema as the desired state.
Tables, fields, events, functions, params, analyzers and buckets are replaced with what your files
say. Two kinds keep their first definition: DEFINE ACCESS (overwriting a record access that has no
WITH JWT key gives it a new random key, which signs every user out) and DEFINE INDEX (an
overwrite rebuilds the index row by row). To change one of those, write OVERWRITE into that
statement yourself for one deploy.
Your own VMs (Pro)
A Pro project runs on two VMs of its own. The project host carries the scheduler, the SSPs, your backends and frontends and the proxy; the database VM carries SurrealDB, with its data on a volume at the provider. The shared host that Sp00ky Cloud runs on keeps only the control plane. The infrastructure is included in the Pro price.
| VM | Default type | Memory |
|---|---|---|
| Database VM | cax21 (ARM) | 8 GB, of which the database may use 6 GB |
| Project host | cx33 | 8 GB |
If cx33 cannot be had when the project is set up, the project host starts on cx23 (4 GB), or on
cpx22 for a new signup, and upgrades to cx33 by itself in a quiet window, 03:30 to 05:30 UTC. A
project admin can ask for it sooner. spky host status shows each VM and any upgrade waiting, and the
console shows the same under Settings, Machines.
An upgrade restarts the project host: web, API and live sync are down for about two minutes. The
database VM is not touched. spky host upgrade asks before it starts, and refuses while a backup, a
database upgrade, a move or a deploy is running, saying which.
A Pro project that still runs on the shared host moves onto its own VMs with spky project move. The
data is copied across while the project keeps running, and the move then waits for your go unless you
passed --no-hold.
cutover and rollback ask before they start, and say that the project goes down. abort stops a
move before the cutover and leaves everything where it was. After the cutover the project is
watched for a while, and rollback takes it back to the shared host in that time: the project goes
down and its data travels back. Only a Pro project can move.
When a project is destroyed, the volumes of its VMs are kept for 7 days.
Checking Status
Shows the current deployment version, status, and all running VMs with their roles and versions.
The operator dashboard
Every deployment also serves a dashboard in the browser, at
https://<slug>-admin.<domain>/admin. spky status prints the URL alongside the
others.
It is the same information spky status and spky logs give you, live and
without a terminal: scheduler and SSP health, end-to-end sync latency, backend
health, workflow runs as they happen, and a live log tail. It also carries the
actions, so you can restart an SSP, retry a failed workflow run, take a backup or
upgrade images from the browser.
Nobody can sign in until you grant someone access with spky admin add <user>.
See the admin dashboard for sign-in, the
actions and their consequences, and the
MCP server it serves for AI agents.
Scaling
Scale your SSP instances horizontally:
You can also set the default SSP count in your sp00ky.yml:
Bucket Storage
Buckets with a file: backend keep their files on a volume of their own,
mounted at /buckets in your SurrealDB container. Size it in sp00ky.yml:
- A Pro project on its own VMs: the scheduler no longer mounts this volume, so the dashboard does not list storage subfolders for such a project.
- It is a real disk of that size. When it is full, uploads fail; nothing
else on the host is affected. Raise
sizeGBand redeploy (or push) to grow it in place, without restarting the database. - It only grows. A deploy that asks for less than the volume already has is refused with the current size. Removing the key keeps the volume as it is.
- The smallest volume is 10 GB. Asking for less gets 10 GB, and you are billed for what you asked for.
- Billing: Starter includes 5 GB and Pro 20 GB. Above that, storage is
$0.10 per GB-month, prorated by the hour, on a separate invoice at the start
of each month.
spky cloud billing usage(and thebilling_usageMCP tool) shows the month so far.
See File Buckets for defining a bucket on this volume.
Infrastructure Environment Variables
Backends read their environment from the encrypted vault (spky env set). The
infrastructure containers Sp00ky Cloud owns — SSP, Scheduler, SurrealDB — take a
fixed environment assembled by the control plane. deployment.env is the escape
hatch for setting a runtime knob on them, per role and per project:
These are not secrets. Unlike spky env set, values are stored
unencrypted in the project’s cloud config. Put credentials in the vault, not here.
Keys the control plane manages itself are refused rather than silently
overridden: SPKY_DB_*, SPKY_AUTH_SECRET, SPKY_SCHEDULER_URL,
SPKY_SSP_ID, SPKY_SSP_ADVERTISE_ADDR, the SSP snapshot/arena directories,
SPKY_JOB_CONFIG, and S3_*. Those carry credentials, per-slot identity, or
values derived from your deployed backends — overriding them breaks the
deployment in ways that are hard to see (a stale SPKY_JOB_CONFIG disables job
routing while health checks still pass).
Values take effect when containers are next created, so run spky deploy or
spky restart after changing them. Omitting the key entirely keeps the previous
setting; an explicit empty map clears it.
deployment.env, the sync: block and the top-level anonymousLiveQueries
are read from sp00ky.yml on every deploy, whether spky deploy runs it or a
git-linked build does. Both paths write the same values,
so a push after a CLI deploy never reverts them.
One pair is set for you: with sync.transport: changefeed in sp00ky.yml,
spky deploy merges SPKY_INGEST_TRANSPORT and SPKY_CHANGEFEED_RETENTION
into the scheduler and ssp roles (your own keys win on a clash), so the
scheduler tails the feed the schema just gained. See
Sync transport. Likewise, with
sync.queryAllowlist set to warn or enforce, spky deploy merges
SPKY_SSP_QUERY_ALLOWLIST=<mode> into the ssp role and publishes the committed allowlist JSON
as _00_query_allowlist rows keyed by the frontend’s package.json version; under enforce a
missing or stale JSON fails the deploy. See the
Query allowlist guide.
SPKY_SSP_DB_TIMEOUT_SECS is the knob most worth knowing here: it bounds how
long a single SSP database call may take before it fails and the connection is
rebuilt (default 15s, 0 disables). It exists because the SurrealDB HTTP
engine has no request timeout of its own and every SSP database call shares one
connection, so an unbounded stall parks every route that touches the database.
See environment variables and
GET /health.
Log Level
logLevel in sp00ky.yml sets RUST_LOG on the Scheduler and SSP containers. Default is info.
Use a { dev, cloud } map to differ per environment:
The value is forwarded to tracing-subscriber, so target-specific directives work:
Each spky deploy sends the resolved cloud value to Sp00ky Cloud, which restarts the Scheduler and SSP containers with the new RUST_LOG. Use --upgrade if you also want the newest SSP and Scheduler build at the same time (it does not move a pinned version).
At trace, the Scheduler logs every replica query and every record it ingests during bootstrap. Useful for diagnosing “why isn’t this row showing up?” See Logs & Monitoring.