The deploy pipeline
Tabbify accepts the same remote deployment from two entry points: a GitHub
workflow or manual tcli deploy --remote. Both submit source coordinates to the
node asynchronously. The node clones the selected revision, builds an OCI image,
stores it in the platform registry, selects one eligible workload target, and
boots a CPU-only Firecracker runner. Public ingress reaches
the application at https://app.tabbify.io/app/<uuid>/ or a configured custom
domain.
The remote path does not build or push the image on your laptop. See the quickstart for the GitHub walkthrough; this page is the in-depth version for both entry points.
GitHub trigger
The tabbify-io/deploy GitHub Action can run on every push and submit the remote
deploy for you:
- uses: actions/checkout@v4
- uses: tabbify-io/deploy@v1
with:
token: ${{ secrets.TABBIFY_TOKEN }}
The action installs tcli and runs tcli deploy --remote. Two pieces of auth
flow through it, and they are different things:
tokenis yourTABBIFY_TOKEN— the bearer credential the node checks onPOST /v1/deploy. Mint it in the console under Deploy tokens (pick a network → Manage tokens → Mint deploy token → Copy; shown once), then add it once as a repository secret.clone-tokendefaults to the Action's automatic${{ github.token }}— a per-run, repo-scoped token the node uses togit cloneyour repo. There is no GitHub App to install and no webhook to register; the clone token lives on the build host and never enters the build guest.
What the node does
tcli deploy --remote posts the repository URL, git ref, clone token, tenant,
manifest, and optional selected target. The node acknowledges with a deploy ID
and continues asynchronously:
1. resolve the in-mesh registry (registry ULA from the roster)
2. resolve a builder supervisor (from [build].builder, the request, or the roster)
3. select one eligible workload target, using the requested supervisor or default
4. clone the repo at the pushed ref (using the clone token) and build an OCI image
5. push the image to the in-mesh Zot registry (/v2)
6. start one Firecracker runner on the selected target at derive_app_ula(uuid)
7. apply the configured health gate before activation
Because the response is async, you poll for the outcome:
GET /v1/deploy/<deploy_id>/status # 202 while pending, 200 with the result
The app UUID is stable for the deployment source. The CLI prefers an explicit
--app-uuid, then
[app].id in tabbify.toml, and finally falls back to
uuid_v5(NAMESPACE_URL, repo_url), so the same repository keeps its address
across pushes. If the manifest has no [[deploy]] block, the node selects the
default eligible supervisor.
One runtime: Firecracker
Tabbify ships a single public runtime. Every app is built from a Dockerfile into
an OCI image, converted to an ext4 rootfs, and booted as a Firecracker microVM
running the image's entrypoint. [build].kind = "docker" describes the OCI
build; runtime-selection keys in older manifests are inert.
See Runtimes for the full story.
The target is capability-checked against the supervisor's roster tags
before any dispatch: a supervisor must carry the firecracker tag to receive a
runner, and a build-only host is not handed an app it cannot boot.
Deploying from your own terminal
The GitHub Action is just a wrapper. You can run the exact same pipeline from any
terminal with tcli:
tcli deploy --remote \
--repo-url https://github.com/tabbify-io/quickstart \
--ref main \
--clone-token "$GITHUB_TOKEN"
tcli reads TABBIFY_TOKEN for the node bearer auth and posts to
https://api.tabbify.io/v1/deploy by default. Or hit the endpoint directly with
your Tabbify API token:
curl -X POST -H "Authorization: Bearer $TABBIFY_TOKEN" \
https://api.tabbify.io/v1/deploy \
-d '{"repo_url":"https://github.com/tabbify-io/quickstart",
"git_ref":"main","tenant":"tabbify",
"app_uuid":"0191e7c2-1111-7222-8333-444455556666",
"clone_token":"<github-token>",
"manifest_toml":"[app]\nname=\"quickstart\"\n[build]\nkind=\"docker\"\n[runtime]\nport=8080\n[routes]\ndynamic_prefixes=[\"/\"]\n",
"targets":[{"supervisor":"thinkpad"}]}'
manifest_toml carries your raw tabbify.toml so the node applies [runtime]
(including port) and [routes] on spawn. Omit it and the app gets the
defaults (port 8080) — fine for an 8080 image, a crash-loop for anything else.
tcli deploy sends this for you; only a hand-rolled curl needs it explicit.
Build, target resolution, capability, registry, spawn, and health failures are reported in deploy status. See the registry for the artifact store and auth for how tokens are issued.
Where it lands: the runner
The pipeline terminates at a runner. supervisord is pure control plane: it
spawns a detached tabbify-runner, applies the configured health gate, and
hands back the app_ula. The address is derived from the app UUID rather than
the host, so later deployments keep the same route.
You can inspect the live mesh from the node:
curl -H "Authorization: Bearer $TABBIFY_TOKEN" \
https://api.tabbify.io/v1/topology
That topology view is for visibility only — addressing is always the UUID hash.
A supervisor also exposes POST /v1/apps/{uuid}/start for advanced or manual
control, but in the normal GitHub Actions flow the deploy pipeline starts the
runner on the selected supervisor; you do not call it by hand.
Status and release limits
The status operation reports the current phase, elapsed time, health outcome, and, when present, a bounded tail of local application output. The tail is for diagnosis and is not durable platform logging.
The platform does not currently provide an automated rollback operation or a durable deployment history. To recover, submit a known-good source revision as a new deployment and follow its status. Passing the candidate health gate reduces risk, but it is not a guarantee that a release will be uninterrupted.
See the runtime for the runner lifecycle, routing
for the address scheme, and the CLI reference for tcli.