Appearance
apco.yml manifest
apco.yml is the project manifest (schema version 2). It is validated by apco manifest validate, and a normalized snapshot—including source or OCI provenance—is stored with every deploy. Create one with apco init.
Schema
Top level
| Field | Type | Required | Default | Description |
|---|---|---|---|---|
version | 2 | no | — | Manifest schema version; only 2 is accepted when present |
app | object | yes | — | The application block (below) |
source | object | no | { rootDirectory: ".", watchPaths: [] } | Monorepo app root and automatic GitHub deploy filters (below) |
image | object | for dockerimage | — | Existing OCI image reference; invalid for source project types (below) |
release | object | no | — | One-shot runnable-app release command executed before traffic activation (below) |
jobs | array | no | [] | Manifest-defined runnable-app scheduled jobs, normalized and pinned to exact deployment scopes (below) |
env | object | no | — | env.files: string[] — reserved for future use (accepted, currently unused) |
database | object | no | — | Managed database block (below) |
dev | object | no | — | Local-dev overrides for apco dev (below) |
app
| Field | Type | Required | Default | Description |
|---|---|---|---|---|
app.name | string | yes | — | App name; used as the project name and, for auto slugs, as the slug seed |
app.slug | string | no | auto | Subdomain: https://<slug>.apco.space. The literal "auto" (or omitting the field) is a sentinel meaning "the server assigns a unique slug on first deploy". Explicit slugs are sanitized client-side (lowercased, non-[a-z0-9-] collapsed to -, trimmed, max 30 chars) and validated server-side: 3–30 chars of lowercase [a-z0-9-]; platform names and the dev- prefix are reserved |
app.type | auto | static | ssr | dockerimage | no | auto | Deployment type (resolution rules below). Existing images must select dockerimage explicitly; auto never infers it |
app.port | positive int | for dockerimage | 3000 for SSR | Runnable app port. Source SSR apps receive it as PORT; an existing image must already listen on the declared port |
app.buildCommand | string | no | — | Overrides the detected build command for source (Railpack) builds |
app.startCommand | string | no | — | Overrides the detected runtime command for source (Railpack) builds |
app.publishDirectory | string | no | — | Static: the folder containing index.html (e.g. dist). Also used as a static-type hint by auto resolution |
app.packageManager | bun | pnpm | npm | yarn | no | — | Forces the toolchain for source (Railpack) builds. Overrides both package.json's "packageManager" field and lockfile detection. Footgun: Railpack's lockfile detection prefers pnpm-lock.yaml over bun.lock when both exist — set this field to be explicit |
app.health.path | string | no | / | Health-check path used for candidate activation and apco verify |
app.health.timeoutSeconds | positive int | no | 60 | Candidate/verification health-check timeout |
image
| Field | Type | Required | Default | Description |
|---|---|---|---|---|
image.reference | OCI reference | yes for dockerimage | — | Publicly routed registry/repository plus an explicit tag or lowercase sha256 digest. Docker Hub shorthand is canonicalized. Tags are resolved once and the immutable repository digest is stored with the deployment. |
dockerimage rejects source, dev, buildCommand, startCommand, publishDirectory, and packageManager. Registry hosts may be public or authenticated-public DNS names; IP literals, local/private/metadata addresses, APCO platform domains, ambiguous DNS answers, URL syntax, and implicit tags are rejected. See Deploying OCI images for credential and runtime trust boundaries.
source and monorepos
| Field | Type | Required | Default | Description |
|---|---|---|---|---|
source.rootDirectory | safe relative path | no | . | Application root inside the uploaded repository. Absolute paths, traversal, backslashes, control characters, glob metacharacters, missing directories, and symlink traversal are rejected. |
source.watchPaths | string[] | no | [] | Up to 64 inclusive repository-relative paths/globs for automatic GitHub Production and PR Preview admission. Empty means every repository change. |
APCO retains the whole repository as build context so the selected app can consume workspace packages. Framework detection, Dockerfile, build/start commands, and publishDirectory resolve from rootDirectory; watch paths remain repository-relative.
Manifest selection is deterministic:
- CLI: the default repository-root
apco.ymlmay setsource.rootDirectory. An explicit nested--manifest apps/web/apco.ymlselectsapps/webautomatically and cannot retarget another root. - GitHub App: the binding selects
rootDirectory.<rootDirectory>/apco.ymlwins when present. A repository-rootapco.ymlis accepted as fallback only when it explicitly sets the samesource.rootDirectory.
GitHub skips an automatic deployment only after obtaining a complete changed-file list and finding no watch-path match. New branches, API errors, truncated lists, and unusual paths fail open and deploy. PR close/cleanup and manual resync are never suppressed by watch paths.
source is valid only for source (auto, static, or ssr) manifests. OCI projects send JSON and never evaluate .apcoignore, pack a repository, or use a GitHub App source binding.
database
| Field | Type | Required | Default | Description |
|---|---|---|---|---|
database.enabled | boolean | no | false | Requires a managed database for the deployment channel (Pro; SSR or dockerimage). Ordinary deploys ensure the exact channel branch; direct --project/CI deploys require it already ready. Containers receive only that channel's DATABASE_URL. |
database.engine | postgres | mariadb | no | postgres | Engine for the project. The first branch fixes this choice for Production, Preview, and Dev; changing it later fails with DATABASE_ENGINE_CONFLICT. Existing manifests without the field remain Postgres. |
release
| Field | Type | Required | Default | Description |
|---|---|---|---|---|
release.command | string (1–2048 chars) | yes when release exists | — | Shell command run once for this deployment/channel after its source image builds or OCI digest is pinned and before its app candidate starts. SSR or dockerimage; the image must provide /bin/sh. |
release.timeoutSeconds | integer 1–900 | no | 300 | Hard deadline. APCO kills the one-shot container and fails the candidate when it expires. |
The release container uses gVisor and the plan's memory, CPU, and process limits. It joins the project's network and, only for a managed database deployment, the database network; it never joins ingress and cannot receive traffic. It receives the exact scoped environment and managed DATABASE_URL for the deployment channel, plus APCO_RELEASE_PHASE=1, APCO_DEPLOYMENT_ID, and APCO_DEPLOYMENT_CHANNEL. Platform values win over same-named project variables.
Release output is included in the deployment build log with a [release] prefix, secret redaction, and a 1 MiB release-output cap. A non-zero exit, timeout, or runner error marks the candidate failed and leaves the previous route serving. APCO atomically claims the command once per deployment row; an indeterminate retry is failed rather than risking duplicate side effects.
Channel and artifact-reuse semantics:
- ordinary CLI, direct HTTP, GitHub Actions, GitHub App, Production, Preview, and Dev deployments all run the command against their exact channel;
- promotion creates a new Production deployment and runs the command against Production data before switching Production traffic;
- rollback reuses a historical artifact but skips its release command, recording
release.status: "skipped", so old schema work is never replayed; - stop/start and other lifecycle operations do not rerun it.
jobs
| Field | Type | Required | Default | Description |
|---|---|---|---|---|
jobs[].name | string | yes | — | Unique 1–63 character lowercase identifier (a-z, 0-9, internal -) retained across deployments. |
jobs[].schedule | five-field cron | yes | — | Minute, hour, day-of-month, month, day-of-week. Six-field/seconds syntax is rejected. |
jobs[].timezone | IANA timezone | no | UTC | Timezone used to compute cron occurrences, including daylight-saving transitions. |
jobs[].command | string (1–2048 chars) | yes | — | Shell command stored only in internal snapshots; never returned by job APIs/CLI/MCP/dashboard. |
jobs[].channels | channel[] | no | [production] | One or more of production, preview, dev; duplicates are normalized away. |
jobs[].timeoutSeconds | integer 1–900 | no | 300 | Per-attempt hard deadline. |
jobs[].retries | integer 0–5 | no | 0 | Explicit retry attempts after the first attempt. |
jobs[].retryDelaySeconds | integer 1–3600 | no | 30 | Base delay; retries use capped exponential backoff. |
jobs[].concurrency | forbid | allow | replace | no | forbid | Same-definition overlap policy. Project plan concurrency still applies. |
jobs[].enabled | boolean | no | true | Manifest scheduling state. A durable user disable override is retained across deploys. |
The manifest schema caps the list at 50 definitions; the live plan may set a smaller per-project allowance and a concurrent logical-run limit. Jobs require an SSR or dockerimage runtime with /bin/sh; OCI jobs may target Production/Preview but not Dev. Each active definition is reconciled under the deployment/promotion/rollback project lock and pinned to the exact Production, Dev, legacy Preview, or GitHub PR Preview route scope. See Scheduled jobs for execution, database isolation, manual runs, retries, cancellation, and idempotency.
Use expand/contract, backward-compatible migrations. A code rollback does not reverse a successful database change, and APCO does not copy data between Preview and Production.
dev
Overrides for apco dev:
dev is invalid for dockerimage; existing-image projects do not have a Dev channel.
| Field | Type | Required | Default | Description |
|---|---|---|---|---|
dev.command | string | no | auto-detected | Dev-server command (e.g. bun run dev); overrides framework detection entirely (split on whitespace) |
dev.port | positive int | no | detected per framework | Local dev port (also overridable with --port) |
dev.envFiles | string[] | no | — | Reserved for future use (accepted, currently unused) |
Deployment types
app.type: auto resolves source manifests at load time, in this order:
- static if
app.publishDirectory(when set) or one of the conventional output foldersdist,.output/public,publicexists in the project directory; - ssr otherwise (including when a
Dockerfileexists).
dockerimage is never inferred and has no source-detection step.
Rules that differ by type:
static | ssr | dockerimage | |
|---|---|---|---|
| Serving | Built files served directly by the platform | Source-built container runs under gVisor | Existing registry image runs under gVisor |
app.port | Ignored | Used (default 3000; injected as PORT) | Required; image must listen there |
| Source/build fields | Allowed | Allowed | Rejected |
image.reference | Rejected | Rejected | Required; explicit tag or digest |
database.enabled: true | MANIFEST_INVALID | Allowed (Pro) | Allowed (Pro) |
release.command | MANIFEST_INVALID | Allowed | Allowed when image provides /bin/sh |
jobs | MANIFEST_INVALID | Production/Preview/Dev | Production/Preview only |
Dev channel (apco dev --sync) | Rejected | Allowed (Pro) | Rejected |
.apco/ conventions
| Path | Uploaded on deploy? | Purpose |
|---|---|---|
.apco/state.json | No (always stripped) | The CLI's local directory→project link (projectId, slug, apiUrl). No secrets — safe to commit or ignore. Written by the first deploy; used by status/logs/env/open/... to resolve the project |
.apco/migrations/*.sql | Source deploys only | SQL migrations by convention — apply explicitly with apco db migrate, from release.command, or at runtime via runMigrations. Bake required files into an existing OCI image. |
.apcoignore | No (and it defines further exclusions) | Gitignore-style patterns (*, **, ?, trailing /, leading / anchors, # comments) excluded from uploads |
What gets packed
For source manifests, apco deploy packs the project directory into a gzipped tar with these exclusions — and nothing else:
.git,node_modules,.apco/state.json,.apcoignore(always), plus every.apcoignorepattern.- Symlinks and special files are skipped.
The CLI rejects archives over 100 MiB with DEPLOY_TRIGGER_FAILED; the server additionally enforces the plan's maxUploadMb limit (413 UPLOAD_TOO_LARGE).
dockerimage deploys pack nothing and send no multipart source field. Their separate plan limit is maxImageMb; 0 disables the feature, and the limit is checked both during pull and against the inspected image size.
Complete annotated source example
yaml
version: 2 # optional; only 2 is valid
app:
name: my-app # required
slug: auto # "auto"/omitted = server assigns; may not start with "dev-" (reserved)
type: auto # auto | static | ssr (dockerimage uses the separate shape below)
port: 3000 # ssr only; app must listen on the injected PORT env var
buildCommand: pnpm build # optional; overrides Railpack's detected build command
startCommand: pnpm start # optional; overrides Railpack's runtime command
publishDirectory: dist # static: folder containing index.html
packageManager: bun # optional: bun | pnpm | npm | yarn (beats lockfile detection)
health: # optional; used by `apco verify`
path: / # default /
timeoutSeconds: 60 # default 60
source:
rootDirectory: apps/web # optional; defaults to repository root
watchPaths: # optional GitHub filters; empty = all changes
- apps/web/**
- packages/shared/**
database:
enabled: true # optional; SSR/dockerimage — managed database (Pro).
engine: mariadb # optional; postgres (default) | mariadb, immutable per project.
# static + database = MANIFEST_INVALID
release:
command: pnpm db:migrate # optional; runnable apps, once before traffic
timeoutSeconds: 300 # optional; 1-900, default 300
jobs:
- name: nightly-report # unique stable lowercase name
schedule: "0 2 * * *" # standard five-field cron
timezone: UTC # optional IANA timezone
command: node report.mjs # internal only; never returned by job APIs
channels: [production] # production | preview | dev
timeoutSeconds: 300 # 1-900
retries: 1 # 0-5
retryDelaySeconds: 30 # 1-3600, exponential retry delay
concurrency: forbid # forbid | allow | replace
enabled: true
dev:
command: bun run dev # optional; overrides framework detection for `apco dev`
port: 3000 # optional; local dev port
envFiles: [] # reserved
env:
files: [] # reservedMinimal existing-image example:
yaml
version: 2
app:
name: image-app
type: dockerimage
port: 8080
health:
path: /
timeoutSeconds: 60
image:
reference: nginxinc/nginx-unprivileged:1.29-alpineValidation errors
| Error | Cause |
|---|---|
MANIFEST_NOT_FOUND | No file at the manifest path — run apco init |
MANIFEST_INVALID | Not valid YAML, schema violations (listed in error.hint as path: message), an existing manifest on apco init without --force, database/release/jobs on a static project, or source/dev/build fields on a dockerimage project |