percher.toml — full reference
Every field and allowed value
How it works, with exact details
percher.toml is the one config file in your project — it tells Percher what your app is called, how to build it, and how to run it. Most apps need only these lines; everything else has a sensible default:
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.
The full annotated example — every section
Complete field index — every field and allowed value
This is the schema contract. Dynamic names such as crons.<name> and headers.<path>.<name> are chosen by you.
app.namerequired; 3–40 charsLowercase public app name; starts with a letter and uses a–z, 0–9, or hyphens.app.runtimenode | bun | python | static | dockerRequired runtime/build path.app.frameworknextjs | sveltekit | astro | remix | nuxt | vite | preact | solid | express | fastify | hono | elysia | fastapi | flask | django | eleventy | hugo | laravel | rails | phoenix | go | rust | dotnet | java | dockerOptional detection override; normally leave it out.build.commandstringOptional custom build command; not allowed with runtime = static.build.outputrelative pathOptional directory containing build output.build.pass_envup to 50 key namesEncrypted runtime env keys also exposed during build; values never belong in TOML.web.port1024–65535Container port. Required when a [web] table is present; otherwise the platform resolves 3000.web.healthpath stringHealth-check path; defaults to /.web.command1–1000 charsContainer start command, especially for Python; bind to 0.0.0.0.web.visibilitypublic | password | privateURL access policy. Public is the default.web.passwordbooleanLegacy alias: true means visibility = password.web.spabooleanStatic-only fallback from deep links to index.html.web.rate_limit.rpsinteger 1–100Sustained requests-per-second target.web.rate_limit.burstinteger 1–1000Ceiling; must be between rps and 10 × rps.data.modepocketbase | postgres | sqlite | convex | supabase | external | noneApp data integration.data.filenon-empty pathRequired when data.mode = sqlite.data.convex.deployment_urlURLRequired when data.mode = convex.data.supabase.urlURLRequired when data.mode = supabase.data.supabase.anon_keynon-empty stringOptional public Supabase anon key; keep privileged keys in env.domain.subdomainvalid app nameParsed but not applied; the app name sets the percher.run subdomain (change it with percher rename <name>).domain.customhostname without https://Parsed but not applied; add a custom domain with percher domains add <domain>.resources.memory256mb | 512mb | 1gb | 2gbPer-container memory cap used for admission.resources.cpu0.25–2.0Per-container CPU cap used for admission.resources.instancesinteger 1–4Static instance count, plan-capped and incompatible with autoscale.resources.autoscale.mininteger 1–4Autoscaling floor; must not exceed max.resources.autoscale.maxinteger 1–4Autoscaling ceiling, plan-capped.resources.autoscale.scale_up_cpu_percentinteger 1–100Optional scale-up CPU threshold.resources.autoscale.scale_up_sustain_seconds30–3600Seconds above the scale-up threshold.resources.autoscale.scale_down_cpu_percentinteger 1–100Optional scale-down threshold; lower than scale-up.resources.autoscale.scale_down_sustain_seconds60–86400Seconds below the scale-down threshold.resources.autoscale.cooldown_seconds30–3600Minimum delay between scaling actions.env.<KEY>legacy string valueBackward-compatible seed value. Prefer the contract below and percher env set for real secrets.env.requiredUPPER_SNAKE_CASE[]Keys that must exist before deploy.env.optionalUPPER_SNAKE_CASE[]Referenced keys that may be absent.env.ignoreUPPER_SNAKE_CASE[]References intentionally ignored by the scanner; a key may appear in only one env list.required_env.<KEY>secret | url | stringOlder typed requirement map; validated before deploy.dev.ignorestring[]File patterns ignored by percher dev.dev.debounce100–10000 msDelay before percher dev rebuilds.dev.auto_openbooleanOpen the URL automatically in dev mode.crons.<name>.schedulecron expression (UTC)Required schedule for a named task, evaluated in UTC.crons.<name>.commandnon-empty stringRequired command for a named task.crons.<name>.descriptionup to 200 charsOptional plain-language dashboard description.addons.pocketbase.enabledbooleanReserved; no effect today. Use data.mode = pocketbase.addons.cron.enabledbooleanReserved; no effect today. Declare jobs under [crons].addons.storage.modepocketbase | volume | s3Reserved; no effect today.addons.storage.max_sizestringReserved; no effect today.checks.<name>.pathnon-empty pathRequired by the parser but not used; the probe uses web.health.checks.<name>.intervalinteger ≥ 1Accepted but not used.checks.<name>.timeoutinteger ≥ 1Read only under checks.health: seconds the deploy waits for the container to become healthy (default 30, max 300). Other names have no effect.checks.<name>.initial_delayinteger ≥ 0Read only under checks.health: seconds before failed probes count (default 5, max 120). Other names have no effect.redirects[].fromexact /path or /prefix/*Source path; unique, no whitespace or placeholders.redirects[].to/path or https:// URLDestination; cannot equal from.redirects[].status301 | 302 | 307 | 308HTTP status; defaults to 308. At most 32 rules.headers.<path>.<name>stringCustom response header: up to 16 paths and 16 headers per path; platform security/transport headers are reserved.How redirects and headers are served (at the edge)
Caddy answers a matching redirect before the request reaches your container — on password-protected apps, before the login gate too. Response headers are applied when the response is written, so a header set here wins over the same header set by your app.
Platform-managed headers (Strict-Transport-Security, X-Frame-Options, X-Content-Type-Options, Referrer-Policy, Permissions-Policy, and transport headers) are rejected at parse time. Prefix rules ("/docs/*") send every matched path to the same to — no capture groups in v1.
percher import maps vercel.json redirects/headers and Netlify _redirects / _headers into these sections.
What the CLI uploads, and .percherignore
percher publish and percher push leave out node_modules, .git, dist, .next, .svelte-kit, build, coverage, .env, .env.*, .bun, *.log, .DS_Store, plus whatever .gitignore excludes.
A .percherignore in the project root (same syntax as .gitignore) drops more files from the upload, and a ! line re-includes one — for example !/dist for a static site whose [build] output is dist. .env files can never be re-included; set secrets with percher env set.
How [resources] counts against your account pool
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.