All pages
Deploy
One call takes a directory to a URL. The platform decides what the directory is, builds it, runs it, and only lets it take traffic once it has answered a health check.
The one call
pilot deploy # in the app's directory
pilot deploy ./apps/web --app shop # a subdirectory, named
Do not write a Dockerfile or a compose file first. Write one only when the platform says it does not recognise the directory, and the error that says so carries everything needed to write it.
- The directory is tarred, with
.dockerignorehonoured and.gitskipped, and sent to the fleet. - The fleet answers with a plan: one step per service, each saying where it came
from. First hit wins, in this order: a compose file, then a
Dockerfile, then a framework recipe, thenunknown. - Each step is built inside a build machine, the service is created or updated, and the release is deployed behind the health gate.
- The answer carries the app, and for each service its name, its URL and its release.
To see the plan without building it, call the plan tool over MCP
or POST /v1/plan directly. It stops after the second step and says what the
platform thinks your repository is.
The flags worth knowing: --file points at a compose file
somewhere other than the directory root, --app overrides the app name the plan
derives, --env adds to the interpolation environment, and --no-wait
returns as soon as each deploy is accepted rather than waiting on the health gate.
pilot deploy --file deploy/compose.yaml
pilot deploy --env API_BASE=https://example.test
pilot deploy -c --no-wait # for a script or an agent
What is detected
A directory that matches one of these needs no configuration from you. Every recipe sets the port to 8080, exposes it, and reads it from the environment, and the router dials that port.
| Signal in the directory | Framework | Health check |
|---|---|---|
package.json with any @webjsdev/* dependency
| webjs |
/__webjs/ready
|
next.config.* and a lockfile
| Next.js |
/
|
react-router.config.* or remix.config.*
| React Router |
/
|
package.json with a bare remix dependency
| Remix |
/
|
vite.config.*
| Vite, served as static files |
/
|
manage.py with requirements.txt or pyproject.toml
| Django |
/
|
main.py or app.py importing FastAPI
| FastAPI |
/
|
Gemfile with bin/rails
| Rails |
/up
|
go.mod
| Go |
/
|
Cargo.toml
| Rust |
/
|
composer.json with artisan
| Laravel |
/
|
Next.js, with the adapter
Next defines a deployment adapter interface and pilots implements it. The adapter ships inside the JavaScript client, so install that first (API and SDKs).
npm i @pilots/sdk
Setting adapterPath: '@pilots/sdk/next' in next.config.js, or
NEXT_ADAPTER_PATH=@pilots/sdk/next with no config change at all, makes the build
produce a standalone server, so the image carries the traced server rather than the whole
repository. Without it the generic recipe still works and the image is simply much
larger.
When you write the Dockerfile
Two rules, and both mistakes build cleanly and then answer with a bad gateway, which is why they are worth stating rather than discovering.
- Bind
0.0.0.0, never127.0.0.1. The loopback address is right in exactly one place, aHEALTHCHECKprobe, which runs inside the guest. - Read the port from the environment, with 8080 as the fallback.
FROM node:24-slim
WORKDIR /app
COPY . .
RUN npm ci --omit=dev
ENV PORT=8080
EXPOSE 8080
CMD ["node", "server.js"]
Monorepos
A root package.json with workspaces deploys each workspace
directory as a service named after it, built from the repository root so the install stays
where the lockfile and the hoisted modules are. A workspace no recipe recognises is
skipped and named in the notes.
Any other layout is unknown at the root. Describe it with a compose file, one build context per app, and deploy that instead (compose files).
Every answer, and what to do
| Answer | What it means | Do |
|---|---|---|
| A URL per service | Deployed and healthy. | Nothing. That is the URL, and it will not change. |
unknown_framework
| No compose file, no Dockerfile, no recipe. | Read the listing and manifests in the answer, write a Dockerfile obeying the two rules, and deploy with it. |
plan_unsupported
| The compose file asks for something pilots does not do. | Every unsupported key is named in one answer. Fix them all in one pass. |
plan_multi_service
| A push tried to deploy a repository holding more than one app. | Commit a compose file and deploy that. |
build_failed
| The build stopped. | Every log line is in the error. Read the one carrying the failure, fix it, build again. |
health_gate_failed
| The app never answered its health check. | Diagnose the replica named in the answer. It is usually the port or the bind address, and deploying again changes nothing. |
quota_exceeded
| The organization is at a ceiling. | The answer names which limit. Free something, or ask an admin to raise it. |