Skip to content

Automated workspaces

slicer workspace gives a project its own microVM, described by a slicer.yaml file that lives in the repository. One command creates the VM, installs what the project needs, copies in your code, and starts its services. Edits on your machine are pushed into the VM in under a second.

slicer ws is a shorter alias for slicer workspace. Without a slicer.yaml, slicer workspace ./path still copies a directory into a fresh VM and opens a shell, as covered in Sandbox your agent in a microVM.

When to use it

Use slicer workspace where you'd otherwise write a docker-compose.yaml with a single container, just to get a project's toolchain off your machine. Typical examples are:

  • Blogs and static sites built with Jekyll, Hugo, or Eleventy
  • Documentation sites built with MkDocs or Docusaurus
  • Web front-ends with a dev server, such as Vite or Next.js
  • Small APIs and services that need a particular language version

With Docker Compose, you find an image with the right toolchain, mount your code into it, and work around the mount, i.e. polling for file changes because they don't reach the container's file watcher. With slicer workspace, slicer.yaml installs packages with apt-get, installs dependencies the way the project's README says to, and runs the dev server as a systemd service. Your code is copied onto the VM's own disk, so file watchers work normally, and there's nothing to install on your machine except Slicer.

Here's the OpenFaaS blog's setup with slicer workspace, and with Docker Compose:

name: openfaas-blog
setup: |
  apt-get update -qy
  apt-get install -qy --no-install-recommends ruby ruby-dev build-essential zlib1g-dev libyaml-dev libreadline-dev make gcc
  gem install -N bundler -v 2.2.2
init: |
  export BUNDLE_PATH=/home/ubuntu/.bundle
  bundle install --jobs 4 --retry 3
services:
  - name: blog
    command: bash -c 'bundle exec jekyll build && exec bundle exec jekyll serve --config _config.yml,_config_dev.yml --host 0.0.0.0 --port 4000'
    env:
      BUNDLE_PATH: /home/ubuntu/.bundle
ready:
  http: http://localhost:4000
forward:
  - "4000:4000"
services:
  openfaas-jekyll:
    command: >
      bash -lc "bundle check || bundle install && bundle exec jekyll build && bundle exec jekyll serve --config _config.yml,_config_dev.yml --force_polling --host 0.0.0.0 --port 4000"
    image: jekyll/jekyll:4
    volumes:
      - .:/srv/jekyll
      - site:/srv/jekyll/_site
    ports:
      - 4000:4000
    environment:
      - JEKYLL_UID=1000
      - BUNDLE_VERSION=system
      - BUNDLER_VERSION=2.6.9

volumes:
  site:

The slicer.yaml version needs no --force_polling, because the VM's file watcher sees each change as it's pushed. It also installs dependencies once in init, instead of checking them on every start, and slicer workspace up waits until the site is actually serving. The full file is below.

You want to... Use
Work on a project that needs its own toolchain and dev server slicer workspace
Get a Linux machine to try things in by hand, or for an agent to work in slicer vm add
Run one-shot jobs from your own code or scripts Tasks

You'll need a running Slicer daemon first. If you don't have one yet, follow the walkthrough.

Two different slicer.yaml files

The project's slicer.yaml describes one workspace, and lives in the project's repository. The daemon's slicer.yaml, generated by slicer new, configures Slicer itself and has a config.host_groups section. Keep them in separate directories.

Example: the OpenFaaS blog

The OpenFaaS blog is a Jekyll site. Building it needs Ruby, a C toolchain for native gems, and a few minutes of bundle install, none of which you'd want to install on your own machine. Its repository includes this slicer.yaml:

name: openfaas-blog
setup: |
  export DEBIAN_FRONTEND=noninteractive
  apt-get update -qy
  apt-get install -qy --no-install-recommends ruby ruby-dev build-essential zlib1g-dev libyaml-dev libreadline-dev make gcc
  gem install -N bundler -v 2.2.2
init: |
  export BUNDLE_PATH=/home/ubuntu/.bundle
  bundle install --jobs 4 --retry 3
services:
  - name: blog
    command: bash -c 'bundle exec jekyll build && exec bundle exec jekyll serve --config _config.yml,_config_dev.yml --host 0.0.0.0 --port 4000'
    env:
      JEKYLL_ENV: production
      PAGES_REPO_NWO: openfaas/openfaas.github.io
      BUNDLE_PATH: /home/ubuntu/.bundle
ready:
  http: http://localhost:4000
  timeout: 120s
forward:
  - "4000:4000"
exclude:
  - .git
  - _site
  - vendor
  - .bundle
  - .sass-cache
  - .jekyll-cache
  - node_modules
  - tmp
  - slicer
  - "*.img"
Field What it does
name The workspace's name. Two checkouts with the same name share one VM, so give them different names if you need two.
setup Runs as root when the VM is created, before your code is copied in. Install OS packages here.
init Runs as the ubuntu user inside your copied workspace, after setup. Install project dependencies here, i.e. bundle install or npm ci.
services Long-running processes. Each becomes a systemd unit named slicer-env-NAME that runs as ubuntu from the workspace, and restarts if it fails.
ready An http URL or an exec command, checked inside the VM, that tells Slicer the workspace is up.
forward Ports, or Unix sockets, to forward from the VM to your machine.
exclude Files and folders to leave out when copying the workspace, on top of any .slicerignore.
fetch Files to copy back to your machine once the workspace is ready, as remote:local.

Bring it up

From the project's directory:

git clone https://github.com/openfaas/openfaas.github.io
cd openfaas.github.io

slicer workspace up
- [ok] Discover host groups (0s)
- [ok] Create VM in vm (2s)
- [ok] Wait for agent on vm-2 (600ms)
- [ok] Wait for setup script on vm-2 (6s)
- [ok] Copy workspace to vm-2 (sent 93.97 MiB) (270ms)
- [ok] Fix workspace ownership on vm-2 (20ms)
- [ok] Run init script on vm-2 (25.33s)
- [ok] Install services on vm-2 (170ms)
- [ok] Wait for readiness on vm-2 (12.01s)
Workspace "openfaas-blog" is ready on vm-2
  IP: 192.168.137.3
  URL: http://localhost:4000

  Shell:  slicer workspace shell
  Push:   slicer workspace push
  Pull:   slicer workspace pull
  Status: slicer workspace status
  Down:   slicer workspace down

Direct access (Linux):
  http://192.168.137.3:4000

The whole blog, with every gem compiled, was being served 46 seconds after slicer workspace up was run. On Linux, open the direct URL. To use localhost instead, or when Slicer is on another machine, start the forwards in another terminal:

slicer workspace forward
Starting port forwarding...
  http://127.0.0.1:4000 → VM:4000

If more than one host group is available, pick one with slicer workspace up --hostgroup NAME.

Edit and push

Your project directory is copied to the same path under /home/ubuntu inside the VM. There's no shared mount, so push your changes when you want the VM to see them:

slicer workspace push
- [ok] Wait for agent on vm-2 (0s)
- [ok] Push workspace to vm-2 (sent 93.97 MiB) (450ms)
- [ok] Fix workspace ownership on vm-2 (30ms)
Push complete for "openfaas-blog" on vm-2

Or keep a terminal open that pushes each file as you save it:

slicer workspace push --watch
Pushed: _posts/2026-10-07-openfaas-surge-finance-platform.md (10ms)

Jekyll notices the change inside the VM and rebuilds, so an edited post is live about two seconds after you save it.

Use slicer workspace push --run-init when dependencies change, i.e. after editing Gemfile or package.json, and --restart-services to restart your services after the copy. slicer workspace pull copies the VM's workspace back to your machine, overwriting local files that have changed.

Check on it

slicer workspace status
WORKSPACE      VM    IP             AGENT  READY  SERVICES
openfaas-blog  vm-2  192.168.137.3  up     ready  blog=Active

READY PROBE                                FORWARD    FETCH
http http://localhost:4000 (timeout 120s)  4000:4000  -

slicer workspace shell opens a shell inside the VM in the workspace. Service logs are in the journal:

slicer vm exec vm-2 -- "journalctl -u slicer-env-blog -n 50 --no-pager"

slicer workspace list shows every workspace you've started on the daemon.

Tear it down

slicer workspace down
Deleted vm-2 (group vm, workspace "openfaas-blog")

This deletes the VM along with anything in it, so pull or slicer vm cp out anything you want to keep first. Workspaces aren't persistent, so treat the VM as disposable and the repository as the source of truth.

More examples

A static site with Python

The smallest useful workspace. Put an index.html next to it:

name: hello
setup: |
  apt-get update -qy
  apt-get install -qy --no-install-recommends python3
services:
  - name: web
    command: exec python3 -m http.server 8000 --bind 0.0.0.0
ready:
  http: http://localhost:8000
  timeout: 60s
forward:
  - "8000:8000"

PostgreSQL

A database for local development, reachable on localhost:5432 once you run slicer workspace forward:

name: postgresql
setup: |
  set -euo pipefail
  export DEBIAN_FRONTEND=noninteractive
  apt-get update -qy
  apt-get install -qy postgresql postgresql-client --no-install-recommends
  systemctl enable postgresql --now

  sudo -u postgres psql -c "ALTER USER postgres WITH PASSWORD 'secret';"
  echo "listen_addresses = '*'" >> /etc/postgresql/*/main/postgresql.conf
  echo "host all all 127.0.0.1/32 md5" >> /etc/postgresql/*/main/pg_hba.conf
  echo "host all all ::1/128 md5" >> /etc/postgresql/*/main/pg_hba.conf

  systemctl restart postgresql
init: |
  PGPASSWORD=secret psql -h 127.0.0.1 -U postgres -c "SELECT version();"
ready:
  exec: sudo -u postgres psql -c "SELECT 1"
  timeout: 30s
forward:
  - "5432:5432"

Kubernetes with K3s

A single-node cluster. fetch copies the kubeconfig back to your machine as ./kubeconfig:

name: k3s
setup: |
  set -euo pipefail

  arkade get k3sup kubectl --path /usr/local/bin
  chmod +x /usr/local/bin/*
  export PATH="/usr/local/bin:${PATH}"

  k3sup install --local
  mkdir -p /home/ubuntu/.kube
  cp kubeconfig /home/ubuntu/.kube/config
  chown -R ubuntu:ubuntu /home/ubuntu/

  k3sup ready --kubeconfig ./kubeconfig --pause 500ms --attempts 120
ready:
  exec: kubectl get nodes
  timeout: 30s
fetch:
  - /home/ubuntu/.kube/config:kubeconfig
forward:
  - "6443:6443"

Then, with slicer workspace forward running:

kubectl --kubeconfig ./kubeconfig get nodes

Redeploy a Helm chart on every push

A local Helm chart, deployed to K3s inside the VM. A deploy service runs helm upgrade --install, then waits for anything under ./chart to change, so every push redeploys the chart. It's a quick loop for developing a chart without a remote cluster or a container registry.

k3s-helm/
├── slicer.yaml
└── chart/
    ├── Chart.yaml
    ├── values.yaml
    └── templates/
        ├── configmap.yaml
        ├── deployment.yaml
        └── service.yaml
name: k3s-helm
setup: |
  set -euo pipefail
  export DEBIAN_FRONTEND=noninteractive
  apt-get update -qy
  apt-get install -qy --no-install-recommends inotify-tools

  arkade get k3sup kubectl helm --path /usr/local/bin
  chmod +x /usr/local/bin/*

  mkdir -p /home/ubuntu/.kube
  k3sup install --local --local-path /home/ubuntu/.kube/config
  chown -R ubuntu:ubuntu /home/ubuntu/.kube
  k3sup ready --kubeconfig /home/ubuntu/.kube/config --pause 500ms --attempts 120
services:
  - name: deploy
    command: |
      while true; do
        helm upgrade --install demo ./chart --wait --timeout 2m
        inotifywait -qq -r -e close_write,create,delete,move ./chart
      done
    env:
      KUBECONFIG: /home/ubuntu/.kube/config
ready:
  http: http://localhost:30080
  timeout: 180s
forward:
  - "3000:30080"
  - "6443:6443"
fetch:
  - /home/ubuntu/.kube/config:kubeconfig
message: Hello from K3s in a microVM
replicas: 1
apiVersion: v2
name: demo
version: 0.1.0
apiVersion: v1
kind: ConfigMap
metadata:
  name: demo
data:
  index.html: |
    <h1>{{ .Values.message }}</h1>
apiVersion: apps/v1
kind: Deployment
metadata:
  name: demo
spec:
  replicas: {{ .Values.replicas }}
  selector:
    matchLabels:
      app: demo
  template:
    metadata:
      labels:
        app: demo
      annotations:
        checksum/config: {{ include (print $.Template.BasePath "/configmap.yaml") . | sha256sum }}
    spec:
      containers:
        - name: web
          image: nginx:alpine
          ports:
            - containerPort: 80
          volumeMounts:
            - name: html
              mountPath: /usr/share/nginx/html
      volumes:
        - name: html
          configMap:
            name: demo
apiVersion: v1
kind: Service
metadata:
  name: demo
spec:
  type: NodePort
  selector:
    app: demo
  ports:
    - port: 80
      targetPort: 80
      nodePort: 30080

The checksum/config annotation changes whenever the ConfigMap does, so Kubernetes rolls out new pods instead of serving the old page.

Bring it up, then check the page:

slicer workspace up
curl -s http://192.168.137.3:30080
<h1>Hello from K3s in a microVM</h1>

K3s was running with the chart deployed 29 seconds after slicer workspace up. Now start pushing changes as you save them:

slicer workspace push --watch

Edit chart/values.yaml to change the message and set replicas: 2. About six seconds later, Helm has upgraded the release and both pods are serving the new page:

curl -s http://192.168.137.3:30080
<h1>Live from push --watch</h1>

The kubeconfig is fetched to ./kubeconfig. With slicer workspace forward running, the app is on http://127.0.0.1:3000, and you can use kubectl and helm from your own machine:

kubectl --kubeconfig ./kubeconfig get deploy
NAME   READY   UP-TO-DATE   AVAILABLE   AGE
demo   2/2     2            2           62s

A remote Docker daemon

Run Docker inside the VM and use it from your machine through a forwarded Unix socket:

name: docker
setup: |
  set -euo pipefail
  curl -sLS https://get.docker.com | sh
  systemctl enable docker --now
  usermod -aG docker ubuntu
ready:
  exec: docker info
  timeout: 60s
forward:
  - "~/.slicer/docker.sock:/var/run/docker.sock"

Because it forwards a socket, slicer workspace up stays running after the workspace is ready, and keeps the socket open until you press Control+C. In another terminal:

export DOCKER_HOST=unix://$HOME/.slicer/docker.sock
docker version

Let your agent do it

With Slicer's agent skills installed, your coding agent knows how to use slicer workspace. Ask it to write a slicer.yaml for your project, bring the workspace up, and push its changes into the VM as it works.