Deploy

Two binaries, one contract

A deploy is a manifest, .sklp/<stack>/deploy.yaml, applied to a node. The workstation runs sklp, which talks to a node; the node runs sklet, which is the node. Everything on this page is the workstation side and the contract itself. To stand up a first node, follow Deploy on a VM. For cross-node networking, see The mesh and Front and private origins. For backups and restore, see Backups.

PieceLives inRole
.sklp/<stack>/deploy.yamlthe reposervices, jobs, spaces, routes, registries
.sklp/deploy-root.yamlthe repowhich space each manifest deploys to, on which node
~/.config/sklp/config.yamlthe workstationdeploy contexts (server, token, CA fingerprint)
node control APIthe node, :6464what sklp deploy talks to

deploy.yaml

version: 1
kind: deploy
name: app
 
env_files: [.env]
project_id: 6f1c…          # optional: pulls secrets marked for "deploy" from core
 
registries:
  rg.fr-par.scw.cloud:
    username: nologin
    password: ${SCW_SECRET_KEY}
 
services:
  api:
    image: rg.fr-par.scw.cloud/acme/api:20261020-0912-ab12cd34
    port: 8080
    env:
      DATABASE_URL: postgres://app:${PG_PASSWORD}@postgres:5432/app
    ready:
      http: http://localhost:8080/health
      timeout: 90s
      soak: 60s
    depends_on: [postgres, migrate]
    resources: { cpu: "1", memory: 512Mi, pids: 512 }
 
  postgres:
    image: postgres:17-bookworm
    volumes: ["pgdata:/var/lib/postgresql/data"]
    backup:
      command: ["sh", "-c", "pg_dump … | aws s3 cp - s3://…"]
      schedule: "0 3 * * *"
 
jobs:
  migrate:
    image: rg.fr-par.scw.cloud/acme/migrate:20261020-0912-ab12cd34
    depends_on: [postgres]
 
spaces:
  production:
    api: { replicas: 2 }
    gateway:
      routes:
        - service: api
          host: app.example.com
          kind: api

Top-level keys

KeyMeaning
servicesLong-running containers, keyed by name. Every space starts from this set.
jobsRun-to-completion tasks (migrations, maintenance). Same shape as a service; names are unique across both maps. A job with schedule: re-runs on that cron.
env_filesDotenv files (relative to the manifest) loaded before ${VAR} expansion. Host env wins.
project_idSkalpai project whose secrets marked for the deploy runner are resolved at load. Needs sklp auth login; soft no-op without it.
spacesNamed spaces (production, staging…) with per-service overrides, plus an optional ephemeral mould for any unlisted name.
gatewayHost-level front door: driver, email, acme.
registriesPull credentials keyed by registry host — the daemonless imagePullSecret.

Service fields

FieldMeaning
imageImmutable tag (no latest).
commandOverrides the image entrypoint/cmd.
replicasInstance count (default 1).
portContainer port the space proxies to (expose is the deprecated alias).
proxyPut the service behind the space proxy.
envEnvironment; values may be ${VAR} templates (see below).
readyOne probe among http, tcp, log, file, plus timeout and soak.
depends_onServices or jobs that must be ready (or complete) first.
volumesname:/container/path[:ro|rw] — named volumes only.
portsRaw port mappings; proxy_protocol lists those that get a PROXY v2 header.
backup{command, schedule}, both required — see Backups.
resourcescpu, memory, pids; require: true fails the launch when cgroups v2 delegation is missing.
capabilitiesExtra Linux capabilities beyond the hardened baseline (see admission).
schedule5-field cron, jobs: only.

Spaces

A named space carries per-service overrides (replicas, port, proxy, env only — image and command stay uniform) plus space-level blocks:

BlockMeaning
ingressservice: "host:container" raw host-port map, no TLS.
gateway.routesPublic routes by host and path, with TLS. Mutually exclusive with ingress per service.
allow_egressCIDRs the space may reach; set = outbound default-deny.
ephemeral mouldisolate: prefix, url (with ${SPACE}), ttl, replicas.

Routes

FieldMeaning
serviceA service of this space, or <space>/<service> on another node over the mesh.
spaceHand the whole host to another space's gateway (ADR-021). path and options are refused.
host, pathHost and optional path prefix; (host, path) is unique across spaces.
kindauth (5 rps / burst 10), api (50 / 100), web, expensive — supplies a rate limit.
rate, burstExplicit per-IP limit; overrides kind.
timeoutPer-request bound (30s); empty = unbounded.
max_bodyBody cap (10Mi); empty = unbounded.
redirect_apexOn the apex route: www. answers 301 to the bare host.
security_headersHSTS, X-Frame-Options, nosniff, referrer policy.
compressGzip text responses.
passthroughRoute TLS by SNI to this port without decrypting.

Env templates

A value written as ${VAR} (or containing one, postgres://app:${PW}@db/app) is persisted as its template. A remote apply ships the variable values to the node, which stores them in the space's 0600 env file — never in the manifest — and rebuilds the value at launch. Registry passwords follow the same path.

Declared images

The node keeps two images per service: Image (what runs) and DeclaredImage (what Git declared at the last full apply). An image watch roll changes Image only. On the next full apply, an incoming tag equal to the previous declaration means Git did not touch it and the rolled image is kept; a different tag means Git changed it and Git wins (ADR-018). A spec the supervisor just rolled back is answered rejected, not replayed.

deploy-root.yaml

.sklp/deploy-root.yaml is the pointer, not the payload: it names which space each manifest deploys to and on which node. A repo without it reconciles nothing.

kind: deploy-root
node: mesh-front
 
deployments:
  - space: meshauthority
    path: .sklp/meshauthority/deploy.yaml
  - space: app-production
    path: .sklp/app/deploy.yaml
    node: sklet-prod-v3      # per-entry override
KeyMeaning
node / node_idDefault target node.
deployments[].spaceDestination space.
deployments[].pathManifest path.
deployments[].node / node_idOverride the node for one entry.

Contexts

A context is a named node target (server, token, CA fingerprint, default space), kubeconfig-style.

sklp deploy login -context prod -server https://203.0.113.10:6464 \
  -token "$T" -ca-fingerprint "$FP"
sklp deploy context list
sklp deploy context use prod
sklp deploy context current

Always pass -context to an apply. The active context is never used implicitly for a remote apply — a bare apply against a remote target is refused.

CommandPurpose
deploy loginSave server, token, CA fingerprint into a context.
deploy context listList contexts, * marks the active one.
deploy context useSet the active context.
deploy context currentPrint the active context.

Applying

sklp deploy apply                       # no stack: list the deployable stacks
sklp deploy apply app -context prod
sklp deploy apply app -context prod -space staging

The node answers per space with a kubectl-style result:

ResultMeaning
createdThe space was brought up by this apply.
configuredThe live supervisor was reconciled toward the manifest.
unchangedDesired and live manifests matched.
rejectedA service's spec is the one its last rollout rolled back; nothing moved.

-force adopts a space already deployed by a different stack; without it the cross-stack apply is refused. -local forces the local socket transport. -server, -token, -token-file, -ca-fingerprint, -insecure address a node without a context.

Operating a node

CommandPurpose
deploy lsRunning instances (-space, empty = every space).
deploy getOne space's readiness and instances.
deploy logsA service's output; -f follows.
deploy execRun a command or open a shell in a live instance.
deploy restartBounce every live instance of a service.
deploy deleteStop a space and remove its runtime; -v also destroys its named volumes.
deploy datastoresThe data each space keeps and how well it is protected.
deploy restoresRestore drills with the latest verdict of each.
deploy fingerprintThe space's TLS certificate fingerprint.
deploy tokenThe space's network Bearer token.
deploy token rotateRotate a node's control token and save it into the context.
deploy mesh lsMesh peers as the node sees them.
deploy mesh rmForget a peer silent for 2 minutes; its mesh IP returns to the pool.

Every remote command takes the same addressing flags as apply (-context, -server, -token, -token-file, -ca-fingerprint, -insecure, -config).

Gateway

sklp gateway manages the host front door. With an explicit -context or -server it pilots the node's gateway over the control API; without, it acts on the local host.

CommandPurpose
gateway up / down / statusThe host gateway singleton (driver from -file).
gateway muxNative ingress-per-space mux: -listen :9080, -listen-tls :9443 (SNI passthrough).
gateway redirect up / down / statusThe :80→:9080 / :443→:9443 NAT; up needs root, -persist writes /etc/nftables.d.

ACME and rate limiting are covered in Deploy on a VM.

Git reconcile

A push to the repo's default branch is applied without anyone running sklp deploy apply: core reads deploy-root.yaml, sends the raw manifest and its variables for each deployment, and the node resolves and applies it. Pushes to other branches never reconcile. To retry a reconcile, push an empty commit.

A node can also pull on its own with sklet reconcile (ADR-014): it polls the repos in reconcile.yaml and needs only Git outbound, so a node behind NAT works the same.

Rollouts — soak and rollback

A rollout is start-first (ADR-013):

  1. The new generation starts beside the old one, which keeps serving.
  2. Once every new instance is ready, the proxy points to it and the old generation is de-routed but kept running.
  3. During the soak (ready.soak, default 60 s) a dying new instance rolls the proxy back to the old generation.
  4. After the soak the old generation stops.

A rollout ends rolled, rolled_back or superseded (a newer apply replaced it). A spec that was rolled back is refused on replay (rejected) until it changes.

Restore drills

The node derives a <space>-restore space next to every production space holding a store it can read back (Postgres via pgbackrest, NATS archives). Nothing is declared: destination, credentials and image come from the production services. Drills are spread over 04:00–05:59, run the production image with the node's own sklet mounted read-only, and their verdicts are kept on the node. sklp deploy restores lists them.

Admission policy

A node refuses, at validation and again at apply:

RefusedWhy
Host-path volumes (/host:/ctr)Only named volumes (name:/ctr) are allowed.
CAP_DAC_READ_SEARCH, CAP_SYS_PTRACE, CAP_NET_ADMIN, CAP_MKNOD, CAP_SYS_CHROOT, CAP_SETPCAP, CAP_SETFCAPThey reach past the user namespace; a node decision, not a manifest one. CAP_SYS_ADMIN is not even a known capability.
Unknown capabilitiesRejected at parse rather than silently granted as nothing.
Images outside the node's trusted registriespolicy.trusted_registries in node.yaml (SKLP_TRUSTED_REGISTRIES).
Tag-only imagesWhen the node sets policy.require_image_digest.

The image policy is a property of the node, never of the manifest.