percher.toml — full reference
Every field and allowed value
Validation is strict — unknown keys in any section (e.g. a typo like [build] start = "..." when only command, output, and pass_env are accepted) fail the parse instead of being silently dropped. Run percher doctor to surface validation issues before you publish.
[app]
name = "my-app" # 3-40 chars, lowercase, a-z 0-9 and hyphens
runtime = "node" # node | bun | python | static | docker
framework = "nextjs" # optional: nextjs, sveltekit, astro, remix,
# nuxt, vite, express, fastify, hono, elysia,
# fastapi, flask, django, docker
[build]
command = "bun run build" # custom build command (optional)
output = ".next" # build output directory (optional)
pass_env = [ # opt-in: expose these env vars at BUILD time
"NEXT_PUBLIC_API_URL", # (Vite/Next/Astro/Expo bake *_PUBLIC_* into
"VITE_PB_URL", # the bundle, so they need build-time access)
] # Values come from "percher env set", never
# from this TOML — only key names live here.
[web]
port = 3000 # port your app listens on (1024-65535)
health = "/health" # health check endpoint (default: /)
password = true # optional: password-gate via SITE_PASSWORD env
[resources] # recommended sizing — see "Pools are measured" below
memory = "512mb" # 256mb | 512mb | 1gb | 2gb
cpu = 0.5 # 0.25 - 2.0
instances = 2 # Phase 6.1 — static N containers. Plan-gated
# (free=1, starter=1, maker=2, pro=4).
# Mutually exclusive with [resources.autoscale].
# Or: CPU-based autoscaling (Phase 6.3)
[resources.autoscale]
min = 1 # initial + floor (plan-clamped)
max = 4 # ceiling (plan-clamped)
# Optional fine-tuning. Most users leave these at defaults:
# scale_up_cpu_percent = 80
# scale_up_sustain_seconds = 120
# scale_down_cpu_percent = 20
# scale_down_sustain_seconds = 600
# cooldown_seconds = 180
[data]
mode = "pocketbase" # pocketbase | postgres | sqlite | convex | supabase | external | none
# mode = "convex"
# convex.deployment_url = "https://your-project.convex.cloud"
# mode = "supabase"
# supabase.url = "https://your-project.supabase.co"
# supabase.anon_key = "eyJ..."
# mode = "sqlite"
# file = "data.db" # required when mode = "sqlite"
[domain]
custom = "myapp.com" # custom domain (requires DNS setup)
[env] # legacy KEY=VALUE shape (still supported)
STRIPE_KEY = "sk_live_..." # environment variables
API_SECRET = "..."
# Or, going forward (FUTURE12 Phase 6 — preview):
# [env]
# required = ["OPENAI_API_KEY"] # must exist before deploy queues
# optional = ["SENTRY_DSN"] # may be referenced; not required
# ignore = ["NODE_ENV"] # explicitly ignored by the env scanner
[crons]
# description is optional — it's shown on the dashboard so anyone can see what the job is for
cleanup = { schedule = "0 3 * * *", command = "node cleanup.js", description = "Delete records older than 90 days" }
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 stringPools are measured, not reserved
memory and cpu are recommended sizing — the per-container cap your app runs inside, not a standing reservation. In steady state your account pool (memory / CPU / disk) is charged on measured usage, so running apps occupy only what they actually use and a light idle app barely touches the pool. That's what lets one account hold many small apps or a single heavy one.
The cap isn't entirely free, though. When an app is deployed, woken, or resized, its requested cap is what counts against the pool for that admission check, and an app with no fresh sample (new, stale, or mid-restart) falls back to its cap until it's measured again. RAM has an extra hard limit: your apps' committed caps can total at most 2× your pool.
Pick a cap that comfortably fits your app's real footprint (see the recommended configurations by app type), then watch each app's measured usage on its Resources tab and your account-wide totals on the Account page. Too low risks an OOM-kill. Much higher than the app needs is harmless for steady-state pooling, but it does make deploy/wake/resize admission and the 2× RAM guard stricter.