Applications#

Define an application in a typed Pkl file named orch.pkl.

amends "orch:/v1alpha1/Application.pkl"

name = "hello"

services {
  ["web"] {
    image = "ghcr.io/acme/hello:1.2.3"
    replicas = 2
    resources {
      cpu = "250m"
      memory = "128Mi"
    }
    http = new Http {
      port = 8080
      domains { "hello.example.com" }
      healthPath = "/healthz"
    }
  }
}

Check a file before deploying it:

orch validate
orch render
orch plan

Services#

Each service requires an OCI image. The most commonly used settings are:

SettingDefaultPurpose
imagerequiredOCI image tag or digest
replicas1Number of running instances
commandimage defaultReplacement entry point
argsimage defaultCommand arguments
restart"always""always", "on-failure", or "never"
resources.cpu"100m"CPU reservation or limit
resources.memory"128Mi"Memory limit
envemptyPlain environment variables
dependsOnemptyServices that must be available before startup
constraintsemptyRequired node labels

Use http for HTTP traffic and tcp for other TCP services. A service cannot define both.

Environment and secrets#

Plain values can live in the application:

env {
  ["APP_ENV"] = "production"
}

Store sensitive values through stdin:

printf '%s' "$DATABASE_URL" | orch secret set hello database-url \
  --from-stdin --target production

Reference the stored name from the application:

secretEnv {
  ["DATABASE_URL"] = "database-url"
}

Secrets can also be projected as files below /run/secrets:

secretFiles {
  new SecretFile {
    source = "signing-key"
    target = "/run/secrets/signing-key"
  }
}

For private image registries:

printf '%s' "$REGISTRY_TOKEN" | orch registry login registry.example:5443 \
  --username robot --password-stdin --target production

Placement#

Add labels to nodes, then constrain a service to matching nodes:

constraints {
  ["region"] = "eu-west"
  ["tier"] = "compute"
}

Constraints are required matches. If no node has all requested labels, the service remains blocked instead of running elsewhere. Replicas spread across eligible nodes when capacity permits.

Local volumes#

Local volumes stay on one worker. A service that mounts one must have exactly one replica.

services {
  ["database"] {
    image = "postgres:17"
    mounts {
      new Mount {
        source = "database"
        target = "/var/lib/postgresql/data"
      }
    }
  }
}

volumes {
  ["database"] {}
}

If that worker is lost, orch reports volume_node_lost. It does not create an empty replacement volume on another worker.

Schedules#

Schedules use five UTC cron fields:

cronJobs {
  ["cleanup"] {
    image = "ghcr.io/acme/cleanup:1.0"
    schedule = "0 2 * * *"
    timeout = "10m"
  }
}

List recent runs and read their logs:

orch cron runs hello cleanup
orch cron logs hello cleanup RUN_ID

Overlapping runs are skipped and missed runs are not replayed.

Reading a local volume#

A cron job can mount a service's local volume read-only, for example to back it up. Each run is placed on the worker that holds the volume:

cronJobs {
  ["database-backup"] {
    image = "ghcr.io/acme/backup:1.0"
    schedule = "0 3 * * *"
    mounts {
      new Mount {
        source = "database"
        target = "/backup/source"
        readOnly = true
      }
    }
  }
}

readOnly must be true, and every mounted volume must belong to the same service in the application. The job waits until that service has been placed. The service keeps writing while the job reads, so use a copy method that is safe for the data, such as the database's own snapshot or backup tool.

Rollouts#

deploy {
  maxSurge = 1
  maxUnavailable = 0
  healthDeadline = "2m"
  autoRollback = true
}

maxSurge and maxUnavailable control how much extra capacity and unavailability a rolling update may use. They cannot both be zero. Applications whose services publish node-local ports must use maxSurge = 0 and maxUnavailable = 1; see node-local TCP and UDP.

See deployments for update and rollback behavior, or networking to expose a service.