Skip to content

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

FieldTypeRequiredDefaultDescription
version2noManifest schema version; only 2 is accepted when present
appobjectyesThe application block (below)
sourceobjectno{ rootDirectory: ".", watchPaths: [] }Monorepo app root and automatic GitHub deploy filters (below)
imageobjectfor dockerimageExisting OCI image reference; invalid for source project types (below)
releaseobjectnoOne-shot runnable-app release command executed before traffic activation (below)
jobsarrayno[]Manifest-defined runnable-app scheduled jobs, normalized and pinned to exact deployment scopes (below)
envobjectnoenv.files: string[] — reserved for future use (accepted, currently unused)
databaseobjectnoManaged database block (below)
devobjectnoLocal-dev overrides for apco dev (below)

app

FieldTypeRequiredDefaultDescription
app.namestringyesApp name; used as the project name and, for auto slugs, as the slug seed
app.slugstringnoautoSubdomain: 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.typeauto | static | ssr | dockerimagenoautoDeployment type (resolution rules below). Existing images must select dockerimage explicitly; auto never infers it
app.portpositive intfor dockerimage3000 for SSRRunnable app port. Source SSR apps receive it as PORT; an existing image must already listen on the declared port
app.buildCommandstringnoOverrides the detected build command for source (Railpack) builds
app.startCommandstringnoOverrides the detected runtime command for source (Railpack) builds
app.publishDirectorystringnoStatic: the folder containing index.html (e.g. dist). Also used as a static-type hint by auto resolution
app.packageManagerbun | pnpm | npm | yarnnoForces 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.pathstringno/Health-check path used for candidate activation and apco verify
app.health.timeoutSecondspositive intno60Candidate/verification health-check timeout

image

FieldTypeRequiredDefaultDescription
image.referenceOCI referenceyes for dockerimagePublicly 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

FieldTypeRequiredDefaultDescription
source.rootDirectorysafe relative pathno.Application root inside the uploaded repository. Absolute paths, traversal, backslashes, control characters, glob metacharacters, missing directories, and symlink traversal are rejected.
source.watchPathsstring[]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.yml may set source.rootDirectory. An explicit nested --manifest apps/web/apco.yml selects apps/web automatically and cannot retarget another root.
  • GitHub App: the binding selects rootDirectory. <rootDirectory>/apco.yml wins when present. A repository-root apco.yml is accepted as fallback only when it explicitly sets the same source.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

FieldTypeRequiredDefaultDescription
database.enabledbooleannofalseRequires 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.enginepostgres | mariadbnopostgresEngine 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

FieldTypeRequiredDefaultDescription
release.commandstring (1–2048 chars)yes when release existsShell 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.timeoutSecondsinteger 1–900no300Hard 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

FieldTypeRequiredDefaultDescription
jobs[].namestringyesUnique 1–63 character lowercase identifier (a-z, 0-9, internal -) retained across deployments.
jobs[].schedulefive-field cronyesMinute, hour, day-of-month, month, day-of-week. Six-field/seconds syntax is rejected.
jobs[].timezoneIANA timezonenoUTCTimezone used to compute cron occurrences, including daylight-saving transitions.
jobs[].commandstring (1–2048 chars)yesShell command stored only in internal snapshots; never returned by job APIs/CLI/MCP/dashboard.
jobs[].channelschannel[]no[production]One or more of production, preview, dev; duplicates are normalized away.
jobs[].timeoutSecondsinteger 1–900no300Per-attempt hard deadline.
jobs[].retriesinteger 0–5no0Explicit retry attempts after the first attempt.
jobs[].retryDelaySecondsinteger 1–3600no30Base delay; retries use capped exponential backoff.
jobs[].concurrencyforbid | allow | replacenoforbidSame-definition overlap policy. Project plan concurrency still applies.
jobs[].enabledbooleannotrueManifest 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.

FieldTypeRequiredDefaultDescription
dev.commandstringnoauto-detectedDev-server command (e.g. bun run dev); overrides framework detection entirely (split on whitespace)
dev.portpositive intnodetected per frameworkLocal dev port (also overridable with --port)
dev.envFilesstring[]noReserved for future use (accepted, currently unused)

Deployment types

app.type: auto resolves source manifests at load time, in this order:

  1. static if app.publishDirectory (when set) or one of the conventional output folders dist, .output/public, public exists in the project directory;
  2. ssr otherwise (including when a Dockerfile exists).

dockerimage is never inferred and has no source-detection step.

Rules that differ by type:

staticssrdockerimage
ServingBuilt files served directly by the platformSource-built container runs under gVisorExisting registry image runs under gVisor
app.portIgnoredUsed (default 3000; injected as PORT)Required; image must listen there
Source/build fieldsAllowedAllowedRejected
image.referenceRejectedRejectedRequired; explicit tag or digest
database.enabled: trueMANIFEST_INVALIDAllowed (Pro)Allowed (Pro)
release.commandMANIFEST_INVALIDAllowedAllowed when image provides /bin/sh
jobsMANIFEST_INVALIDProduction/Preview/DevProduction/Preview only
Dev channel (apco dev --sync)RejectedAllowed (Pro)Rejected

.apco/ conventions

PathUploaded on deploy?Purpose
.apco/state.jsonNo (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/*.sqlSource deploys onlySQL 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.
.apcoignoreNo (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 .apcoignore pattern.
  • 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: []                   # reserved

Minimal 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-alpine

Validation errors

ErrorCause
MANIFEST_NOT_FOUNDNo file at the manifest path — run apco init
MANIFEST_INVALIDNot 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

APCO Cloud — ship apps with one command.