Files
skipper/README.md
2026-04-05 15:28:04 +02:00

8.6 KiB

Skipper MVP

Skipper is a file-backed control plane and host agent pair for lightweight hosting orchestration.

Structure

  • skipper-api/: Express /v1 control plane API
  • skippy-agent/: polling agent that executes declarative work orders
  • shared/: storage, tracing, auth, logging, events, snapshots
  • data/: JSON-backed resources, work orders, logs, events, and auth records

Local Run

docker compose up --build

Skipper CLI

A minimal CLI is now available for common operator tasks.

Run it from the repo with:

npm run cli -- --help

Available commands:

npm run cli -- health --url http://127.0.0.1:3000
npm run cli -- deploy apply --url http://127.0.0.1:3000 --admin-token YOUR_ADMIN_TOKEN --tenant-id example-tenant
DATA_DIR=/opt/skipper/data npm run cli -- node register --node-id host-1 --role worker --region local
npm run cli -- node show --url http://127.0.0.1:3000 --admin-token YOUR_ADMIN_TOKEN --node-id host-1

If you install the repo as an executable package, the binary name is skipper.

Deploy Skipper

The controller can be deployed on its own host from the internal registry.

First-start behavior:

  • Skipper now bootstraps its own on-disk data layout on startup.
  • An empty bind-mounted data directory is valid.
  • You still need to supply an ADMIN_TOKEN.

Files:

Basic flow on the target server:

mkdir -p /opt/skipper
cd /opt/skipper
cp /path/to/repo/deploy/skipper/docker-compose.yml .
cp /path/to/repo/deploy/skipper/.env.example .env

Edit .env:

  • set SKIPPER_IMAGE to the tag you released
  • set a strong ADMIN_TOKEN
  • optionally change SKIPPER_PORT

Start Skipper:

docker login registry.internal.budgethost.io
docker compose up -d

One-line first boot with docker run:

docker run -d \
  --name skipper-api \
  --restart unless-stopped \
  -p 3000:3000 \
  -e HOST=0.0.0.0 \
  -e PORT=3000 \
  -e DATA_DIR=/app/data \
  -e ADMIN_TOKEN='replace-with-a-long-random-admin-token' \
  -v /opt/skipper/data:/app/data \
  registry.internal.budgethost.io/skipper/skipper-api:latest

That command is enough for first boot. The container will create the required /app/data subdirectories automatically.

Verify:

docker compose ps
curl http://127.0.0.1:3000/v1/health

Persisted controller state will live in:

/opt/skipper/data

The next step after this is deploying one or more Skippy agents pointed at the controller URL.

Attach Skippy

Attaching a Skippy agent has two parts:

  1. register the node on the Skipper host
  2. start the agent container on the target host

1. Register The Node On Skipper

Run this on the Skipper host, in the same repo or deployment workspace that owns /opt/skipper/data:

DATA_DIR=/opt/skipper/data npm run cli -- node register --node-id host-1 --role worker --region se-sto-1

That writes:

  • /opt/skipper/data/resources/nodes/host-1.json
  • /opt/skipper/data/auth/nodes/host-1.json

The command prints the generated token. Save it and use it as AGENT_TOKEN on the target host.

You can also provide your own token:

DATA_DIR=/opt/skipper/data npm run cli -- node register --node-id host-1 --token 'your-long-random-token'

2. Start The Agent On The Target Host

Files:

Basic flow on the target host:

mkdir -p /opt/skippy
cd /opt/skippy
cp /path/to/repo/deploy/skippy/docker-compose.yml .
cp /path/to/repo/deploy/skippy/.env.example .env

Edit .env:

  • set SKIPPY_IMAGE
  • set SKIPPER_URL
  • set AGENT_ID
  • set AGENT_TOKEN

Start the agent:

docker login registry.internal.budgethost.io
docker compose up -d

One-line first boot with docker run:

docker run -d \
  --name skippy-agent \
  --restart unless-stopped \
  -e DATA_DIR=/app/data \
  -e SKIPPER_URL='http://your-skipper-host:3000' \
  -e AGENT_ID='host-1' \
  -e AGENT_TOKEN='replace-with-generated-token' \
  -e POLL_INTERVAL_MS=5000 \
  -e HEARTBEAT_INTERVAL_MS=15000 \
  -e SKIPPY_COMPOSE_BASE_DIR=/opt/skipper/tenants \
  -v /var/run/docker.sock:/var/run/docker.sock \
  -v /opt/skippy/data:/app/data \
  -v /opt/skippy/tenants:/opt/skipper/tenants \
  registry.internal.budgethost.io/skipper/skippy-agent:latest

Verify:

docker logs skippy-agent --tail 50
curl -H "x-admin-token: <your-admin-token>" http://your-skipper-host:3000/v1/resources/node/host-1

Image Build And Push

Default image targets:

  • registry.internal.budgethost.io/skipper/skipper-api
  • registry.internal.budgethost.io/skipper/skippy-agent

Prerequisites:

  • Docker daemon access must work from your shell
  • You must be logged into registry.internal.budgethost.io

Sanity checks:

docker info
docker login registry.internal.budgethost.io

Preview tags:

npm run images:print

Build locally:

IMAGE_TAG=0.1.0 npm run images:build

Build and push to the internal registry:

IMAGE_TAG=0.1.0 npm run images:release

Push prebuilt tags only:

IMAGE_TAG=0.1.0 npm run images:push

Optional overrides:

  • IMAGE_REGISTRY
  • IMAGE_NAMESPACE
  • IMAGE_TAG
  • IMAGE_PLATFORM

Run compose from registry-pushed images instead of local builds:

IMAGE_TAG=0.1.0 docker compose -f docker-compose.yml -f docker-compose.registry.yml up -d

First release sequence:

npm run smoke:test
docker login registry.internal.budgethost.io
IMAGE_TAG=0.1.0 npm run images:release
IMAGE_TAG=0.1.0 docker compose -f docker-compose.yml -f docker-compose.registry.yml up -d

Verification:

docker compose -f docker-compose.yml -f docker-compose.registry.yml ps
curl http://localhost:3000/v1/health

Current note:

  • The application smoke test passed in this repository.
  • The image release flow was prepared and invoked, but this environment could not access /var/run/docker.sock, so the actual build and push must be run from a host session with Docker daemon permissions.

Release Manifest

A release-oriented wrapper is available to generate git-aware image tags and write a deployment manifest.

Print the next release plan:

npm run release:print

Write manifest files without building:

npm run release:plan

Build release-tagged images and write manifest:

RUN_SMOKE_TEST=1 npm run release:build

Build, push, and write manifest:

RUN_SMOKE_TEST=1 npm run release:publish

Generated manifest files:

  • artifacts/releases/latest.json
  • artifacts/releases/<version_tag>.json

Release environment overrides:

  • RELEASE_CHANNEL
  • RELEASE_VERSION_TAG
  • RELEASE_GIT_SHA
  • IMAGE_REGISTRY
  • IMAGE_NAMESPACE

Tag behavior:

  • If git metadata is available, the release tag includes the short commit SHA.
  • If git metadata is not available, the release tag falls back to nogit.
  • Unless overridden, the generated tag format is <channel>-<git_sha>-<timestamp>.

Smoke Test

npm run smoke:test

This starts the API and agent as local subprocesses, uses a mocked docker binary, triggers one deploy, and verifies the completed job plus written compose artifacts.

Create a deployment work order:

curl -X POST \
  -H 'x-admin-token: dev-admin-token' \
  -H 'x-idempotency-key: local-run-1' \
  -H 'x-request-id: 11111111-1111-1111-1111-111111111111' \
  -H 'x-correlation-id: 22222222-2222-2222-2222-222222222222' \
  http://localhost:3000/v1/deployments/example-tenant/apply

Inspect work orders:

find data/work-orders -type f | sort

Example Flow

  1. POST /v1/deployments/example-tenant/apply loads data/resources/tenants/example-tenant.json and creates a declarative deploy_service work order in data/work-orders/pending/.
  2. skippy-agent polls GET /v1/nodes/host-1/work-orders/next every 5 seconds and receives the next work order for host-1.
  3. The agent writes the compose file to /opt/skipper/tenants/example-tenant/docker-compose.yml, writes .env, and runs docker compose up -d.
  4. The agent sends POST /v1/work-orders/:id/result with a structured result object plus state updates.
  5. skipper-api moves the finished work order into data/work-orders/finished/, updates resource state, writes structured logs, and emits events.