PandaStack

Apps (git-driven hosting)

Connect a Git repo and PandaStack builds and serves it behind a stable URL — auto-detected frameworks, zero-downtime deploys, scale-to-zero, and push-to-deploy from GitHub.

Apps are PandaStack's Vercel/Render/Railway-style hosting: point it at a Git repository and PandaStack clones it, detects the framework, installs dependencies, builds, and serves the result behind a stable URL — all inside an isolated Firecracker microVM. Deploys are blue-green (zero downtime), idle apps scale to zero and wake on the next request, and a connected GitHub repo redeploys automatically on every push.

An app runs in a persistent sandbox built from the universal base template — Ubuntu 24.04 with mise-managed runtimes (Node 22 LTS, Python 3.12, Go, Bun pre-warmed). Your repo declares the exact versions it needs (.nvmrc / .python-version / .tool-versions / mise.toml) and the deploy pipeline installs them on demand.

Apps are ephemeral-friendly: they scale to zero when idle and wake on the next request, so a per-PR preview app costs compute only while someone is looking at it. For the shared lifecycle, the guarantees at each step, and when to choose an ephemeral app over an always-on one, see Ephemeral environments.

Create an app

REST

curl -X POST https://api.pandastack.ai/v1/apps \
  -H "Authorization: Bearer $PANDASTACK_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "name": "my-site",
    "git_url": "https://github.com/me/my-site",
    "git_branch": "main",
    "port": 3000
  }'

Python

import pandastack

client = pandastack.Client(api_key="pds_...")
app = client.apps.create(
    name="my-site",
    git_url="https://github.com/me/my-site",
    git_branch="main",
)
deploy = client.apps.deploy(app["id"])
print(deploy["status"])  # "queued"

TypeScript

import { Client } from "@pandastack/sdk";

const client = new Client({ apiKey: "pds_..." });
const app = await client.apps.create({
  name: "my-site",
  git_url: "https://github.com/me/my-site",
});
await client.apps.deploy(app.id);

CLI

pandastack app create \
  --name my-site \
  --git-url https://github.com/me/my-site
pandastack app deploy <app-id>

Creating an app stores its configuration but does not build it — trigger the first build with a deploy. All create fields except name and git_url are optional:

FieldDefaultNotes
nameRequired. Unique app name within the workspace.
git_urlRequired. Repository to clone and build.
git_branchmainBranch to track and deploy.
frameworkauto-detectPin to nextjs / vite / cra / node / python / static / generic. Empty = auto-detect.
runtime / runtime_versionautoForce a mise runtime (e.g. node 20); empty resolves from the repo's version files.
install_commandframework defaultOverride the dependency-install step.
build_commandframework defaultOverride the build step.
start_commandframework defaultOverride the runtime start command (required for python/generic).
root_directoryrepo rootSub-directory of the repo to build from (monorepos).
port3000Port the app listens on inside the VM.
env{}Environment variables injected at both build and runtime.
templatebasemicroVM template for the runtime sandbox.
cpu2vCPU for the runtime sandbox.
memory_mb1024RAM for the runtime sandbox (see the RAM note).
auto_deploytrueRedeploy on a matching GitHub push (requires a connected repo).
auto_hibernatetrueScale to zero after idle_timeout_seconds of no traffic.
idle_timeout_seconds900Idle window before hibernation (15 min default; min 60).

Framework detection

If you don't pin a framework, PandaStack detects it from the repo, in this precedence:

  1. Explicit pin — the framework field on the app.
  2. Repo manifest — a pandastack.json at the repo root (see below).
  3. Auto-detect — from the files present:
DetectedSignalInstallBuildStart (default)
nextjspackage.json has nextnpm installnpm run buildnext start -p $PORT -H 0.0.0.0
vitepackage.json has vitenpm installnpm run buildserve -s dist -l $PORT
crapackage.json has react-scriptsnpm installnpm run buildserve -s build -l $PORT
nodepackage.json (other)npm installnpm start
staticindex.html, no package.jsonserve -s . -l $PORT
pythonrequirements.txt / pyproject.toml / Pipfile / setup.pypip install -r requirements.txtyou must set start_command
genericnone of the aboveyou must set start_command

A user override always wins over the manifest, which wins over the framework default. For Python and generic apps, set start_command yourself (e.g. uvicorn app:app --host 0.0.0.0 --port $PORT) — $PORT and $HOST are exported into the launch shell.

The pandastack.json manifest

Commit a pandastack.json at the repo root to declare build config in the repo instead of on the app:

{
  "type": "vite",
  "outputDir": "dist",
  "installCommand": "pnpm install",
  "buildCommand": "pnpm build",
  "startCommand": "node server.js"
}

Deploy

A deploy provisions a fresh sandbox, builds the repo in it, health-checks the app, then atomically flips traffic to the new sandbox (blue-green) and tears down the old one.

# Deploy the tracked branch's latest commit
curl -X POST https://api.pandastack.ai/v1/apps/<id>/deploys \
  -H "Authorization: Bearer $PANDASTACK_API_KEY"

# Or pin a specific ref (commit SHA, branch, or tag)
curl -X POST https://api.pandastack.ai/v1/apps/<id>/deploys \
  -H "Authorization: Bearer $PANDASTACK_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{"git_ref": "abc1234"}'

The pipeline runs (12-minute budget):

  1. Provision a fresh blue sandbox on the app's template.
  2. Shallow git clone the ref (private repos use a GitHub installation token, never persisted).
  3. Detect the framework and read pandastack.json.
  4. Set up runtimes with mise (mise install honours .nvmrc / .python-version / .tool-versions / mise.toml).
  5. Run install → build, with mise reshim so newly-installed CLIs (uvicorn, gunicorn, …) are on PATH.
  6. Start the app detached, then HTTP health-check its port (up to ~60s).
  7. Blue-green flip: point the app at the new sandbox, mark the previous deployment superseded, and tear the old sandbox down.

A deployment moves through queuedbuildingdeployinglive, or failed if any step errors. A successful flip marks the prior deployment superseded; a rollback marks one rolled_back.

Watch a deploy's logs

GET /v1/apps/{id}/deploys/{deployID}/logs streams the build log (SSE):

for line in client.apps.deploy_logs(app_id, deployment_id):
    print(line)

List or fetch deployments with client.apps.deployments(app_id) and client.apps.deployment(app_id, deployment_id) (the single-deployment record includes its full build log).

Connect

Once an app is running, GET /v1/apps/{id} returns a computed url:

  • When PandaStack is configured with an app host suffix (production), the app gets a stable host: https://<app-id>.<suffix>/. The app UUID is the bearer credential, and the URL survives every deploy.
  • Otherwise it falls back to the authenticated path proxy: /v1/apps/{id}/proxy/.

Either form is durable across blue-green deploys — it always forwards to the app's current sandbox.

Runtime logs

App stdout/stderr is captured in the guest to /var/log/pandastack-app.log and served by GET /v1/apps/{id}/runtime-logs (add ?follow=1 for a live SSE tail):

curl -H "Authorization: Bearer $PANDASTACK_API_KEY" \
  "https://api.pandastack.ai/v1/apps/<id>/runtime-logs?follow=1"

This is not the same as GET /v1/sandboxes/{id}/logs, which serves the host-side Firecracker console log. Use runtime-logs for your application's output.

Scale to zero (auto-hibernate)

With auto_hibernate (the default), an app that receives no traffic for idle_timeout_seconds (default 15 minutes) is hibernated — its sandbox is snapshotted and stopped, freeing CPU/RAM. The next request to its URL transparently wakes it (typically sub-second) and the request proceeds; concurrent requests during a wake are coalesced so only one wake happens. The app's /workspace state and the deployed build are preserved across the cycle.

Set auto_hibernate: false to keep an app always-on, or tune idle_timeout_seconds (minimum 60).

Health monitoring

A background monitor reconciles every running app on a 30-second loop. If an app's port stops responding for 2 consecutive checks, the monitor restarts the app process in place. After 5 restarts in the app's lifetime without recovery, it parks the app in error (so a crash-looping app doesn't restart forever). If the underlying sandbox is lost entirely, the app is moved to error.

Deploy from GitHub

Connecting a GitHub repository unlocks private-repo cloning and push-to-deploy.

  1. ConnectGET /v1/github/connect returns the GitHub App install URL. Install it on the repos you want; the callback records the installation against your workspace.
  2. List reposGET /v1/github/repos?installation_id=<id> lists the repositories that installation can access.
  3. Create the app with the GitHub fields so pushes can match it:
curl -X POST https://api.pandastack.ai/v1/apps \
  -H "Authorization: Bearer $PANDASTACK_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "name": "my-site",
    "git_url": "https://github.com/me/my-site",
    "git_branch": "main",
    "github_installation_id": 12345678,
    "github_repo_full_name": "me/my-site",
    "auto_deploy": true
  }'

Now an HMAC-verified push webhook to the tracked branch triggers a deploy pinned to the exact pushed commit, for every app with auto_deploy: true whose repo and branch match. Private repos are cloned with an on-demand installation token that is never written to logs or persisted.

Manage connections with GET /v1/github/installations and DELETE /v1/github/installations/{id}.

Rollback

POST /v1/apps/{id}/rollback re-deploys the most recent superseded deployment (or a specific one via {"deployment_id": "..."}) — a byte-identical rebuild on a fresh sandbox, pinned to that deployment's commit, with the same blue-green flip.

client.apps.rollback(app_id)

Update and delete

# Partial update — only provided fields change; a new deploy applies them.
client.apps.update(app_id, build_command="pnpm build", env={"NODE_ENV": "production"})

# Delete the app AND tear down its runtime sandbox.
client.apps.delete(app_id)

Configuration changes (update/PATCH) take effect on the next deploy — they don't rebuild the running app automatically.

Runtime environment

A few details worth knowing when your build or start command misbehaves:

  • env applies at build and runtime. Variables you set are exported before the install/build steps and before the app starts. PORT and HOST (0.0.0.0) are injected automatically.
  • Baked-RAM constraint. Firecracker can't change a snapshot's memory at restore, so an app's actual RAM is governed by the base template's baked size, not the per-app memory_mb. For a larger footprint, build a custom template at the size you need and set template.
  • mise layout. Runtimes live under /opt/mise with shims on PATH. Build steps run as non-login shells, so the pipeline exports MISE_DATA_DIR / MISE_CONFIG_DIR / PATH explicitly — and runs mise reshim after install so console scripts installed by pip/npm (uvicorn, gunicorn, …) are found.

Limits

LimitValue
Runtime basebase template — Ubuntu 24.04 + mise (Node 22 LTS, Python 3.12, Go, Bun)
Default size2 vCPU · 1 GiB RAM (cpu / memory_mb on create)
Deploy budget12 minutes per deploy
Deploy modelBlue-green (zero downtime); previous deployment kept for one-click rollback
Idle behaviorScale-to-zero after idle_timeout_seconds (default 15 min), sub-second wake
Auto-deployGitHub push → deploy pinned to the pushed commit

Coming soon

  • Custom domains — bring your own domain with managed TLS (SNI routing). Today apps are served on the stable <app-id>.<suffix> URL.
  • Build-log streaming in the dashboard — the SSE endpoint exists; richer in-dashboard streaming is in progress.

Want one of these sooner? Tell us in GitHub Discussions.

On this page