Configuring a workstation

Go beyond the defaults: declarative Workstation and WorkstationConfig manifests, a browser IDE, devtools, ports, and more.

A bare Workstation gets you a clean machine. Real environments are described declaratively, in YAML, and applied with rl apply. This page shows the two-resource model and builds up a fully configured browser-IDE workstation.

For exhaustive field tables, see the Reference Guide.

Two resources: config layer + instance

Ringleader splits what to install from which machine:

  • A WorkstationConfig is a reusable configuration layer: image, packages, devtools, ports, identity, and so on.
  • A Workstation is an instance. It can carry config inline, but usually it just matches a config.

A config attaches to a workstation in one of two ways:

  1. By selector: the config’s spec.selector.matchLabels match the workstation’s metadata.labels. No reference needed.
  2. By explicit reference: the workstation lists it in spec.configs.
# By selector — any Workstation labeled tier: dev picks this up automatically,
# including ones created later. This is the default to reach for.
spec:
  selector:
    matchLabels:
      tier: dev
# By explicit reference — the Workstation names the config directly. Reach
# for this when a config belongs to exactly one workstation, not a group.
spec:
  configs:
    - name: ide
      namespace: dev

Selector matching is the one to default to: label a workstation once and it inherits every config that targets that label, including configs added later. Use explicit reference for a config meant for one specific workstation, such as a one-off you do not want any other workstation to pick up. The browser-IDE example further down this page uses selector matching.

All matching configs plus the workstation’s own spec are merged into one effective configuration (lower priority applies first), which rl workstation get-resolved-configuration <name> prints on demand.

Where manifests live

Ringleader does not care where your manifest file sits on disk. apply reads the local path you give it (rl apply -f path/to/workstation.yaml), and the workstation and its configs are what get stored, not the file itself. In practice, keep the manifest in your project’s repository, typically alongside the code it supports (a deploy/ or .ringleader/ folder both work fine), and commit it like any other config. That is what makes an environment reproducible and reviewable: anyone on the team can rl apply -f the same file and get the same workstation, and a change to the environment shows up as a diff in a pull request instead of living only on your machine.

A browser-IDE workstation

The manifest below (a YAML file you write and apply) boots a Debian workstation with Node.js, the Claude Code and Codex CLIs, and VS Code Web (code-server) auto-started on port 8080, and forwards that port back to your Mac so the IDE is reachable at http://127.0.0.1:8080.

apiVersion: core.ringleader.dev/v1
kind: Namespace
metadata:
  name: dev
---
apiVersion: workstations.ringleader.dev/v1
kind: WorkstationConfig
metadata:
  name: ide
  namespace: dev
spec:
  # Attach to every Workstation in this namespace whose labels match.
  selector:
    matchLabels:
      tier: dev
  priority: 100

  image:
    os: linux
    distribution: debian
    version: "13"

  # The agent owns user creation; default the login shell.
  identity:
    shell: /bin/bash

  # OS packages. Each entry is a bare name or {name, version, updatePolicy}.
  packages:
    - git
    - name: curl
      updatePolicy: latest

  # Curated install recipes, installed by the in-VM devtool registry.
  devtools:
    - name: nodejs        # prerequisite for the npm-installed CLIs
    - name: claude-code
    - name: codex
    - name: vscode-web

  # Per-tool configuration pushed to already-installed tools. Here: serve
  # code-server on :8080 with no auth (the loopback-only forward fronts it).
  toolconfigs:
    - id: vscode-web
      name: vscode-web
      config:
        port: 8080
        auth: none

  # Declared listening ports.
  ports:
    - 8080

  # Unattended security upgrades (Debian: unattended-upgrades).
  securityUpdates: true

  # Forwarding is not implicit. This template makes the daemon seed a
  # device-local LocalBinding once, when the workstation first reaches Running, that
  # forwards every listening port (incl. :8080) back to the host 1:1.
  defaultLocalBinding:
    enabled: true
    autoForward:
      forwardAll: true
---
apiVersion: workstations.ringleader.dev/v1
kind: Workstation
metadata:
  name: ide-box
  namespace: dev
  labels:
    tier: dev          # matches the `ide` config's selector
spec:
  # VM sizing for local providers (qemu -m / -smp). NOTE: providerConfig is
  # read from the Workstation spec only — it is NOT merged from config layers —
  # so it must live here, not on the WorkstationConfig. A browser IDE plus an
  # AI agent needs real headroom.
  providerConfig:
    memory: 6   # GiB
    cpus: 2

Apply it and wait for it to finish setting up:

rl apply -f ide-box.yaml
rl workstation wait ide-box -n dev --for Ready --timeout 15m

The devtool installs are real npm/code-server network installs (a few minutes on a fresh VM); the workstation reports Configured once it has applied them. Then open http://127.0.0.1:8080 in your browser.

providerConfig is not overlaid

Most fields merge from config layers, but providerConfig is resolved from the Workstation spec only. Put VM sizing (memory, cpus) and cloud providerConfig.gcp settings directly on the Workstation, not on a shared WorkstationConfig.

More configuration

The same config fields can be set on either a WorkstationConfig or a Workstation. A few common ones:

Environment variables and dotfiles

spec:
  environment:
    EDITOR: vim
    GOPATH: /home/dev/go
  dotfiles:
    .bashrc: |
      alias ll='ls -alF'

Pin a provider or image

By default Ringleader chooses a provider for you (automatic placement). Pin one explicitly when you need to:

spec:
  provider: gcp                 # shorthand for requirements: [provider:gcp]
  image:
    distribution: debian
    version: "13"

Stop and restart declaratively

spec:
  stopped: true            # halt the machine; it rests in the Stopped phase

Set stopped: false (or remove it) to start it again. To force a restart of a running workstation, change spec.restartNonce to any new value.

Where to go next

  • Tutorials: want the hands-on, step-by-step version of what you just built? Start there and work up to running an AI coding agent inside the workstation.
  • CLI Reference: every command for driving these resources.
  • Reference Guide: the full field tables for each kind.