Configuration reference
Every field in cell.yaml and config.yaml.
Files
~/.config/solitary/
config.yaml user-wide defaults
cells/<name>/cell.yaml one cell
cells/<name>/.env that cell's secret values, host-side onlyA cell's name is the name of its directory, never a field inside the file, so the two can never disagree. solitary init <name> creates the directory and a commented cell.yaml.
config.yaml holds vm:, git: and network: blocks used as the defaults for every cell, and gpu:, which says which of the host's GPUs a cell that asks for one renders with — see gpu. A cell that sets network.allow replaces the user-wide list rather than adding to it: what a cell may reach should be readable in one place.
A complete cell
description: >- # optional; what this cell is for, at most 350 characters
Claude Code with neovim and tmux, on a network that reaches
anthropic, github and npm and nothing else.
image: ghcr.io/you/nvim-claude:latest
# build: ./Containerfile # or build it instead — set one, not both
command: sleep infinity # optional; must not exit — it is the container's life
secrets: # only these names are passed into the cell
CLAUDE_API_KEY:
GITHUB_TOKEN:
required: false
ports: # omit and every listening port reaches host localhost;
- 8080 # set and only these do
devices: # optional; device nodes in the machine to pass into the cell
- /dev/kvm
gpu: true # optional; render OpenGL and Vulkan on the host's GPU
network: # omit and the cell reaches whatever the host reaches;
allow: # set and it reaches these and nothing else
- github.com
- api.anthropic.com
resolvers: # optional; defaults to 1.1.1.1 and 8.8.8.8
- 10.0.0.53
vpn: ./vpn.conf # optional; send all of it through this WireGuard tunnel
git: # optional; usually set once in config.yaml instead
name: Ada Lovelace
email: ada@example.com
vm: # optional; falls back to config.yaml, then to built-ins
cpus: 4
memory: 8GiBWhat it is
description
What this cell is for, in a sentence or two. clone shows it before the definition is installed and the dashboard shows it above everything else about the cell, so it is written for someone who has not met the cell yet — what it holds, and what it is allowed to reach.
description: >-
Claude Code with neovim and tmux, on a network that reaches
anthropic, github and npm and nothing else.At most 350 characters, which is a few lines wherever a cell is listed. Line breaks are folded away, so >- above and | are the same as one long line: write it in whichever shape reads best in the file. A cell with no description is fine — nothing that shows one leaves a gap for it — but a published cell has an audience, and this is the only field written for them.
The toolset
image
A published container image holding the cell's tools. Exactly one of image or build is required.
Changing it and running up again replaces the container while keeping the VM, its disk, and the cell's home. The container is the toolset, not the boundary.
build
A Containerfile to build instead, as a path relative to the cell's directory. That directory becomes the build context: it is copied into the machine and built there, so nothing in a Containerfile ever runs on the host. .git and node_modules are left out of the copy, and the copy is deleted afterwards so a context that carried build-time secrets does not linger.
up rebuilds when anything in the context changes and reuses the image when nothing has.
Do not set both image and build.
command
The command the container runs. It must not exit — it is the container's life, and shells opened by up or shell are separate from it. Defaults to sleep infinity.
user
The user work happens as inside the cell: the one shell and exec land on, and the one the cell's home belongs to.
user: cellLeave it out and everything in the cell runs as root, which is what almost every cell wants: the machine is the boundary, and root inside a rootless container is an unprivileged user in the machine, so nothing is gained by giving up the ability to install a package.
Set it when something in the cell refuses to run as root — limactl does — or when the image serves a login of its own — an sshd, an editor server — and hands someone a session as a user it created. Then up mounts the home through a view in which it belongs to that user, while the machine still sees it as its own; without it the home belongs to the container's root and everything that user does in it fails on a permission. The container's command still runs as whatever the image declared, since that is usually the root the server needs.
The user has to exist in the image, and giving it what it needs is the image's half of this — see the user inside a cell.
Authority
secrets
The environment variable names this cell is allowed to see, as a mapping of name to options:
secrets:
CLAUDE_API_KEY:
GITHUB_TOKEN:
required: false
description: pushing branches and opening pull requests| Option | Default | Meaning |
|---|---|---|
required | true | The cell will not start until the name has a value. |
description | none | What the value is for. Shown when up asks for it and in the dashboard. |
A name written on its own — CLAUDE_API_KEY:, with nothing under it — takes both defaults, so it is required and undescribed. Set required: false for a credential the cell can work without: up still offers it, pressing enter skips it, and the variable is simply absent inside the container.
Values come from cells/<name>/.env, which stays on the host and is never copied into the VM; the values are passed to the container as environment variables when it starts. The file may hold more than a cell needs — only the names that cell declares are ever passed in. up asks for any that are missing, and solitary secrets <name> sets or rotates them later.
Because the values live on the host, rm followed by up gives you a clean cell that is still authenticated.
ports
Which guest ports reach the host. When empty, every port the container listens on is reachable on host localhost. When set, only these are forwarded.
ports:
- 8080
- 3000devices
Device nodes in the machine to pass into the container, as absolute paths under /dev. A cell reaches none of the machine's own devices without this.
devices:
- /dev/kvmThe node has to be one the machine already has, so this is not a way to reach the host's hardware — it is for what the guest kernel offers. /dev/kvm is the one worth naming: where the host allows nested virtualisation, it lets a cell run a virtual machine of its own.
Passing the node through is only half of what is needed, and up does the other half. The container is rootless, so its root is the machine's user on the other side; a node the guest kernel left to root is one the cell would get and be unable to open. So each declared node is handed to that user at every up — every one, because a reboot makes the nodes again as the distribution says. The group is left alone and nothing is made world-readable.
A device is fixed once a container is created, so changing this list replaces the container the way changing image or secrets does.
A cell that runs a guest of its own needs two more things beside the node. Its shared memory has to hold the guest — see vm, which sizes it — and it has to be able to run the emulator as someone other than root, because lima refuses to run as root and a cell is root in its container until user says otherwise.
A device is authority, in the way a secret is. The machine stays the boundary — a cell given /dev/kvm can run a guest of its own, not read the host's disks — but this is the one setting that reaches past the container into the machine, so clone shows it before a definition is installed. Name only the nodes a cell needs.
gpu
Gives the cell a GPU to render with. The machine gets a virtual GPU, and the host does its work on a real one: OpenGL through virgl, Vulkan through Venus. The host keeps its GPU, and any number of cells can share it. Only rendering crosses over — CUDA, a video encoder, or anything else that needs the vendor's own stack does not.
gpu: trueIt is a yes or no, not a device, because which GPU is a fact about the host rather than the cell. solitary picks one: a discrete card before an integrated one, named by its /dev/dri/by-path entry so that it is the same card after a reboot. solitary doctor says which it would use. A host with more than one GPU, where the guess is wrong, names its choice in config.yaml:
gpu: /dev/dri/by-path/pci-0000:01:00.0-render # ls -l /dev/dri/by-path lists themA host that cannot give a GPU — no render node, a node this user cannot open, QEMU older than 11 or without Venus, or not Linux — starts the cell anyway, says so, and the cell renders on the CPU. The same definition starts on every host.
On Linux, GPU mode needs QEMU 11 or newer built with OpenGL and virglrenderer Venus support. On x86-64, solitary enables KVM's guest PAT setting so Venus can read GPU-written memory through the guest mapping. With an Intel host CPU and a discrete GPU, this also needs a Linux host kernel of 6.16 or newer; see Mesa's Venus requirements. If the QEMU on your PATH is older, set QEMU_SYSTEM_X86_64 to the path of a suitable binary before running solitary up.
The machine sees one GPU and makes /dev/dri/renderD128 for it, and the container is given that node without it being listed under devices. The image needs Mesa's drivers to use it — on Ubuntu, mesa-vulkan-drivers, libgl1-mesa-dri and libegl-mesa0; vulkaninfo --summary in the cell then names the host's card as Virtio-GPU Venus (…), and eglinfo -B as virgl (…).
The GPU is part of the machine, so turning it on or off takes effect at the next boot, like vm. Lima has no setting for it, so solitary passes the QEMU arguments through QEMU_SYSTEM_<ARCH> at every start — Lima logs a warning that this is a debugging aid each time — and a machine started with limactl start directly boots without one.
A GPU is authority in the way a device is, and more of it: what the cell renders is run on the host, by the host's GPU driver, inside qemu. clone shows it before a definition is installed. See security.
network
| Field | Meaning |
|---|---|
allow | Domains, IPv4 addresses, and IPv4 CIDR blocks the cell may open connections to. Empty means the cell reaches whatever the host reaches; set means default-deny. A domain covers its subdomains. |
resolvers | The DNS servers the cell's own resolver forwards to. Empty means 1.1.1.1 and 8.8.8.8. The entry host means the resolver the machine is given by its network, which is the host's. |
vpn | A wg-quick configuration, as a path relative to the cell's directory. Everything the cell reaches then leaves through that tunnel. |
Each entry under resolvers must be an IPv4 address or the literal host; anything else is refused when the cell is read.
IPv6 entries are refused in allow, resolvers, and a tunnel's Endpoint. Restricted and VPN-only cells drop non-loopback IPv6 packets. Unrestricted VMs may have an IPv6 route. See networking and routing a cell through a VPN.
The machine
vm
Every field is optional and falls back to config.yaml, then to the defaults built into the binary:
| Field | Default | Meaning |
|---|---|---|
base | ubuntu-lts | The Lima image template. |
cpus | 2 | |
memory | 4GiB | On Linux this must fit /dev/shm; see troubleshooting. |
disk | 20GiB | Grows at the next boot. It cannot shrink: a value below what the machine already has is refused, naming both ways out. |
provision | — | A shell script run once, as root, after the built-in podman setup. A value here replaces the user-wide one rather than appending to it. |
vm:
cpus: 4
memory: 8GiB
provision: |
apt-get install -y build-essentialThe vm block describes settings a machine reads at boot. Change it while a cell is running and up warns that the running cell still uses the old ones; down then up applies them without touching the disk.
provision is the exception. The new script runs at the next start, but what the old one did is already on the machine's disk, and nothing undoes it — a package it installed stays installed. up warns when the script changed, and the dashboard says so in the cell's detail view; only rm then up gets a machine built by the current script alone, and that discards the disk and the cell's home with it. See changing a cell for which settings land in which layer.
git
git:
name: Ada Lovelace
email: ada@example.comA cell has nowhere of its own to keep a git identity: nothing is mounted from the host, and anything configured by hand inside a cell is gone when it is rebuilt. So git: is passed in as environment variables, which git reads ahead of any config file. Git wants an author and a committer, each with a name and an email, and has no single setting for both — solitary fills in all four from these two fields.
Write it once in config.yaml and every cell commits as you.
Input and boundary checks
vm.base is an image template name containing letters, digits, dots, underscores or dashes. build and network.vpn must be relative paths inside the cell directory; symlink components are refused. Build reads remain confined to that directory.
Allow entries must be IPv4 addresses/CIDRs or ASCII DNS names with labels of at most 63 characters, up to 253 characters overall. Punycode names, private single-label names and an optional trailing dot are supported. Tunnel endpoint ports must be integers from 1 to 65535. WireGuard shell hooks and SaveConfig are refused; the latter could overwrite the managed configuration.
A forwarded localhost service is reachable by other local users and processes. Browser reachability also depends on browser and service protections. Require authentication on editors, terminals and other privileged services.
Solitary rejects nonempty Lima _config/default.yaml and _config/override.yaml settings because Lima can merge host mounts into an empty mounts list. Use a separate LIMA_HOME for cells if other Lima machines require global settings. solitary doctor checks stored cell definitions for unsafe mounts, copy rules, networks and forwards.