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.
| Piece | Lives in | Role |
|---|---|---|
.sklp/<stack>/deploy.yaml | the repo | services, jobs, spaces, routes, registries |
.sklp/deploy-root.yaml | the repo | which space each manifest deploys to, on which node |
~/.config/sklp/config.yaml | the workstation | deploy contexts (server, token, CA fingerprint) |
| node control API | the node, :6464 | what 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: apiTop-level keys
| Key | Meaning |
|---|---|
services | Long-running containers, keyed by name. Every space starts from this set. |
jobs | Run-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_files | Dotenv files (relative to the manifest) loaded before ${VAR} expansion. Host env wins. |
project_id | Skalpai project whose secrets marked for the deploy runner are resolved at load. Needs sklp auth login; soft no-op without it. |
spaces | Named spaces (production, staging…) with per-service overrides, plus an optional ephemeral mould for any unlisted name. |
gateway | Host-level front door: driver, email, acme. |
registries | Pull credentials keyed by registry host — the daemonless imagePullSecret. |
Service fields
| Field | Meaning |
|---|---|
image | Immutable tag (no latest). |
command | Overrides the image entrypoint/cmd. |
replicas | Instance count (default 1). |
port | Container port the space proxies to (expose is the deprecated alias). |
proxy | Put the service behind the space proxy. |
env | Environment; values may be ${VAR} templates (see below). |
ready | One probe among http, tcp, log, file, plus timeout and soak. |
depends_on | Services or jobs that must be ready (or complete) first. |
volumes | name:/container/path[:ro|rw] — named volumes only. |
ports | Raw port mappings; proxy_protocol lists those that get a PROXY v2 header. |
backup | {command, schedule}, both required — see Backups. |
resources | cpu, memory, pids; require: true fails the launch when cgroups v2 delegation is missing. |
capabilities | Extra Linux capabilities beyond the hardened baseline (see admission). |
schedule | 5-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:
| Block | Meaning |
|---|---|
ingress | service: "host:container" raw host-port map, no TLS. |
gateway.routes | Public routes by host and path, with TLS. Mutually exclusive with ingress per service. |
allow_egress | CIDRs the space may reach; set = outbound default-deny. |
ephemeral mould | isolate: prefix, url (with ${SPACE}), ttl, replicas. |
Routes
| Field | Meaning |
|---|---|
service | A service of this space, or <space>/<service> on another node over the mesh. |
space | Hand the whole host to another space's gateway (ADR-021). path and options are refused. |
host, path | Host and optional path prefix; (host, path) is unique across spaces. |
kind | auth (5 rps / burst 10), api (50 / 100), web, expensive — supplies a rate limit. |
rate, burst | Explicit per-IP limit; overrides kind. |
timeout | Per-request bound (30s); empty = unbounded. |
max_body | Body cap (10Mi); empty = unbounded. |
redirect_apex | On the apex route: www. answers 301 to the bare host. |
security_headers | HSTS, X-Frame-Options, nosniff, referrer policy. |
compress | Gzip text responses. |
passthrough | Route 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| Key | Meaning |
|---|---|
node / node_id | Default target node. |
deployments[].space | Destination space. |
deployments[].path | Manifest path. |
deployments[].node / node_id | Override 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 currentAlways 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.
| Command | Purpose |
|---|---|
deploy login | Save server, token, CA fingerprint into a context. |
deploy context list | List contexts, * marks the active one. |
deploy context use | Set the active context. |
deploy context current | Print 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 stagingThe node answers per space with a kubectl-style result:
| Result | Meaning |
|---|---|
created | The space was brought up by this apply. |
configured | The live supervisor was reconciled toward the manifest. |
unchanged | Desired and live manifests matched. |
rejected | A 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
| Command | Purpose |
|---|---|
deploy ls | Running instances (-space, empty = every space). |
deploy get | One space's readiness and instances. |
deploy logs | A service's output; -f follows. |
deploy exec | Run a command or open a shell in a live instance. |
deploy restart | Bounce every live instance of a service. |
deploy delete | Stop a space and remove its runtime; -v also destroys its named volumes. |
deploy datastores | The data each space keeps and how well it is protected. |
deploy restores | Restore drills with the latest verdict of each. |
deploy fingerprint | The space's TLS certificate fingerprint. |
deploy token | The space's network Bearer token. |
deploy token rotate | Rotate a node's control token and save it into the context. |
deploy mesh ls | Mesh peers as the node sees them. |
deploy mesh rm | Forget 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.
| Command | Purpose |
|---|---|
gateway up / down / status | The host gateway singleton (driver from -file). |
gateway mux | Native ingress-per-space mux: -listen :9080, -listen-tls :9443 (SNI passthrough). |
gateway redirect up / down / status | The :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):
- The new generation starts beside the old one, which keeps serving.
- Once every new instance is ready, the proxy points to it and the old generation is de-routed but kept running.
- During the soak (
ready.soak, default 60 s) a dying new instance rolls the proxy back to the old generation. - 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:
| Refused | Why |
|---|---|
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_SETFCAP | They reach past the user namespace; a node decision, not a manifest one. CAP_SYS_ADMIN is not even a known capability. |
| Unknown capabilities | Rejected at parse rather than silently granted as nothing. |
| Images outside the node's trusted registries | policy.trusted_registries in node.yaml (SKLP_TRUSTED_REGISTRIES). |
| Tag-only images | When the node sets policy.require_image_digest. |
The image policy is a property of the node, never of the manifest.