Skip to content

Deploying apps

apco deploy puts source code or an existing OCI image live at the authoritative project URL—with zero downtime on redeploys. Source manifests are packed/uploaded/built; dockerimage manifests send JSON, pull from a registry, and pin the immutable digest without uploading source.

How a deploy works

For a source deployment:

  1. Pack — the CLI creates a tarball of the current directory. .git, node_modules, .apco/state.json, and anything matched by .apcoignore (gitignore syntax) are excluded. The rest of .apco/ — including .apco/migrations/*.sql — ships with the app.
  2. Upload — the archive is sent to the control plane. Your plan caps the upload size (UPLOAD_TOO_LARGE, HTTP 413) and deploys per hour (RATE_LIMITED, HTTP 429).
  3. Build & release — the worker builds the app and, when configured, runs its one-shot release command against the exact channel environment/database.
  4. Start & activate — the worker starts the candidate and only switches traffic once the new version is healthy.

For app.type: dockerimage, the CLI skips pack/upload entirely. The control plane admits a normalized JSON manifest, the worker validates and pulls the public/authenticated registry image, enforces the plan size and platform, pins its repository digest, then uses the same release/start/health/activation path.

Monorepos

Select an application directory without losing repository-level workspace context:

yaml
version: 2
app:
  name: storefront
  type: ssr
  buildCommand: pnpm build
  startCommand: pnpm start
source:
  rootDirectory: apps/storefront
  watchPaths:
    - apps/storefront/**
    - packages/ui/**

APCO uploads and builds from the repository root, then resolves framework detection, Dockerfile, build/start commands, and publish output relative to apps/storefront. This lets the app import packages/ui without copying or flattening the workspace.

The root manifest above is the simplest CLI/GitHub-compatible form. With the CLI, apco --manifest apps/storefront/apco.yml deploy is also supported: the nested manifest selects its own directory and cannot point elsewhere. For a GitHub App binding, a nested <root>/apco.yml wins; repository-root fallback is allowed only when it explicitly names the configured root.

watchPaths affect automatic GitHub Production pushes and open-PR Preview updates. They are inclusive and repository-relative; an empty list deploys on every change. APCO suppresses work only from a complete GitHub file list. API errors, truncation, new branches, or unusual paths fail open and deploy.

Deployment statuses move through queuedbuildingrunning (or failed). Older deployments become superseded; stopping a project adds stopped.

Verified email required

Deploys require a verified email address (EMAIL_NOT_VERIFIED, HTTP 403). Check your inbox after signing up.

Deployment types

The manifest's app.type controls how your app is built and served — see the manifest reference for the full apco.yml schema.

  • type: auto (default) picks static when a publish directory (dist, .output/public, public) exists in your project, otherwise ssr.
  • type: static — files served directly by the platform's edge (Caddy).
  • type: ssr — your server runs in an isolated container (gVisor).
  • type: dockerimage — an existing Linux OCI image runs in the same isolated container boundary; the type is explicit and is never inferred.

Static sites

Static sites are not built on the platform — build locally, then deploy the output. The worker serves the folder that contains index.html:

yaml
version: 2
app:
  name: hello-static
  type: static
  publishDirectory: dist   # folder with index.html

If publishDirectory is omitted, the worker auto-detects among dist, .output/public, public, build, and the archive root.

bash
npm run build      # produce dist/
apco deploy        # uploads and serves dist/

SSR apps

Server apps are built from source with Railpack and run in a container. Your app must listen on the injected PORT environment variable (the manifest's app.port, default 3000).

yaml
version: 2
app:
  name: hello-ssr
  type: ssr
  port: 3000
  buildCommand: pnpm build     # optional Railpack build override
  packageManager: bun          # optional: bun | pnpm | npm | yarn
  health:
    path: /health
    timeoutSeconds: 60
  • buildCommand overrides Railpack's detected build step.
  • packageManager forces the toolchain when lockfile detection isn't enough (e.g. Bun). Railpack prefers pnpm-lock.yaml over bun.lock when both exist — the manifest field wins over everything.
  • Your environment variables are available as build-time secrets and at runtime.

Health check: after starting the container, the worker polls health.path (default /) until it returns 2xx/3xx or timeoutSeconds (default 60) elapses. If the check fails, the deploy is marked failed and the previous deployment keeps serving traffic — a broken build never takes your site down.

Dockerfile override

If a Dockerfile exists at the project root, the worker builds the image from it instead of using Railpack. Everything else (env vars, PORT, health check, plan limits) works the same.

Existing OCI images

yaml
version: 2
app:
  name: image-app
  type: dockerimage
  port: 8080
  health:
    path: /
    timeoutSeconds: 60
image:
  reference: nginxinc/nginx-unprivileged:1.29-alpine

The reference must include an explicit tag or sha256 digest and use a publicly routed registry. Tags are resolved once and every later lifecycle operation reuses the stored digest. OCI projects support Production/Preview, promotion, Production rollback, stop/start recovery, managed databases, release commands, and Production/Preview scheduled jobs; they do not support Dev or GitHub App source deployments. See Deploying OCI images for private credentials, trust, quotas, and failure codes.

Release commands

SSR and OCI-image apps can run schema migrations or other one-off work after the source build or digest pin and before the candidate starts:

yaml
version: 2
app:
  name: api
  type: ssr
database:
  enabled: true
release:
  command: pnpm db:migrate
  timeoutSeconds: 300

APCO runs the command once per deployment/channel in a hardened gVisor container that is never attached to ingress. It uses the same plan CPU, memory, and process limits as the app, receives the exact channel-scoped environment and managed DATABASE_URL, and has a 1–900 second deadline (300 by default). The image must contain /bin/sh; the command runs as /bin/sh -lc <command>.

Release output appears in build logs with [release] prefixes. Known environment values and common secret forms are redacted, and release output is capped at 1 MiB. A timeout, non-zero exit, or runner error fails the candidate, removes it, and leaves the previous deployment serving. If a worker retry finds an indeterminate attempt, it fails closed instead of executing the command a second time.

Production, Preview, Dev, CLI, GitHub Actions, and GitHub App deployments share this path. Promotion reruns the release command as a new Production deployment against Production data. Rollback deliberately records the release as skipped and never replays historical migration work; stop/start also does not rerun it.

Design migrations for mixed application versions: prefer expand/contract and other forward-compatible schema changes. Rolling application code back does not reverse a successful database migration. Preview and Production databases remain independent; promotion never copies data.

Redeploying

Just run apco deploy again. The new deployment builds while the old one serves traffic; the switch happens only after the health check passes. Redeploying is also how env var changes reach a running SSR app.

Rollback

Roll back to a previous deployment in one command:

bash
apco rollback                 # latest superseded deployment
apco rollback <deploymentId>  # a specific one (see `apco status`)
Rolled back my-app to deployment 3f2b...: https://my-app.apco.space

Rollback re-deploys the previous immutable artifact without rebuilding. Source images are retained; OCI deployments store and, if needed, re-pull the exact repository digest. Production and Dev source rollbacks stay on their channel; OCI rollback is Production-only because OCI has no Dev channel. A configured release command is recorded as skipped and is never replayed from the historical artifact.

Promotion

apco promote reuses a running Preview artifact as a new Production deployment. It does not rebuild or copy Preview data. If the manifest has a release command, APCO runs it once against the Production environment and Production database before starting the Production candidate; failure leaves the current Production route unchanged and the Preview remains live.

Stop and start

bash
apco stop      # take the app offline (visitors see a 503 page)
apco start     # bring it back

Both accept --project <slug|id> and --no-wait (return as soon as the action is queued instead of waiting for completion). Stopping does not delete anything—apco start resumes the same deployment. If a stopped OCI image was pruned locally, start recovers it by the stored digest rather than resolving the original tag.

Status

bash
apco status
Project my-app: running.

The --json output additionally lists recent deployments — including the deployment ids you can pass to apco rollback and apco logs:

json
{ "ok": true, "operation": "status", "message": "Project my-app: running.",
  "url": "https://my-app.apco.space",
  "project": { "id": "d2c1a9b4-...", "slug": "my-app", "type": "ssr" },
  "deployments": [
    { "id": "8a41f3aa-...", "status": "running",
      "source": { "rootDirectory": "apps/storefront", "manifestPath": "apco.yml",
        "watchPaths": ["apps/storefront/**", "packages/ui/**"] } },
    { "id": "3f2b91c0-...", "status": "superseded",
      "source": { "rootDirectory": ".", "manifestPath": "apco.yml", "watchPaths": [] } }
  ] }

You can also verify the live URL end-to-end:

bash
apco verify    # polls the health path until it returns 200

For OCI deployments, status/deployment detail carries an artifact object with the requested reference, resolved digest, OS, and architecture instead of source provenance. Credentials are never included.

Renaming a project

bash
apco projects rename shop
my-app.apco.space -> shop.apco.space

Renames are zero-downtime: the command waits until the new slug serves traffic. The URL changes, so update any links.

Reserved slugs

Slugs starting with dev- are reserved for dev channel hosts — create and rename will reject them.

Managing projects

bash
apco projects list             # all projects with status and URL
apco projects delete my-app --yes   # destructive: removes the project

APCO Cloud — ship apps with one command.