macOS Developer Setup (VZ)¶
This is the in-depth VZ guide: the full setup, including building from source and custom images. The VZ backend uses Apple's Virtualization.framework to run Linux VMs on macOS via vfkit.
Just want shed running?
Use the macOS Quickstart — Homebrew + shed-desktop, no source build. This page is for contributors, custom images, or running an unreleased commit.
Prerequisites¶
- macOS 13+ (Ventura) on Apple Silicon (arm64)
- Docker (for VM image management)
Intel macOS support is not currently available.
Homebrew Install (Recommended)¶
The fastest way to get started. Homebrew handles vfkit, code signing, config generation, and service management.
1. Install¶
This installs shed (CLI) and shed-server, installs the vfkit dependency, generates a default server config (image refs resolve from the server version — no pinned tags to bump on upgrade), and codesigns the server binary.
For credential brokering (SSH agent forwarding, AWS credentials, Docker registry auth):
2. Configure¶
Edit the server config to enable mounts and extensions:
Uncomment the mounts section to mount tool configs into VMs, and the extensions section if you installed shed-host-agent. (The older credentials key still works as a fallback when mounts is absent, but mounts is preferred.)
3. Start services¶
4. Create a test shed¶
The first shed create pulls the OCI image registry-direct from
ghcr.io and materializes the flattened erofs lower via host-native
mkfs.erofs --tar=f. The pull step dominates wallclock on the first
run (registry I/O); subsequent creates from the same manifest mount
the cached erofs in under a second.
Service management¶
Logs are at $(brew --prefix)/var/log/shed-server.log and $(brew --prefix)/var/log/shed-host-agent.log.
Upgrading from v0.4.x¶
The image store schema changed from v2 to v3 with the OCI image rollout. Pre-v3 sheds, snapshots, and cached blobs are not migrated automatically — the in-guest initramfs rejects them.
To upgrade:
brew services stop shed
# Wipe the legacy store. Backup first if anything in there is precious.
rm -rf ~/Library/Application\ Support/shed/vz/blobs
rm -rf ~/Library/Application\ Support/shed/vz/instances
rm -rf ~/Library/Application\ Support/shed/vz/snapshots
rm -rf ~/Library/Application\ Support/shed/vz/uppers
brew upgrade charliek/tap/shed
brew services start shed
sudo shed-server pull-images
Workspace data under --local-dir mounts is unaffected. Workspace data
that lived only inside the deleted upper layers is lost, by design.
Build from Source (Alternative)¶
Use this if you're contributing to shed or need a custom build.
1. Install vfkit¶
2. Build shed-server¶
3. Set up VZ images¶
Published images (recommended)¶
Shed pulls published OCI references registry-direct (no Docker daemon needed) and stacks the layers as overlayfs lowers on first boot.
You usually don't need to name them. When default_image and
image_aliases are omitted, the server resolves them from its own
version — ghcr.io/charliek/shed-vz-{base,extensions,full}:vX.Y.Z — so
the config never carries a version to bump. A make build from a clean
release tag (git describe yields vX.Y.Z) is release-shaped and gets
this automatically:
vz:
pull_policy: missing
# default_image / image_aliases omitted -> resolved from the server version
If your tree is ahead of a tag or dirty (a typical contributor build), there is no matching published image, so pin an explicit ref to the release whose images you want to pull:
vz:
default_image: ghcr.io/charliek/shed-vz-full:v0.6.2
image_aliases:
base: ghcr.io/charliek/shed-vz-base:v0.6.2
extensions: ghcr.io/charliek/shed-vz-extensions:v0.6.2
full: ghcr.io/charliek/shed-vz-full:v0.6.2
pull_policy: missing
To pin a custom or mirrored registry while still tracking the server
version, use the ${shed.version} token (expands to the running server's
tag on release builds):
Check https://github.com/charliek/shed/pkgs/container/shed-vz-full for
published tags. Pre-v0.5.0 published images use the legacy flattened
layout and will not work with v0.5.0+ shed-server.
The first shed create pulls the layer blobs plus the pre-built rootfs
erofs blob (built at publish time, not on the host since v0.5.2) and
boots from it directly. Subsequent sheds from the same manifest reuse the
cached blobs; images that share layer blobs skip re-downloading them.
Build images from source¶
This builds the full variant. Build other variants with --variant:
./scripts/build-vz-rootfs.sh --variant base # Minimal image
./scripts/build-vz-rootfs.sh --variant extensions # Base + credential brokering
./scripts/build-vz-rootfs.sh --all # All variants
The script writes directly into the local blob store and advances the
base, extensions, and full tags. Confirm with shed image ls.
See Images for details. The OCI image
store lives under ~/Library/Application Support/shed/vz/ — see
Storage Model and the
upgrade-and-reclaim cookbook for how to manage disk space.
Configure server for local builds¶
When you build images locally, label them with shed image build/pull -t
<label> and reference the label. You can either set default_image to a
label (used when no --image is passed) or omit it and pass --image
<label> on every create:
vz:
vfkit_path: vfkit
instance_dir: ~/Library/Application Support/shed/vz/instances
socket_dir: ~/.shed/vz/sockets
default_image: full # a local label, or a ghcr.io/...:vX ref
Then create (omit --image to use default_image, or override per shed):
default_image may be a Docker ref (ghcr.io/...:vX), an image_aliases
name, a local label set with -t, or an absolute path to an ext4/erofs
image. Docker refs are pulled on first use per pull_policy.
4. Create directories¶
5. Configure the server¶
Create ~/.config/shed/server.yaml:
name: my-mac
http_port: 8080
ssh_port: 2222
default_backend: vz
vz:
vfkit_path: vfkit
# kernel_path and initrd_path are optional fallbacks for OCI images.
# The published / locally-built image blobs include their own kernel
# and initramfs via manifest annotations; these fields only apply if
# you're booting a legacy raw-rootfs image (rare).
# kernel_path: ~/Library/Application Support/shed/vz/vmlinux
# initrd_path: ~/Library/Application Support/shed/vz/initrd.img
# default_image / image_aliases omitted -> resolved from the server
# version (see "Published images" above). On an ahead-of-tag or dirty
# build, pin them explicitly instead.
pull_policy: missing
instance_dir: ~/Library/Application Support/shed/vz/instances
socket_dir: ~/.shed/vz/sockets
default_cpus: 2
default_memory_mb: 4096
default_disk_gb: 20
console_port: 1024
notify_port: 1026
tcp_proxy_port: 1028
start_timeout: 60s
stop_timeout: 10s
mounts:
claude:
source: ~/.shed/mounts/claude
target: /home/shed/.claude
readonly: false
env_file: ~/.shed/env
6. Code signing¶
The shed-server binary needs the com.apple.security.virtualization entitlement:
7. Start the server¶
Pass --config explicitly when your config lives outside the current
working directory. shed-server without --config only looks at
./server.yaml (CWD), not ~/.config/shed/server.yaml.
8. Create a test shed¶
shed server add registers the server under the name: field from
server.yaml, not literally as localhost. Confirm with
shed server list before creating a shed.
shed server add localhost
shed server list # shows the registered name (e.g. "my-mac")
shed create test --image full # required when default_image is omitted
shed console test
Configuration Reference¶
See VZ Configuration for all available fields.
How It Works¶
The VZ backend launches each VM as a vfkit subprocess. Communication with the guest uses vsock over per-port Unix sockets (one socket per port, named <name>-<port>.sock). This differs from Firecracker, which uses a single multiplexed socket with a CONNECT/OK handshake.
Networking uses NAT provided by Virtualization.framework. The guest obtains an IP via DHCP through systemd-networkd. From the host's perspective, the guest IP isn't routable; service traffic from the host goes through DialService's vsock TCP-proxy hop. shed list and the API report 127.0.0.1 as the shed's IP so the field has a sensible value, but no host code dials that address.
The rootfs is a standard ext4 image, same as Firecracker. Each instance gets its own copy.
Troubleshooting¶
"vfkit not found"
: Install vfkit with brew install vfkit or add it to your PATH.
Code signing errors
: Re-sign the binary: codesign --entitlements internal/vz/entitlements.plist -s - ./bin/shed-server
"Virtualization.framework not available" : Check that you're running macOS 13+ (Ventura or later).
VM fails to boot
: Verify kernel_path, initrd_path, and default_image point to valid files (or a pullable ref). Check that the rootfs was built successfully. Check the console log at <instance_dir>/<name>/console.log for boot messages.
Health check timeout
: Check that vsock socket files exist in ~/.shed/vz/sockets/. Verify vfkit is running with ps aux | grep vfkit. Check the console log for systemd boot errors.
Permission denied : Ensure the entitlements plist is applied to the binary via code signing.