Skip to main content
PercherPercher

Documentation

Everything you need to deploy your first app with Percher.

Quick start

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.

Recommended configurations by app type

Use these as starting points for [resources] and [data] in your percher.toml. You can always adjust after deploying based on actual usage.

Static site / landing page

Minimal

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
  • For pure static sites, use a minimal HTTP server like serve or http-server
  • No database needed — skip [data] entirely
  • 128mb is plenty since there's no server-side processing
  • Frameworks: Astro (static), Vite, 11ty, Hugo (with Node server wrapper)

API service / webhook handler

Light

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
  • Go and Rust APIs can often run with 128mb due to lower memory overhead
  • For Python (Flask, FastAPI): 256mb minimum, 512mb if using pandas/numpy
  • Add [data] mode = "pocketbase" if you need persistent storage
  • Set env vars for external service keys: bunx percher env set STRIPE_KEY=...
  • Frameworks: Express, Fastify, Hono, Flask, FastAPI, Gin, Actix, Fiber

Fullstack web app (SSR)

Standard

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
  • Next.js, Remix, SvelteKit, Nuxt, and Astro SSR all work out of the box
  • 512mb handles SSR rendering, API routes, and typical traffic well
  • For apps with image processing or heavy SSR: consider 768mb
  • Next.js: ensure standalone output mode or standard start script in package.json
  • Health endpoint: most frameworks serve / successfully which works as a health check

Fullstack app with database

Standard + Data

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"
  • PocketBase provides: SQLite database, REST API, auth, file storage, realtime subscriptions
  • Your app receives POCKETBASE_URL (internal) and POCKETBASE_PUBLIC_URL (public with SSL)
  • For Vite-based frontends, VITE_POCKETBASE_URL is also injected automatically
  • PocketBase admin UI is at pb-<app-name>.percher.run/_/
  • The PocketBase sidecar runs in its own container with 128mb — separate from your app's resources
  • Good for: todo apps, blogs, CMS, user dashboards, internal tools, MVPs

Real-time application

Standard

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
  • WebSocket and SSE connections are supported through the Caddy reverse proxy
  • Memory scales with concurrent connections — estimate ~1mb per active WebSocket connection
  • For 500+ concurrent connections: increase to 768mb or 1gb
  • If using PocketBase realtime subscriptions, add [data] mode = "pocketbase"
  • Frameworks: Socket.io, ws, Hono WebSocket, Fastify WebSocket

Background worker / queue processor

Light

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
  • Your worker MUST expose an HTTP health endpoint — Percher needs it to verify the container is running
  • Pattern: run your job processor alongside a minimal HTTP server on port 3000 that returns 200 on /health
  • For CPU-intensive batch jobs: increase cpu to 1.0
  • For memory-intensive processing (large files, data transforms): increase memory to 512mb or 1gb
  • Use env vars for queue connection strings: bunx percher env set REDIS_URL=...

Python data app / ML inference

Heavy

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
  • Python apps with numpy/pandas need at least 512mb — 1gb recommended
  • For model inference (scikit-learn, small transformers): 1gb minimum
  • Large language models or image models likely exceed Percher's resource limits — use a dedicated GPU service
  • FastAPI with uvicorn: use port 8000, set in percher.toml [web] port = 8000
  • Flask: default port 5000, update accordingly
  • requirements.txt or pyproject.toml is auto-detected by Nixpacks
  • Percher has a 30-second health check timeout — ensure your app starts within that window

Go or Rust microservice

Minimal

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
  • Go and Rust binaries are extremely memory-efficient — 128mb handles most workloads
  • For services processing large payloads or many concurrent requests: 256mb
  • Use a Dockerfile for Go/Rust projects until native runtime labels are added to percher.toml
  • Build a small final image with a multi-stage Dockerfile for best cold start and disk usage
  • Rust build times can be long (2-5 min) — prefer release binaries in the final stage
  • Common ports: Go stdlib net/http defaults to 8080, Actix/Axum default varies — set explicitly

Monorepo / multi-package app

Standard

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
  • Percher uploads and builds from the project root — all workspace packages are included
  • Ensure your start script in package.json builds and starts the right package
  • For Turborepo/Nx: the start script should handle the build chain
  • If using workspace dependencies, ensure they're built before the app starts
  • Larger monorepos may need more memory during the build phase (this doesn't affect runtime)

PHP application

Standard

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
  • Use a Dockerfile for PHP projects until native PHP runtime labels are added to percher.toml
  • Laravel: ensure APP_KEY is set via env vars, port matches artisan serve
  • For Laravel with database: add [data] mode = "pocketbase" or set DATABASE_URL env var to an external DB
  • PHP built-in server: php -S 0.0.0.0:8080 -t public
  • Memory: 256mb for most PHP apps, 512mb for Laravel with queues or heavy ORM usage

CLI reference

percher initInitialize a project with percher.toml
percher create <name> --template nodeCreate a new app from a template
percher pushDeploy (or redeploy) the current project
percher push --previewDeploy a preview without replacing live
percher preview promote <id>Promote a preview deploy to live
percher preview discard <id>Discard a preview deploy
percher logs [app]View runtime logs (streaming)
percher logs [app] --tail 50View last 50 log lines
percher env set KEY=VALSet an environment variable
percher env listList environment variables (masked)
percher env unset KEYRemove an environment variable
percher data [app]Show PocketBase status, collections, and stats
percher doctor [app]Run health and connectivity diagnostics
percher domains add <domain>Add a custom domain and print DNS records
percher domains verify <domain>Verify domain ownership and routing
percher domains listList custom domains
percher domains remove <domain>Remove a custom domain
percher versionsList deploy history with commit SHAs
percher rollback <sha>Roll back to a previous version
percher whoamiShow current user and token info
percher loginAuthenticate via browser (device flow)
percher openOpen the app URL in your browser

Environment variables

Env 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

Versions and rollback

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.

Deploy instruction for AI agents

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.

Crash diagnostics

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 — database, auth & files

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.

Setup

# 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 SSL
VITE_POCKETBASE_URLPublic URL (Vite convention)

Connecting from your app

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);

CRUD operations

// 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);

Auth

// 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);
}

File uploads

// 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);

Realtime subscriptions

// Subscribe to changes
pb.collection('messages').subscribe('*', (e) => {
  console.log(e.action, e.record);
  // action: 'create' | 'update' | 'delete'
});

// Unsubscribe
pb.collection('messages').unsubscribe();

Admin UI

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 string

MCP tools for AI assistants

Percher includes an MCP (Model Context Protocol) server so AI assistants like Claude Code, Cursor, and Windsurf can deploy and manage your apps directly.

Setup

// Claude Code: ~/.claude/mcp.json
// Cursor: .cursor/mcp.json
{
  "mcpServers": {
    "percher": {
      "command": "bunx",
      "args": ["percher-mcp"]
    }
  }
}

All tools

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.

Migrating from Supabase

Two options: keep using your Supabase project as an external database, or switch to Percher's managed PocketBase.

Option 1: Keep Supabase (easiest)

# 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.

Option 2: Switch to PocketBase

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.record
supabase.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).

Migrating from Convex

Option 1: Keep Convex (easiest)

# 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.

Option 2: Switch to PocketBase

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 app
Convex auth (clerk/auth0)PocketBase built-in auth
Convex file storagePocketBase file fields
Realtime (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.

Migrating from Vercel

Most Vercel projects deploy on Percher with minimal changes.

What works out of the box

  • Next.js (Pages Router and App Router)
  • SvelteKit, Remix, Nuxt, Astro
  • Static sites (Vite, React, Vue)
  • API routes (Express, Hono, Fastify)
  • Environment variables
  • Custom domains

What needs changes

Vercel Serverless FunctionsRun as a normal Node.js server (Express/Hono) — no cold starts
Vercel Edge FunctionsNot supported — use standard API routes
Vercel KV / Postgres / BlobUse PocketBase (mode = "pocketbase") or external service
vercel.json rewrites/redirectsHandle in your app code or framework config
Vercel Cron JobsUse [crons] in percher.toml
Vercel AnalyticsUse a third-party analytics service
ISR / On-demand RevalidationWorks — Next.js ISR runs in the container

Steps

# 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.com

Custom domains

Every 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.

Persistent app data

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.

Percher — AI-native app hosting