Replaces a running Linux root filesystem with a new in-memory rootfs built from OCI (Docker) images, rootfs tarballs, and Containerfiles. The old root is kept around for inspection and modification.
Works as a Linux rescue environment integrating with systemd's
rescue.target. Supports headless mode with Tailscale for SSH access
after the pivot.
Coordinates with systemd, OpenRC, and SysVinit to gracefully stop services before pivoting.
- Pulls OCI images and/or extracts rootfs tarballs into a RAM-backed tmpfs
- Merges multiple layers in order (later wins on file conflicts)
- Coordinates with the init system to stop services
- Calls
pivot_root(2)to atomically swap the root filesystem - Execs the entrypoint in the new rootfs
The old root is accessible at /mnt/oldroot by default.
Download from releases. Binaries are fully static (musl libc) for x86_64, aarch64, and armv7:
curl -LO https://github.com/ananthb/xenomorph/releases/latest/download/xenomorph-x86_64-linux
chmod +x xenomorph-x86_64-linux
sudo mv xenomorph-x86_64-linux /usr/local/bin/xenomorphEach release includes SHA256SUMS for verification.
nix run github:ananthb/xenomorph -- --helpRequires Zig 0.15.x:
zig build -Doptimize=ReleaseSafe
sudo cp zig-out/bin/xenomorph /usr/local/bin/# Default alpine image
sudo xenomorph pivot
# Specific image
sudo xenomorph pivot --image ubuntu:22.04
# Local rootfs tarball
sudo xenomorph pivot --rootfs ./my-rootfs.tar.gz
# Merge multiple layers (later wins on conflict)
sudo xenomorph pivot --image alpine:latest --rootfs ./extra-files/
# Custom entrypoint and command
sudo xenomorph pivot --image alpine:latest --entrypoint /bin/sh --command -c --command "echo hello"
# Dry run
sudo xenomorph pivot --dry-run# Cache only (pre-warm for fast pivot later)
sudo xenomorph build --image alpine:latest
# Write OCI layout to disk
sudo xenomorph build --image alpine:latest -o my-image.oci
# Also write a rootfs tarball
sudo xenomorph build --image alpine:latest -o my-image.oci --rootfs-output rootfs.tar.gzsudo xenomorph pivot --containerfile ./Containerfile
sudo xenomorph build --containerfile ./Dockerfile --context ./app/Supported instructions: FROM, COPY, ADD, ENV, WORKDIR,
ENTRYPOINT, CMD, LABEL, ARG, EXPOSE, VOLUME, USER.
RUN is not yet supported.
For pivoting over SSH without losing your connection:
sudo xenomorph pivot --headless --tailscale-authkey tskey-auth-xxxxxThis forks into the background, pivots the root filesystem, and starts
Tailscale in the new rootfs. Reconnect via ssh root@<hostname>-xenomorph.
Use an ephemeral auth key so the node is automatically removed from your tailnet when xenomorph is done.
The --headless flag:
- Forks and detaches from the terminal (
setsid) - Logs to
/var/log/xenomorph.log(configurable with--log-dir) - Prints the Tailscale hostname and PID before forking
- Implies
--force(no confirmation prompt)
xenomorph integrates with systemd as a rescue target service. When the system enters rescue mode, xenomorph pivots to the configured rootfs.
A cache warmup service runs during normal boot to pre-pull images, so the pivot is instant when rescue.target is reached.
{
inputs.xenomorph.url = "github:ananthb/xenomorph";
outputs = { self, nixpkgs, xenomorph, ... }: {
nixosConfigurations.myhost = nixpkgs.lib.nixosSystem {
modules = [
xenomorph.nixosModules.default
{
services.xenomorph = {
enable = true;
package = xenomorph.packages.x86_64-linux.default;
# Images to merge into the rescue rootfs
images = [ "docker.io/library/alpine:latest" ];
# Tailscale for SSH access after pivot
tailscale = {
enable = true;
authKeyFile = "/run/secrets/tailscale-key";
};
# Pre-warm cache on boot (default: true)
warmupBuildCache = true;
};
}
];
};
};
}This creates two systemd services:
xenomorph-cache-warm.service— runs onmulti-user.targetto pre-pull imagesxenomorph-pivot.service— runs onrescue.targetto pivot into the new rootfs
Trigger the pivot with:
sudo systemctl isolate rescue.targetService files are included in the release tarball under
init/systemd/:
xenomorph-pivot.service— pivots onrescue.targetxenomorph-cache-warm.service— pre-warms cache on boot
Install them:
sudo cp init/systemd/*.service /etc/systemd/system/
sudo systemctl daemon-reload
sudo systemctl enable xenomorph-pivot.service xenomorph-cache-warm.serviceEdit the pivot service to configure your images and Tailscale auth key:
sudo systemctl edit xenomorph-pivot.service[Service]
ExecStart=
ExecStart=/usr/local/bin/xenomorph pivot --systemd-mode --force --image alpine:latest --tailscale-authkey tskey-auth-xxxxxThe --systemd-mode flag skips init coordination and process termination
(systemd has already stopped services when entering rescue.target).
CacheDirectory=xenomorph sets CACHE_DIRECTORY so xenomorph uses
/var/cache/xenomorph automatically.
xenomorph caches built rootfs images at the configured cache directory
(default /var/cache/xenomorph, overridable with --cache-dir or the
CACHE_DIRECTORY environment variable).
The cache key is derived from the normalized layer list. Repeated
pivot or build invocations with the same layers skip all image
pulls and layer merging.
Use xenomorph build with no output flags to pre-warm the cache.
- Linux kernel with
pivot_rootsupport - Root privileges (
CAP_SYS_ADMIN) - Network access for pulling registry images
Licensed under the terms of the AGPL-3.0.