Everything you need to deploy your first app with Percher.
Percher ships as a CLI — no global install needed. Run it with bunx (Bun) or npx (Node).
# Log in to your Percher account bunx percher login # Initialize in your project directory cd my-app bunx percher init # Deploy bunx percher push
That's it. Your app is live at your-app.percher.run with SSL, builds, and a health check — typically under 60 seconds.
Use these as starting points for [resources] and [data] in your percher.toml. You can always adjust after deploying based on actual usage.
HTML, CSS, and client-side JS. Marketing sites, documentation, portfolios. Served via a lightweight static file server.
[app] name = "my-site" runtime = "node" # or "static" [web] port = 3000 health = "/health" # or "/" for static sites [resources] memory = "128mb" cpu = 0.25
REST or GraphQL endpoints, webhook receivers, microservices. Stateless request-response with no frontend.
[app] name = "my-api" runtime = "node" # or "python"; use "docker" for custom runtimes [web] port = 3000 health = "/health" [resources] memory = "256mb" cpu = 0.5
Server-rendered apps with both frontend and backend. The most common deployment type — includes HTML rendering, API routes, and asset serving.
[app] name = "my-app" runtime = "node" [web] port = 3000 health = "/health" # or "/api/health" [resources] memory = "512mb" cpu = 0.5
CRUD applications, admin panels, dashboards, SaaS apps. Server-rendered frontend with a managed PocketBase database for auth, storage, and data.
[app] name = "my-app" runtime = "node" [web] port = 3000 health = "/health" [resources] memory = "512mb" cpu = 0.5 [data] mode = "pocketbase"
Chat apps, live dashboards, collaborative editors, notification systems. Uses WebSocket or Server-Sent Events for persistent connections.
[app] name = "my-realtime-app" runtime = "node" [web] port = 3000 health = "/health" [resources] memory = "512mb" # more if many concurrent connections cpu = 0.5
Job processors, scheduled tasks, queue consumers. Typically no web frontend — but still needs a health endpoint for Percher's health checks.
[app] name = "my-worker" runtime = "node" # or "python"; use "docker" for custom runtimes [web] port = 3000 # minimal health server health = "/health" [resources] memory = "256mb" # depends on job complexity cpu = 0.5
Data processing apps, ML model serving, Jupyter-backed APIs, scientific computing. Python with numpy, pandas, scikit-learn, or lightweight model inference.
[app] name = "my-ml-app" runtime = "python" [web] port = 8000 # FastAPI/uvicorn default health = "/health" [resources] memory = "1gb" # or more for large models cpu = 1.0
Compiled language services with minimal memory footprint. Ideal for high-throughput, low-latency APIs and utilities.
[app] name = "my-service" runtime = "docker" # Go/Rust via Dockerfile until native labels land [web] port = 8080 # Go default; Rust varies health = "/health" [resources] memory = "128mb" # compiled binaries are very efficient cpu = 0.25
Apps built from a monorepo with multiple packages (e.g., shared libraries + app). Nixpacks builds from the project root.
[app] name = "my-app" runtime = "node" [web] port = 3000 health = "/health" [resources] memory = "512mb" cpu = 0.5
Laravel, Symfony, or vanilla PHP apps. Use a Dockerfile until native PHP runtime labels are added to Percher config.
[app] name = "my-php-app" runtime = "docker" # PHP via Dockerfile until native labels land [web] port = 8080 health = "/health" [resources] memory = "256mb" cpu = 0.5
percher initInitialize a project with percher.tomlpercher create <name> --template nodeCreate a new app from a templatepercher pushDeploy (or redeploy) the current projectpercher push --previewDeploy a preview without replacing livepercher preview promote <id>Promote a preview deploy to livepercher preview discard <id>Discard a preview deploypercher logs [app]View runtime logs (streaming)percher logs [app] --tail 50View last 50 log linespercher env set KEY=VALSet an environment variablepercher env listList environment variables (masked)percher env unset KEYRemove an environment variablepercher data [app]Show PocketBase status, collections, and statspercher doctor [app]Run health and connectivity diagnosticspercher domains add <domain>Add a custom domain and print DNS recordspercher domains verify <domain>Verify domain ownership and routingpercher domains listList custom domainspercher domains remove <domain>Remove a custom domainpercher versionsList deploy history with commit SHAspercher rollback <sha>Roll back to a previous versionpercher whoamiShow current user and token infopercher loginAuthenticate via browser (device flow)percher openOpen the app URL in your browserEnv vars are encrypted at rest (AES-256-GCM) and injected at container startup. Changes take effect on the next deploy — run percher push to apply immediately.
bunx percher env set STRIPE_KEY=sk_live_... bunx percher env list # values are masked bunx percher env unset STRIPE_KEY
Every deploy is stored as a git commit in Percher's internal Forgejo instance. You can list versions and roll back to any previous deploy:
# List all versions bunx percher versions # e950cff deploy: dep_850j... 2026-04-16 # 867d57f Initial commit 2026-04-16 # Roll back bunx percher rollback e950cff
Rollback redeploys the exact code from that version through the same build pipeline. You can also roll back from the Versions tab in the dashboard.
Add this to your project's CLAUDE.md, cursor rules, or paste it when you start a new project. Your AI assistant will know how to build the app so it deploys correctly on Percher.
This app will be deployed on Percher (https://percher.app). ## Percher deployment requirements - The app MUST expose an HTTP server on a configurable port (default 3000) - The app MUST have a health check endpoint that returns HTTP 200, usually GET /health - A percher.toml file must exist in the project root - Percher uses Nixpacks to auto-detect and build — use standard project structures (package.json, requirements.txt, go.mod, etc.) ## Supported runtimes Percher config currently uses runtime = "node", "python", "static", or "docker". Use Dockerfile-based projects for Go, Rust, PHP, Ruby, Java, .NET, Elixir, and other runtimes. ## How to deploy If Percher MCP tools are available (percher_init, percher_push), use those. Otherwise use the CLI: bunx percher init # generates percher.toml bunx percher push # builds and deploys If not authenticated, run: bunx percher login ## percher.toml Generated by percher init. Key fields: [app] name = "my-app" # becomes my-app.percher.run runtime = "node" # auto-detected [web] port = 3000 # the port your app listens on health = "/health" # health check path ## Database For apps that need a database, add to percher.toml: [data] mode = "pocketbase" A managed PocketBase instance is provisioned automatically. Use the POCKETBASE_URL env var to connect from your app. ## Environment variables Set via: bunx percher env set KEY=VALUE Or MCP tool: percher_env_set Available at runtime as standard environment variables. Apps deploy to https://<app-name>.percher.run with automatic SSL.
With this context, you can just say "build me an app that does X" and the agent will structure it correctly for Percher deployment.
When your app crashes, Percher automatically collects logs, redacts secrets, and (if configured) runs AI-powered analysis to explain what went wrong. Crash reports appear as a banner on your app's dashboard page with a copy-paste fix prompt you can hand to your AI assistant.
The watchdog monitors Docker container events in real time. If your app exits with a non-zero code, it queues a crash analysis within seconds. OOM kills (exit code 137) are flagged automatically with memory limit suggestions.
PocketBase is a managed SQLite-backed backend that gives you a REST API, user auth, file storage, and realtime subscriptions — all in one. It runs as a sidecar container alongside your app.
# percher.toml [data] mode = "pocketbase" # That's it. Deploy and PocketBase is ready.
Three env vars are injected automatically:
POCKETBASE_URLInternal Docker URL (server-side calls)POCKETBASE_PUBLIC_URLPublic URL with SSLVITE_POCKETBASE_URLPublic URL (Vite convention)import PocketBase from 'pocketbase'; // Server-side: use internal Docker URL (no SSL overhead) const pb = new PocketBase(process.env.POCKETBASE_URL); // Client-side (browser): use public URL with SSL const pb = new PocketBase(import.meta.env.VITE_POCKETBASE_URL); // or for Next.js: const pb = new PocketBase(process.env.NEXT_PUBLIC_POCKETBASE_URL);
// Create a record
const task = await pb.collection('tasks').create({
title: 'Buy groceries',
done: false,
user: pb.authStore.record?.id,
});
// List with filters
const tasks = await pb.collection('tasks').getList(1, 20, {
filter: 'done = false',
sort: '-created',
});
// Update
await pb.collection('tasks').update(task.id, { done: true });
// Delete
await pb.collection('tasks').delete(task.id);// Sign up
await pb.collection('users').create({
email: 'user@example.com',
password: 'securepassword',
passwordConfirm: 'securepassword',
});
// Log in
const auth = await pb.collection('users').authWithPassword(
'user@example.com',
'securepassword',
);
// auth.token is now set in pb.authStore
// Check auth state
if (pb.authStore.isValid) {
console.log('Logged in as', pb.authStore.record?.email);
}// Upload a file
const formData = new FormData();
formData.append('title', 'My photo');
formData.append('image', fileInput.files[0]);
const record = await pb.collection('posts').create(formData);
// Get file URL
const url = pb.files.getURL(record, record.image);// Subscribe to changes
pb.collection('messages').subscribe('*', (e) => {
console.log(e.action, e.record);
// action: 'create' | 'update' | 'delete'
});
// Unsubscribe
pb.collection('messages').unsubscribe();Your PocketBase admin panel is available at pb-yourapp.percher.run/_/. Use it to create collections, set API rules, and manage data. The admin password is shown once after the first deploy — save it.
percher.toml — full reference[app]
name = "my-app" # 3-40 chars, lowercase, a-z 0-9 and hyphens
runtime = "node" # node | python | static | docker
framework = "nextjs" # optional: nextjs, sveltekit, astro, remix,
# vite, express, fastify, hono, fastapi, flask,
# django, docker
[build]
command = "bun run build" # custom build command (optional)
output = ".next" # build output directory (optional)
[web]
port = 3000 # port your app listens on (1024-65535)
health = "/health" # health check endpoint (default: /)
[resources]
memory = "512mb" # 128mb | 256mb | 512mb | 1gb | 2gb
cpu = 0.5 # 0.25 - 2.0
[data]
mode = "pocketbase" # pocketbase | convex | supabase | none
# mode = "convex"
# convex.deployment_url = "https://your-project.convex.cloud"
# mode = "supabase"
# supabase.url = "https://your-project.supabase.co"
# supabase.anon_key = "eyJ..."
[domain]
custom = "myapp.com" # custom domain (requires DNS setup)
[env]
STRIPE_KEY = "sk_live_..." # environment variables
API_SECRET = "..."
[crons]
cleanup = { schedule = "0 3 * * *", command = "node cleanup.js" }
report = { schedule = "*/15 * * * *", command = "python report.py" }
[dev]
ignore = ["*.log", "tmp/"] # files to ignore in dev mode
debounce = 300 # ms to wait before rebuilding (100-10000)
[required_env]
STRIPE_KEY = "secret" # must be set before deploy
DATABASE_URL = "url" # validates URL format
APP_NAME = "string" # any non-empty stringPercher includes an MCP (Model Context Protocol) server so AI assistants like Claude Code, Cursor, and Windsurf can deploy and manage your apps directly.
// Claude Code: ~/.claude/mcp.json
// Cursor: .cursor/mcp.json
{
"mcpServers": {
"percher": {
"command": "bunx",
"args": ["percher-mcp"]
}
}
}percher_initGenerate percher.tomlDetects framework, runtime, port. Creates config file.percher_pushDeploy the projectPackages files, uploads tarball, triggers build + deploy.percher_logsView app logsArgs: app (name or id), lines (default 50), follow (boolean).percher_env_setSet env variablesArgs: app, vars (object of key-value pairs).percher_env_unsetRemove env variableArgs: app, key.percher_env_listList env variablesArgs: app. Values are masked.percher_versionsDeployment historyArgs: app. Returns commit SHAs and timestamps.percher_rollbackRoll back to versionArgs: app, commitSha. Redeploys exact code from that version.percher_doctorHealth diagnosticsChecks auth, API, container, route, PocketBase, env, deploys.percher_dataDatabase statusShows PocketBase collections, stats, admin link.percher_create_from_templateScaffold new appTemplates: node, python, static, nextjs, hono, express.percher_domain_addAdd custom domainArgs: app, domain. Returns DNS instructions.percher_domain_verifyVerify domain DNSChecks TXT and CNAME/A records.percher_domain_listList custom domainsShows domains with verification status.percher_domain_removeRemove custom domainArgs: app, domain.percher_whoamiCurrent user infoShows email and token status.percher_openGet app URLReturns the public URL for the app.Two options: keep using your Supabase project as an external database, or switch to Percher's managed PocketBase.
# percher.toml [data] mode = "supabase" [data.supabase] url = "https://your-project.supabase.co" anon_key = "eyJ..." # No code changes needed — your existing Supabase client keeps working. # SUPABASE_URL and VITE_SUPABASE_URL are injected automatically.
Concept mapping for AI agents rewriting data layer:
supabase.from('table').select()pb.collection('table').getList()supabase.from('table').insert({})pb.collection('table').create({})supabase.from('table').update({})pb.collection('table').update(id, {})supabase.from('table').delete()pb.collection('table').delete(id)supabase.auth.signUp()pb.collection('users').create({})supabase.auth.signInWithPassword()pb.collection('users').authWithPassword()supabase.auth.getUser()pb.authStore.recordsupabase.storage.upload()pb.collection('x').create(formData)supabase.channel().subscribe()pb.collection('x').subscribe('*', fn)Row Level Security (SQL policies)PocketBase API rules (per collection).select('*, posts(*)')pb.getList({ expand: 'posts' })Not portable: Edge Functions (use API routes instead), Postgres views/triggers (rewrite as app logic), PostGIS (not available), Supabase Vector/embeddings (use an external service).
# percher.toml [data] mode = "convex" [data.convex] deployment_url = "https://your-project.convex.cloud" # CONVEX_URL and VITE_CONVEX_URL are injected automatically. # Your existing Convex client, queries, and mutations keep working.
Concept mapping:
useQuery(api.tasks.list)pb.collection('tasks').getList()useMutation(api.tasks.create)pb.collection('tasks').create({})Convex schema (schema.ts)PocketBase collections (admin UI or API)Convex functions (convex/)API routes in your appConvex auth (clerk/auth0)PocketBase built-in authConvex file storagePocketBase file fieldsRealtime (automatic)pb.collection('x').subscribe('*', fn)Key difference: Convex runs server functions in their cloud. With PocketBase, your API routes run in your app container. Move Convex functions to Express/Hono/Next.js API routes.
Most Vercel projects deploy on Percher with minimal changes.
# 1. Initialize Percher in your Vercel project
cd my-vercel-app
bunx percher init
# Percher auto-detects Next.js/SvelteKit/etc.
# 2. Add a health endpoint (if missing)
# Next.js: create app/api/health/route.ts
# export function GET() { return Response.json({ status: 'ok' }) }
# 3. Move env vars
bunx percher env set DATABASE_URL=...
bunx percher env set STRIPE_KEY=...
# 4. Deploy
bunx percher push
# Your app is live at your-app.percher.run
# Add your custom domain: bunx percher domains add myapp.comEvery app gets a name.percher.run subdomain. You can also add your own domain:
# Add a domain bunx percher domains add myapp.com # Percher returns DNS instructions: # 1. Add TXT record: _percher-challenge.myapp.com -> percher-domain-verification=<token> # 2. Add CNAME: myapp.com → your-app.percher.run # Verify (after DNS propagation) bunx percher domains verify myapp.com # SSL is provisioned automatically via Let's Encrypt.
Each app has a persistent /app/data directory backed by a Docker volume. Data in this directory survives container restarts and redeploys. Use it for SQLite databases, uploaded files, caches, or any state your app needs to persist. The volume is deleted when the app is deleted.