Configuration
Configure your Ploy projects with ploy.yaml
Configuration
Ploy uses a ploy.yaml file in the root of your repository to configure build settings and deployment options. This file provides fine-grained control over how your project is built and deployed.
Quick Start
Create a ploy.yaml file in your repository root:
kind: static
build: npm run build
out: distCommit and push the file to trigger a deployment with your custom configuration.
Configuration Options
kind
Type: "static" | "nextjs" | "dynamic" | "worker"
Required: No (auto-detected if omitted)
Specifies the project type:
static: Static site generation (React, Vue, Angular, HTML, etc.)nextjs: Next.js applications with server-side rendering supportdynamic: Worker-based applications and full-stack runtimesworker: Alias fordynamic
kind: staticIf kind is set to static, the build command is required.
Example - Static React App:
kind: static
build: npm run build
out: distExample - Next.js App:
kind: nextjs
build: npm run build
out: .nextExample - Cloudflare Vite / TanStack Start App:
kind: dynamic
build: pnpm build
out: distproject
Type: string
Required: No
The ID of the Ploy project this directory deploys to. ploy push and ploy git remote use it when you don't pass --project.
kind: worker
project: proj_abc123build
Type: string
Required: Yes (when your project needs a build step)
The command to run to build your project. This is typically a script defined in your package.json.
build: npm run buildCommon examples:
# npm
build: npm run build
# pnpm
build: pnpm build
# yarn
build: yarn build
# Custom script
build: npm run build:productionThe build command is required for static apps and most dynamic apps,
including Vite, TanStack Start, and worker bundles built during deployment.
assets
Type: object
Required: No
Configures uploaded static assets for dynamic projects such as Cloudflare Vite
apps. Set static asset behavior here in ploy.yaml.
assets:
binding: ASSETS
not_found_handling: single-page-application
run_worker_first:
- /api/*
- "!/api/docs/*"Fields:
binding: Optional binding name exposed to your worker. Must be uppercase with underscores.not_found_handling: One ofnone,404-page, orsingle-page-application.run_worker_first: Eithertrue,false, or an array of route patterns.
Use run_worker_first when your worker should handle some requests before the
asset layer. In the example above, /api/* stays worker-first, while
/api/docs/* is served from static assets.
out
Type: string
Required: No (auto-detected if omitted)
Default: Framework-specific (e.g., dist, build, .next)
The relative path to the output directory containing your built files.
out: distValidation rules:
- Must be a relative path (not absolute)
- Cannot start with
/ - Cannot contain
..(path traversal prevention) - Trailing slashes are automatically removed
Common output directories:
| Framework | Default Output |
|---|---|
| Vite | dist |
| Create React App | build |
| Next.js | .next |
| Angular | dist/project-name |
| SvelteKit | build |
| Astro | dist |
Example:
kind: static
build: npm run build
out: distInvalid paths: - out: /dist ❌ (absolute path) - out: ../dist ❌ (path
traversal) - out: dist/ ✅ (trailing slash auto-removed)
base
Type: string
Required: No
Default: Repository root
The base directory where your project is located within the repository. This is useful for monorepos where your deployable project is in a subdirectory.
base: apps/webValidation rules:
- Must be a relative path (not absolute)
- Cannot start with
/ - Cannot contain
..(path traversal prevention) - Trailing slashes are automatically removed
Monorepo example:
my-monorepo/
├── apps/
│ ├── web/ ← Your Next.js app
│ └── api/
├── packages/
│ └── ui/
└── ploy.yamlkind: nextjs
base: apps/web
build: npm run build
out: .nextWhen using base, all paths (like out) are relative to the base directory,
not the repository root.
Monorepos
Ploy detects monorepos automatically — there is no monorepo field. A repository is treated as a monorepo when it contains workspace indicators (pnpm-workspace.yaml, package.json with workspaces, turbo.json, etc.) or when more than one ploy.yaml is present in the repository.
When a monorepo is detected and a project has a base field, Ploy installs dependencies from the repository root (so workspace dependencies resolve) and runs the build command from base.
my-monorepo/
├── apps/
│ ├── web/
│ │ └── ploy.yaml ← Config for web app
│ └── docs/
│ └── ploy.yaml ← Config for docs app
├── packages/
│ └── ui/
└── package.jsonapps/web/ploy.yaml:
kind: nextjs
base: apps/web
build: pnpm build --filter webapps/docs/ploy.yaml:
kind: nextjs
base: apps/docs
build: pnpm build --filter docsEach app's ploy.yaml is a complete configuration on its own — it is not
merged with a root config unless it opts in with extends.
To exclude directories that happen to contain a ploy.yaml (e.g., examples/, templates/, fixtures) from monorepo detection, add a ploy-workspace.yaml at the repository root:
# ploy-workspace.yaml
exclude:
- examples/**
- templates/**node_modules/ is always excluded.
migrations
Type: string
Required: No
Default: migrations/ in the project directory
The directory holding your database migrations. The path is resolved relative to the directory containing ploy.yaml, so it can point outside that directory — useful when several configs (for example, one per region) share the same migrations:
my-app/
├── deploy/
│ ├── eu/
│ │ └── ploy.yaml
│ └── kr/
│ └── ploy.yaml
├── migrations/
│ └── 001_init.sql
└── src/deploy/kr/ploy.yaml:
kind: worker
db:
DB: kr_db
migrations: ../../migrationsValidation rules:
- Must be a relative path (not absolute)
- Must resolve to a directory inside the repository
- Trailing slashes are automatically removed
The directory supports the same layouts as the default migrations/ folder, including per-binding subdirectories.
extends
Type: string
Required: No
Path to another ploy.yaml to inherit from, relative to the file that declares it. Use it to deploy several variants of the same app — for example one per region — that differ only in which resources they bind:
my-app/
├── ploy.yaml # project "my-app"
├── deploy/
│ └── kr/
│ └── ploy.yaml # project "kr"
├── migrations/
│ └── 001_init.sql
└── src/ploy.yaml:
kind: worker
name: my-app
build: pnpm build
db:
DB: main_db
cache:
CACHE: main_cache
cron:
CLEANUP: "0 * * * *"deploy/kr/ploy.yaml:
extends: ../../ploy.yaml
db:
DB: kr_db
cache:
CACHE: kr_cacheThe kr project builds the same app from the repository root, keeps the CLEANUP cron and build command, and binds kr_db and kr_cache instead.
Merge rules (the extending file wins):
-
Single values such as
kind,build,out,main,base,compatibility_date,log_level, andaiare replaced. -
Binding maps (
db,queue,cache,state,fs,sandbox,workflow,cron,timer) are merged per binding. Set a binding tonullto drop an inherited one:extends: ../../ploy.yaml cron: CLEANUP: null # this variant runs no cleanup cron -
Objects and lists (
auth,assets,compatibility_flags,deploy) are replaced as a whole. -
nameanddevare not inherited, so a variant never clashes with its parent's project name or local dev port. The name defaults to the variant's directory (krabove). -
An inherited
migrationspath keeps pointing at the same directory.
App directory: a variant builds from its base when one is set (inherited or its own), otherwise from the directory of the top-most config it extends — the repository root in the example above. Unless migrations is set, migrations are read from migrations/ in the app directory, so every variant applies the same migrations to its own databases.
Validation rules:
- Must be a relative path that stays inside the repository
- A config can extend a config that itself uses
extends, up to 5 levels deep; circular chains are rejected - The merged result must be a valid
ploy.yaml
Resources are scoped to your organization by name. Two projects that bind the same resource name share that resource, so give each variant its own names when the data must stay separate.
To run a variant locally, pass its config from the app directory:
ploy dev -c deploy/kr/ploy.yamlagentSDK
Type: boolean
Required: No
Default: false
Enables the Agent SDK bindings. When set to true, Ploy automatically configures all bindings required by @meetploy/agent-sdk:
ai: true(AI gateway)state: { PLOY_AGENT_STATE: ploy_agent_state }(durable key-value storage)fs: { PLOY_AGENT_FILES: ploy_agent_files }(file storage)workflow: { PLOY_AGENT_WORKFLOW: ploy_agent_run }(durable workflow)timer: { PLOY_AGENT_SCHEDULER: ploy_agent_scheduler }(scheduled tasks)
kind: worker
agentSDK: trueYou can still add extra bindings alongside agentSDK: true — they will be merged with the defaults.
Running ploy dev will warn you if @meetploy/agent-sdk is installed but
agentSDK: true is not set. Running ploy types will automatically add it.
agentRules
Type: boolean
Required: No
Default: true
When ploy dev detects an AI coding agent, it creates or refreshes version-matched instructions in the project's AGENTS.md or CLAUDE.md. These instructions direct the agent to documentation bundled with the installed @meetploy/cli version.
Disable this behavior for a project with:
agentRules: falseskew_protection
Type: boolean
Required: No
Default: true
Controls Skew Protection, which pins a client's
framework-managed requests to the deployment that served its page. Enabled by
default; set to false to opt out.
kind: nextjs
skew_protection: falsedeploy
Type: object
Required: No
Skips deployments for some pushes. Ploy normally deploys every push to every branch.
deploy:
ignored_branches: ^(assets|dependabot)/
skip_markers:
- "[no deploy]"ignored_branches: a regular expression. Pushes to a branch that matches it are not deployed. Patterns with nested repetition such as(a+)+, back-references, or more than two unbounded quantifiers (*,+,{n,}) are rejected.skip_markers: extra commit message markers. A push is not deployed when its latest commit message contains one of them, ignoring case.
These markers skip a deployment even without any configuration: [skip deploy], [deploy skip], [skip ci], [ci skip] and [skip].
git commit -m "docs: fix typo [skip deploy]"Ploy reads deploy from the ploy.yaml at the pushed commit. To apply one set of rules to every project in a repository, put deploy in ploy-workspace.yaml instead. When the pushed commit has no such file, for example on a branch that only holds assets, Ploy reads it from the production branch. You can also set both rules in Project Settings → Preferences → Deploy Rules; all three sources apply. A deployment you start manually from the dashboard ignores these rules.
Complete Examples
Static Vite + React App
kind: static
build: npm run build
out: distNext.js App
kind: nextjs
build: pnpm build
out: .nextMonorepo with Turborepo
Project structure:
my-app/
├── apps/
│ ├── web/ # Next.js frontend
│ └── docs/ # Documentation site
├── packages/
│ └── ui/ # Shared UI components
├── package.json # Root with workspaces
├── turbo.json
└── ploy.yamlploy.yaml for deploying the web app:
kind: nextjs
base: apps/web
build: npm run build
out: .nextCustom Output Directory
kind: static
build: npm run build:prod
out: public/distSPA with Custom Base Path
kind: static
build: npm run build
out: build
base: packages/clientConfiguration Priority
Ploy uses the following priority order for configuration:
ploy.yaml(highest priority) - Explicit configuration file- Dashboard settings - Manual overrides in project settings
- Auto-detection (lowest priority) - Framework detection
Settings in ploy.yaml will override dashboard settings and
auto-detection. This ensures your repository configuration is the source of
truth.
Validation Errors
Common validation errors and how to fix them:
"Build command is required when kind is 'static'"
# ❌ Missing build command
kind: static
out: dist
# ✅ Fixed
kind: static
build: npm run build
out: dist"Out path must be relative, not absolute"
# ❌ Absolute path
kind: static
build: npm run build
out: /dist
# ✅ Fixed - relative path
kind: static
build: npm run build
out: dist"Out path cannot contain '..'"
# ❌ Path traversal
kind: static
build: npm run build
out: ../dist
# ✅ Fixed
kind: static
build: npm run build
out: dist"Base path must be relative, not absolute"
# ❌ Absolute path
kind: static
base: /apps/web
build: npm run build
# ✅ Fixed
kind: static
base: apps/web
build: npm run buildBest Practices
- Commit
ploy.yamlto version control - Keep configuration in sync with code - Use
kind: staticfor most frameworks - Only usenextjsfor Next.js apps requiring SSR - Use relative paths - Never use absolute paths or
..for security - Match your framework's output directory - Check your framework's documentation for the correct
outpath
Troubleshooting
My build is failing
- Check that your
buildcommand works locally:npm run build - Verify the
outdirectory exists after building - Review deployment logs for specific error messages
My monorepo isn't building correctly
- Verify your root
package.jsonhas workspace configuration (or apnpm-workspace.yamlexists) - Check that the
basepath points to the correct app directory - Ensure build commands are run from the base directory
- To exclude unrelated
ploy.yamlfiles (e.g., inexamples/), add aploy-workspace.yamlwith anexclude:list at the repo root
Configuration changes aren't taking effect
- Ensure
ploy.yamlis committed to your repository - Push your changes to trigger a new deployment
- Check deployment logs to see which configuration was used
- Verify there are no YAML syntax errors in your file
Next Steps
- Review the Platform overview for how deployments work
- Learn about Self-Hosting to deploy Ploy on your infrastructure
- Check out the example projects for inspiration
How is this guide?
Last updated on