platform.yaml reference
The manifest is optional — a bare source tree is detected by its files. Add platform.yaml when you need to choose a runtime, pin a version, attach a database or change how the app starts.
The file at a glance#
Put platform.yaml at the root of your source. Every key is optional; unknown keys are rejected so a typo fails the deployment instead of being silently ignored.
runtime: auto # auto | static | node | python | php
version: "22" # runtime release: "22" | "22.12" | "22.12.1" — always quote it
port: 3000 # pin the listen port (default: allocated for you, exposed as $PORT)
site_dir: . # static only — folder to serve
spa: true # static only — fall back to /index.html for unknown paths
docroot: public # php only — web root inside the source
install_cmd: null # override the install step
build_cmd: null # override the build step (node)
start_cmd: null # override the start command
env: {KEY: value} # static environment, committed with the source
s3: true # inject S3_* with your personal bucket's credentials
databases: [main] # attach project databases → DB_MAIN_HOST / PORT / USER / PASSWORD / NAME
resources: {cpu: "1.0", memory: 512m}
volumes: [{name: data, path: /app/data}]
Key reference#
| Key | Type · limits | Default | Applies to |
|---|---|---|---|
runtime |
auto, static, node, python, php |
auto — detect from files |
all |
version |
string of 1–3 dot-separated numbers, ≤ 24 chars | per runtime, see versions | all |
port |
integer 1–65535 | allocated from 10000–19999 per daemon | all |
site_dir |
relative path, no .. |
. |
static |
spa |
boolean | false |
static |
docroot |
relative path, no .. |
public |
php |
install_cmd |
string ≤ 2000 chars | runtime default | all |
build_cmd |
string ≤ 2000 chars | npm run build if the script exists |
node |
start_cmd |
string ≤ 2000 chars | runtime default | node, python |
env |
map; keys [A-Za-z_][A-Za-z0-9_]*, values become strings |
{} |
all |
s3 |
boolean | false |
all |
databases |
list of ≤ 16 names [a-z0-9][a-z0-9_-]{0,39} |
[] |
all |
resources.cpu |
number 0.01–64, as a string |
platform default | all |
resources.memory |
digits + b, k, m or g (512m) |
platform default | all |
volumes |
≤ 8 × {name, path}; name [a-z0-9][a-z0-9._-]*, path absolute, no .. |
none | all |
port and $PORT#
The platform picks a free port in your daemon and gives it to your app as the PORT environment variable. Your app must listen on 0.0.0.0:$PORT. Set port: only when something needs a fixed number; if that port is taken in your daemon the deployment is refused.
resources#
resources limits this app's container. If you leave it out, the platform's default limits (set by an admin) apply. Use it to give a heavy build more memory — or to keep a small site small.
volumes#
volumes:
- name: uploads
path: /app/uploads
A volume is a named Docker volume in your daemon, mounted at path. It persists across deploys and recreates and is included in project backups. Everything outside a volume is thrown away with the container.
env, s3 and databases#
env is the lowest-effort place for non-secret constants. Real secrets belong in project variables. s3: true and databases: [name] make the platform inject credentials as S3_* and DB_<NAME>_* variables — see Databases and File storage.
Create the database first
databases: [main] only injects variables for a database that already exists in the project. If it does not, deployment still succeeds but the DB_MAIN_* variables are missing. Create the database, then deploy or recreate.
The four runtimes#
static
- base
nginx:<v>-alpine(1.27)- detected by
index.html- serves
site_dirvia nginx- extras
spa: true→ fallback to/index.html; gzip on
node
- base
node:<v>-alpine(24 LTS)- detected by
package.json- install
npm ci, falling back tonpm install- start
npm run start, elseserver.js/index.js/dist/main.js
python
- base
python:<v>-slim(3.12)- detected by
requirements.txt,pyproject.toml- install
pip install -r requirements.txtorpip install .- start
start_cmd, elseuvicorn main:app/app.main:app
php
- base
php:<v>-fpm-alpine(8.5) + nginx + supervisor- detected by
composer.json,public/index.php- install
composer install --no-devwhen there is acomposer.jsonand novendor/- serves
docrootvia fpm on a unix socket
Examples#
runtime: static
site_dir: dist # the folder your build produced and you committed or archived
spa: true # client-side routing: unknown paths return index.html
runtime: node
version: "24"
install_cmd: npm ci --include=dev # the build below needs devDependencies (tsc)
databases: [main]
s3: true
resources: {memory: 512m}
{ "scripts": { "build": "tsc -p .", "start": "node dist/main.js" } }
runtime: python
version: "3.12"
start_cmd: uvicorn app.main:app --host 0.0.0.0 --port $PORT
databases: [main]
fastapi
uvicorn[standard]
psycopg[binary]
runtime: php
version: "8.5"
docroot: public
volumes:
- {name: storage, path: /var/www/app/storage}
NODE_ENV=production while installing
The generated Dockerfile sets NODE_ENV=production before npm ci, so npm skips devDependencies. If your build step needs them (TypeScript, Vite, a CLI), either move those packages into dependencies or set install_cmd: npm ci --include=dev.
Runtime versions#
A project chooses its runtime version, not only its runtime. The platform builds the Docker tag from a validated string, so node + "22" becomes node:22-alpine.
How a version is chosen#
- platform.yaml
version: "22"— always wins - Source hintan exact or major version in a file you already have
- Platform defaultnode 24 · python 3.12 · php 8.5 · nginx 1.27
Recognised hint files (only a plain exact or major version — optionally with a leading v — counts; ranges like >=18 are ignored and mentioned in the build log):
| Runtime | Hint files |
|---|---|
| node | .nvmrc, .node-version, package.json → engines.node |
| python | .python-version |
| php | composer.json → require.php |
Accepted values#
22, 22.12 or 22.12.1 — one to three numbers separated by dots, at most 24 characters, digits only. Quote it.
YAML turns 3.10 into 3.1
Unquoted version: 3.10 is read by YAML as the number 3.1, which silently drops the zero. The platform therefore rejects floats and tells you to write version: "3.10". An unquoted whole number such as version: 22 is fine. Booleans are rejected too.
Fresh or pinned?#
Official Docker tags move — node:22-alpine is whatever 22.x is newest today. The platform handles that explicitly:
| You wrote | Behaviour |
|---|---|
"22" or "22.12" (short) |
Floating. The base image is pulled again on every deploy, so a rebuild picks up security fixes. If the registry cannot be reached the build continues from the local copy and writes a warning to the log. |
"22.12.1" (three parts) |
Pinned. Pulled only if it is not on the daemon yet — reproducible rebuilds. |
Every build logs # base image: <ref> (<digest>), and the App tab shows the base image in use.
Defaults, and changing them#
| Runtime | Default | Note |
|---|---|---|
| node | 24 | the newest Node.js LTS when the default was set |
| python | 3.12 | |
| php | 8.5 | |
| static (nginx) | 1.27 |
Administrators can override a default for the whole platform with PLATFORM_DEFAULT_NODE_VERSION, …_PYTHON_VERSION, …_PHP_VERSION and …_STATIC_VERSION.
A new default moves existing apps
Apps that do not set version follow the default. When it changes, they move to the new version on their next rebuild — already-built images keep running until then. Pin version in anything that must not move.