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 — Tailscale runs in-process via tsnet, so the new
rootfs does not need to ship tailscaled or tailscale binaries.
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 - Re-execs itself as
--init(PID 1 in the new rootfs), brings uptsnet/SSH if requested, then execs the entrypoint
The old root is accessible at /mnt/oldroot by default.
Download from releases.
Binaries are pure-Go statics (CGO_ENABLED=0) for x86_64, aarch64,
and armv7:
curl -LO https://github.com/ananthb/xmorph/releases/latest/download/xmorph-x86_64-linux
chmod +x xmorph-x86_64-linux
sudo mv xmorph-x86_64-linux /usr/local/bin/xmorphEach release includes SHA256SUMS for verification.
nix run github:ananthb/xmorph -- --helpRequires Go 1.26+:
CGO_ENABLED=0 go build -o xmorph ./cmd/xmorph
sudo cp xmorph /usr/local/bin/# Default alpine image
sudo xmorph pivot
# Specific image
sudo xmorph pivot --image ubuntu:22.04
# Local rootfs tarball
sudo xmorph pivot --rootfs ./my-rootfs.tar.gz
# Merge multiple layers (later wins on conflict)
sudo xmorph pivot --image alpine:latest --rootfs ./extra-files/
# Custom entrypoint and command
sudo xmorph pivot --image alpine:latest --entrypoint /bin/sh --command -c --command "echo hello"
# Dry run
sudo xmorph pivot --dry-run# Cache only (pre-warm for fast pivot later)
sudo xmorph build --image alpine:latest
# Write OCI layout to disk
sudo xmorph build --image alpine:latest -o my-image.oci
# Also write a rootfs tarball
sudo xmorph build --image alpine:latest -o my-image.oci --rootfs-output rootfs.tar.gzsudo xmorph pivot --containerfile ./Containerfile
sudo xmorph 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 xmorph pivot --headless --tailscale.authkey tskey-auth-xxxxxThis forks into the background, pivots the root filesystem, and brings
up Tailscale (via tsnet) inside the new rootfs. Reconnect via Tailscale
SSH: ssh root@<hostname>-xmorph.
Use an ephemeral auth key so the node is automatically removed from your tailnet when xmorph is done.
The --headless flag:
- Forks and detaches from the terminal (
setsid) - Logs to
/var/log/xmorph.log(configurable with--log-dir) - Prints the Tailscale hostname and PID before forking
- Implies
--force(no confirmation prompt)
xmorph integrates with systemd as a rescue target service. When the system enters rescue mode, xmorph 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.xmorph.url = "github:ananthb/xmorph";
outputs = { self, nixpkgs, xmorph, ... }: {
nixosConfigurations.myhost = nixpkgs.lib.nixosSystem {
modules = [
xmorph.nixosModules.default
{
services.xmorph = {
enable = true;
package = xmorph.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:
xmorph-cache-warm.service— runs onmulti-user.targetto pre-pull imagesxmorph-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/:
xmorph-pivot.service— pivots onrescue.targetxmorph-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 xmorph-pivot.service xmorph-cache-warm.serviceEdit the pivot service to configure your images and Tailscale auth key:
sudo systemctl edit xmorph-pivot.service[Service]
ExecStart=
ExecStart=/usr/local/bin/xmorph 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=xmorph sets CACHE_DIRECTORY so xmorph uses
/var/cache/xmorph automatically.
xmorph caches built rootfs images at the configured cache directory
(default /var/cache/xmorph, 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 xmorph 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.