feat(openclaw): add Nix-built Docker image with app extraction from upstream
Pure Nix buildLayeredImage that extracts /app from upstream ghcr.io/openclaw/openclaw via manifest-aware Python script. Avoids fromImage which breaks Debian dynamic linker by shadowing /lib -> usr/lib symlink. Includes: nix, nodejs_22, kubectl, jq, curl, git, emacs, python3+pymupdf, tea. Custom NSS with node user (UID 1000). Replicated docker-entrypoint.sh. Ultraworked with [Sisyphus](https://github.com/code-yeongyu/oh-my-openagent) Co-authored-by: Sisyphus <clio-agent@sisyphuslabs.ai>
This commit is contained in:
@@ -304,6 +304,8 @@
|
||||
"custom-nextcloud-talk-desktop" = pkgs.custom.nextcloud-talk-desktop;
|
||||
# nix-deck kernel from Jovian-NixOS (Steam Deck) - expensive to build
|
||||
"nix-deck-kernel" = self.nixosConfigurations.nix-deck.config.boot.kernelPackages.kernel;
|
||||
# OpenClaw docker image (pulled + augmented with nix tools)
|
||||
"openclaw-image" = pkgs.custom.openclaw-image;
|
||||
}
|
||||
else
|
||||
{ }
|
||||
|
||||
@@ -8,4 +8,5 @@
|
||||
pi-coding-agent = pkgs.callPackage ./pi-coding-agent { };
|
||||
nextcloud-talk-desktop = pkgs.callPackage ./nextcloud-talk-desktop { };
|
||||
opencode = pkgs.callPackage ./opencode { };
|
||||
openclaw-image = pkgs.callPackage ./openclaw-image { };
|
||||
}
|
||||
|
||||
224
packages/openclaw-image/default.nix
Normal file
224
packages/openclaw-image/default.nix
Normal file
@@ -0,0 +1,224 @@
|
||||
{
|
||||
pkgs,
|
||||
lib,
|
||||
}:
|
||||
|
||||
let
|
||||
# Pin the upstream openclaw image version
|
||||
# Updated by Renovate when new versions appear in flake.lock
|
||||
openclawImageTag = "2026.4.14";
|
||||
openclawImageDigest = "sha256:7ea070b04d1e70811fe8ba15feaad5890b1646021b24e00f4795bd4587a594ed";
|
||||
|
||||
# Pull the upstream openclaw Docker image (only to extract /app from it)
|
||||
openclawBase = pkgs.dockerTools.pullImage {
|
||||
imageName = "ghcr.io/openclaw/openclaw";
|
||||
imageDigest = openclawImageDigest;
|
||||
sha256 = "sha256-mSAa7lmciD6OXd3KHr8pf2VJ0aHPGnjdGbeu+oFhNo8=";
|
||||
finalImageTag = openclawImageTag;
|
||||
os = "linux";
|
||||
arch = "amd64";
|
||||
};
|
||||
|
||||
# Extract the openclaw application (/app) from the upstream Docker image.
|
||||
#
|
||||
# We don't use fromImage because Nix's copyToRoot creates a /lib/ directory
|
||||
# that shadows the Debian base image's /lib -> usr/lib symlink, breaking
|
||||
# the glibc dynamic linker (/lib64/ld-linux-x86-64.so.2 chain).
|
||||
# Instead, we extract only /app from the upstream image layers, then build
|
||||
# a pure Nix image where everything runs against Nix's glibc.
|
||||
#
|
||||
# Docker image tars use the Image Manifest v2 format: each layer is a
|
||||
# separate .tar within the outer tar. We extract all layers to find /app.
|
||||
openclawApp =
|
||||
pkgs.runCommand "openclaw-app"
|
||||
{
|
||||
nativeBuildInputs = [
|
||||
pkgs.python3
|
||||
pkgs.gnutar
|
||||
];
|
||||
}
|
||||
''
|
||||
mkdir -p $out/app
|
||||
|
||||
# Extract all layers from the Docker image tarball
|
||||
mkdir workdir
|
||||
tar xf ${openclawBase} -C workdir
|
||||
|
||||
# Python: parse manifest, generate shell script to extract /app from layers
|
||||
python3 << 'PYEOF'
|
||||
import json
|
||||
|
||||
with open("workdir/manifest.json") as f:
|
||||
manifest = json.load(f)
|
||||
|
||||
layers = manifest[0]["Layers"]
|
||||
|
||||
with open("extract.sh", "w") as script:
|
||||
script.write("#!/bin/sh\nset -e\n")
|
||||
for layer in layers:
|
||||
if layer.endswith(".tar"):
|
||||
lp = f"workdir/{layer}"
|
||||
else:
|
||||
lp = f"workdir/{layer}/layer.tar"
|
||||
|
||||
script.write(f"""
|
||||
if [ -f '{lp}' ]; then
|
||||
if tar tf '{lp}' 2>/dev/null | grep -q '^app/'; then
|
||||
echo "Extracting /app from {layer}..." >&2
|
||||
tar xf '{lp}' -C "$OUT_DIR" --strip-components=0 app/ 2>/dev/null || true
|
||||
fi
|
||||
fi
|
||||
""")
|
||||
PYEOF
|
||||
|
||||
OUT_DIR="$out" sh extract.sh
|
||||
|
||||
if [ ! -f "$out/app/openclaw.mjs" ]; then
|
||||
echo "ERROR: /app/openclaw.mjs not found after extraction"
|
||||
ls -la $out/app/ 2>/dev/null || true
|
||||
exit 1
|
||||
fi
|
||||
echo "Successfully extracted openclaw app"
|
||||
'';
|
||||
|
||||
# Python environment with pymupdf
|
||||
pythonEnv = pkgs.python3.withPackages (ps: [ ps.pymupdf ]);
|
||||
|
||||
# Custom NSS files that include the "node" user (UID 1000, GID 1000).
|
||||
# fakeNss only creates root/nobody, so we create our own with all three.
|
||||
openclawNss = pkgs.runCommand "openclaw-nss" { } ''
|
||||
mkdir -p $out/etc
|
||||
cat > $out/etc/passwd << 'EOF'
|
||||
root:x:0:0:root user:/var/empty:/bin/sh
|
||||
nobody:x:65534:65534:nobody:/var/empty:/bin/sh
|
||||
node:x:1000:1000::/home/node:/bin/bash
|
||||
EOF
|
||||
|
||||
cat > $out/etc/group << 'EOF'
|
||||
root:x:0:
|
||||
nobody:x:65534:
|
||||
node:x:1000:
|
||||
EOF
|
||||
|
||||
cat > $out/etc/shadow << 'EOF'
|
||||
root:!x:::::::
|
||||
nobody:!x:::::::
|
||||
node:!x:::::::
|
||||
EOF
|
||||
'';
|
||||
|
||||
# Node user home directory
|
||||
nodeHome = pkgs.runCommand "node-home" { } ''
|
||||
mkdir -p $out/home/node
|
||||
'';
|
||||
|
||||
# Docker entrypoint script — equivalent to the upstream docker-entrypoint.sh.
|
||||
# We replicate it as a Nix derivation to avoid extracting the Debian binary
|
||||
# layer and to avoid filesystem conflicts in the image customization layer.
|
||||
dockerEntrypoint = pkgs.writeShellScript "docker-entrypoint.sh" ''
|
||||
#!/bin/sh
|
||||
set -e
|
||||
|
||||
# Run command with node if the first arg contains a "-" or is not a
|
||||
# system command. The last part inside the "{}" is a workaround for
|
||||
# the following bug in ash/dash:
|
||||
# https://bugs.debian.org/cgi-bin/bugreport.cgi?bug=874264
|
||||
if [ "''${1#-}" != "$1" ] || [ -z "$(command -v "''${1}")" ] || { [ -f "''${1}" ] && ! [ -x "''${1}" ]; }; then
|
||||
set -- node "$@"
|
||||
fi
|
||||
|
||||
exec "$@"
|
||||
'';
|
||||
|
||||
# Wrap the entrypoint as a derivation so it can be placed via copyToRoot
|
||||
# instead of extraCommands (which can't write to paths that already have
|
||||
# Nix store symlinks from other contents)
|
||||
entrypointPkg = pkgs.runCommand "docker-entrypoint" { } ''
|
||||
mkdir -p $out/usr/local/bin
|
||||
cp ${dockerEntrypoint} $out/usr/local/bin/docker-entrypoint.sh
|
||||
chmod +x $out/usr/local/bin/docker-entrypoint.sh
|
||||
'';
|
||||
|
||||
in
|
||||
pkgs.dockerTools.buildLayeredImage {
|
||||
name = "openclaw";
|
||||
tag = openclawImageTag;
|
||||
|
||||
# Don't use fromImage — see openclawApp derivation comment
|
||||
maxLayers = 120;
|
||||
|
||||
contents = [
|
||||
# System basics
|
||||
pkgs.bashInteractive
|
||||
pkgs.coreutils
|
||||
pkgs.cacert
|
||||
|
||||
# Custom NSS with node user
|
||||
openclawNss
|
||||
|
||||
# Node user home directory
|
||||
nodeHome
|
||||
|
||||
# The openclaw application extracted from the upstream image
|
||||
openclawApp
|
||||
|
||||
# Docker entrypoint script (in /usr/local/bin)
|
||||
entrypointPkg
|
||||
|
||||
# Runtime package manager (agents can `nix run` arbitrary packages)
|
||||
pkgs.nix
|
||||
|
||||
# Tools baked into the image
|
||||
pkgs.kubectl
|
||||
pkgs.jq
|
||||
pkgs.curl
|
||||
pkgs.git
|
||||
pkgs.emacs
|
||||
|
||||
# Node.js 22+ (for openclaw runtime, QMD when packaged, matrix-bot-sdk)
|
||||
pkgs.nodejs_22
|
||||
|
||||
# Python with pymupdf (PDF-to-image for Claude vision)
|
||||
pythonEnv
|
||||
|
||||
# Gitea CLI (PR workflow)
|
||||
pkgs.tea
|
||||
];
|
||||
|
||||
extraCommands = ''
|
||||
# Create /tmp with correct permissions (needed by Node.js and nix)
|
||||
mkdir -p tmp
|
||||
chmod 1777 tmp
|
||||
|
||||
# Create /run for nix-daemon socket
|
||||
mkdir -p run
|
||||
|
||||
# Create /var/empty (referenced by NSS passwd home dirs)
|
||||
mkdir -p var/empty
|
||||
'';
|
||||
|
||||
config = {
|
||||
Entrypoint = [ "docker-entrypoint.sh" ];
|
||||
Cmd = [
|
||||
"node"
|
||||
"openclaw.mjs"
|
||||
"gateway"
|
||||
"--allow-unconfigured"
|
||||
];
|
||||
WorkingDir = "/app";
|
||||
User = "node";
|
||||
|
||||
Env = [
|
||||
# SSL certificates
|
||||
"SSL_CERT_FILE=${pkgs.cacert}/etc/ssl/certs/ca-bundle.crt"
|
||||
"NIX_SSL_CERT_FILE=${pkgs.cacert}/etc/ssl/certs/ca-bundle.crt"
|
||||
|
||||
# Nix configuration
|
||||
"NIX_PATH=nixpkgs=flake:nixpkgs"
|
||||
|
||||
# PATH: standard dirs + Nix store bin dirs are appended by buildLayeredImage
|
||||
"PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin"
|
||||
"NODE_ENV=production"
|
||||
];
|
||||
};
|
||||
}
|
||||
Reference in New Issue
Block a user