Skip to main content
Percher is still being built and account creation is paused — get notified when it opens.

percher.toml — full reference

Every field and allowed value

Ask your agent
Set up the config Percher needs for my app.Read the guide at percher.app/docs/percher-toml
Fix my Percher config if something's wrong with it.Read the guide at percher.app/docs/percher-toml
How it works, with exact details

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, preact, solid, express, fastify,
                           # hono, elysia, fastapi, flask, django, eleventy,
                           # hugo, laravel, rails, phoenix, go, rust, dotnet,
                           # java, 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: /)
command = "npm start"      # optional start command; bind servers to 0.0.0.0
visibility = "password"    # "public" (default) | "password" | "private"
                           #   "private" = Percher-login gate (owner + invitees)
password = true            # legacy alias for visibility = "password"
spa = false                # static only: deep links fall back to index.html
rate_limit = { rps = 10, burst = 50 }
                           # optional per-app rate limit, keyed by client IP
                           # (default: unlimited). rps 1-100, burst 1-1000,
                           # rps <= burst <= rps*10. Over the ceiling -> 429.
                           # Needs the platform's rate-limit module (operator).

[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, max=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]
subdomain = "my-app"       # optional requested name.percher.run label
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" }

[[redirects]]              # up to 32 — served at the edge, before your app
from = "/old-path"         # exact path, or a prefix ending in "/*"
to = "/new-path"           # relative path or absolute https:// URL
status = 308               # 301 | 302 | 307 | 308 (default 308)

[headers."/*"]             # response headers per path (16 paths x 16 headers)
X-Custom-Header = "value"  # platform security headers can't be overridden

[dev]
ignore = ["*.log", "tmp/"] # files to ignore in dev mode
debounce = 300             # ms to wait before rebuilding (100-10000)
auto_open = true            # automatically open the dev URL

[required_env]
STRIPE_KEY = "secret"      # must be set before deploy
DATABASE_URL = "url"       # validates URL format
APP_NAME = "string"        # any non-empty string

[addons.pocketbase]
enabled = true

[addons.cron]
enabled = true

[addons.storage]
mode = "volume"            # pocketbase | volume | s3
max_size = "1gb"           # optional; plan enforcement applies

[checks.ready]
path = "/health"
interval = 30              # seconds, integer >= 1
timeout = 5                # seconds, integer >= 1
initial_delay = 10         # seconds, integer >= 0

Complete field index

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 nameOptional requested percher.run subdomain.
domain.customhostname without https://Optional custom domain; DNS setup is still required.
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 expressionRequired schedule for a named task.
crons.<name>.commandnon-empty stringRequired command for a named task.
crons.<name>.descriptionup to 200 charsOptional plain-language dashboard description.
addons.pocketbase.enabledbooleanExplicitly enable or disable the PocketBase add-on.
addons.cron.enabledbooleanExplicitly enable or disable cron support.
addons.storage.modepocketbase | volume | s3Storage add-on mode.
addons.storage.max_sizestringOptional storage size request; plan enforcement applies.
checks.<name>.pathnon-empty pathRequired path for a named health check.
checks.<name>.intervalinteger ≥ 1Optional check interval in seconds.
checks.<name>.timeoutinteger ≥ 1Optional timeout in seconds.
checks.<name>.initial_delayinteger ≥ 0Optional delay before the first check.
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.

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