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.