Ploy
Ploy

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: dist

Commit 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 support
  • dynamic: Worker-based applications and full-stack runtimes
  • worker: Alias for dynamic
kind: static

If kind is set to static, the build command is required.

Example - Static React App:

kind: static
build: npm run build
out: dist

Example - Next.js App:

kind: nextjs
build: npm run build
out: .next

Example - Cloudflare Vite / TanStack Start App:

kind: dynamic
build: pnpm build
out: dist

project

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_abc123

build

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 build

Common examples:

# npm
build: npm run build

# pnpm
build: pnpm build

# yarn
build: yarn build

# Custom script
build: npm run build:production

The 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 of none, 404-page, or single-page-application.
  • run_worker_first: Either true, 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: dist

Validation rules:

  • Must be a relative path (not absolute)
  • Cannot start with /
  • Cannot contain .. (path traversal prevention)
  • Trailing slashes are automatically removed

Common output directories:

FrameworkDefault Output
Vitedist
Create React Appbuild
Next.js.next
Angulardist/project-name
SvelteKitbuild
Astrodist

Example:

kind: static
build: npm run build
out: dist

Invalid 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/web

Validation 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.yaml
kind: nextjs
base: apps/web
build: npm run build
out: .next

When 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.json

apps/web/ploy.yaml:

kind: nextjs
base: apps/web
build: pnpm build --filter web

apps/docs/ploy.yaml:

kind: nextjs
base: apps/docs
build: pnpm build --filter docs

Each 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: ../../migrations

Validation 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_cache

The 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, and ai are replaced.

  • Binding maps (db, queue, cache, state, fs, sandbox, workflow, cron, timer) are merged per binding. Set a binding to null to 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.

  • name and dev are 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 (kr above).

  • An inherited migrations path 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.yaml

agentSDK

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: true

You 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: false

skew_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: false

deploy

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: dist

Next.js App

kind: nextjs
build: pnpm build
out: .next

Monorepo 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.yaml

ploy.yaml for deploying the web app:

kind: nextjs
base: apps/web
build: npm run build
out: .next

Custom Output Directory

kind: static
build: npm run build:prod
out: public/dist

SPA with Custom Base Path

kind: static
build: npm run build
out: build
base: packages/client

Configuration Priority

Ploy uses the following priority order for configuration:

  1. ploy.yaml (highest priority) - Explicit configuration file
  2. Dashboard settings - Manual overrides in project settings
  3. 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 build

Best Practices

  1. Commit ploy.yaml to version control - Keep configuration in sync with code
  2. Use kind: static for most frameworks - Only use nextjs for Next.js apps requiring SSR
  3. Use relative paths - Never use absolute paths or .. for security
  4. Match your framework's output directory - Check your framework's documentation for the correct out path

Troubleshooting

My build is failing

  1. Check that your build command works locally: npm run build
  2. Verify the out directory exists after building
  3. Review deployment logs for specific error messages

My monorepo isn't building correctly

  1. Verify your root package.json has workspace configuration (or a pnpm-workspace.yaml exists)
  2. Check that the base path points to the correct app directory
  3. Ensure build commands are run from the base directory
  4. To exclude unrelated ploy.yaml files (e.g., in examples/), add a ploy-workspace.yaml with an exclude: list at the repo root

Configuration changes aren't taking effect

  1. Ensure ploy.yaml is committed to your repository
  2. Push your changes to trigger a new deployment
  3. Check deployment logs to see which configuration was used
  4. Verify there are no YAML syntax errors in your file

Next Steps

How is this guide?

Last updated on