---
title: Self-hosting a spindle
description: Run your own spindle CI runner, including microVM workflows.
---


## Prerequisites

- Go
- For the **nixery** engine: Docker (or Podman with Docker
  compatibility enabled).
- For the **microVM** engine: a Linux host with KVM, plus the
  microVM host dependencies described in [Running microVM
  workflows](#running-microvm-workflows).

## Configuration

Spindle is configured using environment variables. The following environment variables are available:

- `SPINDLE_SERVER_LISTEN_ADDR`: The address the server listens on (default: `"0.0.0.0:6555"`).
- `SPINDLE_SERVER_DB_PATH`: The path to the SQLite database file (default: `"spindle.db"`).
- `SPINDLE_SERVER_HOSTNAME`: The hostname of the server (required).
- `SPINDLE_SERVER_JETSTREAM_ENDPOINT`: The endpoint of the Jetstream server (default: `"wss://jetstream1.us-west.bsky.network/subscribe"`).
- `SPINDLE_SERVER_DEV`: A boolean indicating whether the server is running in development mode (default: `false`).
- `SPINDLE_SERVER_OWNER`: The DID of the owner (required).
- `SPINDLE_SERVER_LOG_DIR`: The directory to store workflow logs (default: `"/var/log/spindle"`).
- `SPINDLE_SERVER_DOCKER_SOCKET`: Path to Docker socket to expose to invoked Spindle containers (default: `""`).
- `SPINDLE_PIPELINES_NIXERY`: The Nixery URL (default: `"nixery.tangled.sh"`).
- `SPINDLE_PIPELINES_WORKFLOW_TIMEOUT`: The default workflow timeout (default: `"5m"`).

For the microVM engine, the following are also available
(prefix `SPINDLE_MICROVM_PIPELINES_`):

- `SPINDLE_MICROVM_PIPELINES_IMAGE_DIR`: Directory containing
  microVM images (**required** to use the engine). See
  [Running microVM workflows](#running-microvm-workflows).
- `SPINDLE_MICROVM_PIPELINES_DEFAULT_IMAGE`: Image used when a
  workflow doesn't set `image` (default: `"nixos-x86_64"`).
- `SPINDLE_MICROVM_PIPELINES_OVERLAY_DIR`: Where per-workflow
  temporary disks are created (default: the system temp dir).
- `SPINDLE_MICROVM_PIPELINES_ENABLE_KVM`: Use KVM hardware
  acceleration (default: `true`). Without KVM, guests fall
  back to slow software emulation.
- `SPINDLE_MICROVM_PIPELINES_WORKFLOW_TIMEOUT`: Default
  workflow timeout (default: `"5m"`).

Optional resource limits (a value of `0` disables that
limit). The limits cap usage across all running microVM
workflows:

- `SPINDLE_MICROVM_PIPELINES_MAX_TOTAL_MEMORY_MIB`
- `SPINDLE_MICROVM_PIPELINES_MAX_TOTAL_VCPUS`
- `SPINDLE_MICROVM_PIPELINES_MAX_TOTAL_DISK_MIB`

Optional cgroup enforcement:

- `SPINDLE_MICROVM_PIPELINES_ENABLE_CGROUPS`: Place each
  workflow's QEMU and slirp4netns in a per-workflow cgroup=
  (default: `false`).
- `SPINDLE_MICROVM_PIPELINES_CGROUP_PARENT`: Parent cgroup;
  `self` resolves the spindle service's own cgroup (default:
  `"self"`).
- `SPINDLE_MICROVM_PIPELINES_CGROUP_PIDS_MAX`: Max processes
  per workflow cgroup (default: `4096`).
- `SPINDLE_MICROVM_PIPELINES_CGROUP_SWAP_MAX_MIB`: Max swap
  per workflow cgroup (default: `0`, no swap).
- `SPINDLE_MICROVM_PIPELINES_CGROUP_SUPERVISOR_MEMORY_MIN_MIB`:
  Memory protected for spindle itself so it isn't OOM-killed
  before the workflows (default: `512`).

To push paths built inside microVMs back to a shared Nix
cache (and read from it), configure the cache (prefix
`SPINDLE_NIX_CACHE_`):

- `SPINDLE_NIX_CACHE_READ_URLS`: Comma-separated binary cache
  URLs the guest reads from.
- `SPINDLE_NIX_CACHE_TRUSTED_PUBLIC_KEYS`: Comma-separated
  trusted public keys for those caches.
- `SPINDLE_NIX_CACHE_UPLOAD_URL`: Cache URL that paths built
  in the guest are uploaded to.

## Running spindle

1.  **Set the environment variables.** For example:

    ```shell
    export SPINDLE_SERVER_HOSTNAME="your-hostname"
    export SPINDLE_SERVER_OWNER="your-did"
    ```

2.  **Build the Spindle binary.**

    ```shell
    cd core
    go mod download
    go build -o cmd/spindle/spindle cmd/spindle/main.go
    ```

3.  **Create the log directory.**

    ```shell
    sudo mkdir -p /var/log/spindle
    sudo chown $USER:$USER -R /var/log/spindle
    ```

4.  **Run the Spindle binary.**

    ```shell
    ./cmd/spindle/spindle
    ```

Spindle will now start, connect to the Jetstream server, and begin processing pipelines.

## Running microVM workflows

The microVM engine needs a few extra things on the host, and
it needs images to boot.

### Host dependencies

microVM workflows depend on a handful of host tools and
devices. spindle checks for the ones an image needs right
before it launches, so a missing dependency surfaces as a
clear error. You'll need:

- `qemu`: the runner. The QEMU binary for the image's arch
  must be present (e.g. `qemu-system-x86_64`).
- `mkfs.ext4` (from `e2fsprogs`): to format the per-workflow
  writable volumes.
- [`slirp4netns`](https://github.com/rootless-containers/slirp4netns#install),
  `ip` (from `iproute2`), `mount` and `unshare` (from `util-linux`):
  used to sandbox guest networking.
- `/dev/kvm`: for hardware acceleration (unless you disable
  KVM with `SPINDLE_MICROVM_PIPELINES_ENABLE_KVM=false`).
- `/dev/vhost-vsock`: used by QEMU to enable guest-to-host vsock
  communication.
- `/dev/vsock`: used by spindle on the host to listen on vsock ports
  and accept guest agent connections.
- `/dev/net/tun`: required by `slirp4netns` to set up tap devices
  for sandboxed guest networking.

On NixOS, the [spindle
module](https://tangled.org/tangled.org/core/blob/master/nix/modules/spindle.nix)
puts `qemu`, `e2fsprogs`, `slirp4netns`, `iproute2` and
`util-linux` on the service's `PATH` for you.

### Container virtualization

Running `spindle` inside a container (e.g., Docker, Podman, or LXD) requires passing host device nodes into the container and granting the runtime additional privileges. Because spindle uses nested namespaces (`unshare`) and helper tools (`slirp4netns`), the container configuration will require:

- `/dev/vsock`, `/dev/vhost-vsock`, `/dev/kvm`, and `/dev/net/tun` mapped from the host.
- `NET_ADMIN` and `SYS_ADMIN` capabilities to manage network namespaces and mounts inside the container.
- Relaxed seccomp filters (e.g., `seccomp=unconfined`) and SELinux/AppArmor containment if they restrict namespace creation or device access.

### Building images

Images are built with Nix. The flake exposes packages for the
two stock images (use the `-tarball` prefixed ones for a gzipped
tarball you can copy to another host):

```shell
# a NixOS image
nix build .#spindle-nixos-image
# an Alpine image
nix build .#spindle-alpine-image
```

### Installing images

Spindle looks for images in
`SPINDLE_MICROVM_PIPELINES_IMAGE_DIR`. An image is resolved by
the name a workflow puts in its `image` field, matched
literally against what's on disk:

1. a directory `<name>/` containing a `spec.json` (next to the
   kernel/initrd/store-disk), or
2. a flat `<name>.json` self-contained spec.

Resolution depends only on the name and what's on disk, never
on the host doing the resolving, so the same workflow resolves
to the same image on every spindle. If you keep multiple
arches side by side, you can name them `<name>-<arch>` (e.g.
`nixos-x86_64`, `alpine-aarch64`); the suffix is just part of
the name. To make a name like `nixos` work if you are hosting
multiple arches, you can use symlinks.

On NixOS, you'll most likely want to use `systemd.tmpfiles.rules`
to set these up declaratively.

