Skip to content

Commit 5e14d5e

Browse files
committed
feat: add persist-nix feature
1 parent 8ed5cbe commit 5e14d5e

5 files changed

Lines changed: 208 additions & 0 deletions

File tree

src/persist-nix/README.md

Lines changed: 55 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,55 @@
1+
# Persist Nix store (persist-nix)
2+
3+
Retain the Nix store (`/nix/store`) across container rebuilds to speed up installation of nix-based dotfiles.
4+
5+
## What this feature does
6+
7+
This feature persists the Nix store directory that is used when managing dotfiles with Nix. The persistent Docker volume significantly reduces download and build times during container rebuilds, especially when dotfiles are heavily managed through Nix.
8+
9+
## How it works
10+
11+
### Build phase (as root)
12+
13+
The `install.sh` script creates a Docker volume and mount point:
14+
- `/.persist-nix-store` – Volume for the Nix store
15+
16+
The directory is created with permissions `777` so all users can access it.
17+
18+
### User initialization phase (in user context)
19+
20+
The `persist-nix-init.sh` script (runs via `postCreateCommand`) performs:
21+
22+
1. **Nix availability check**: Verifies Nix is installed; skips initialization if not available
23+
2. **Backup of existing data**: If `/nix/store` exists as a regular directory, it is renamed to `/nix/store.bak` (preserving any existing store)
24+
3. **Symlink creation**: Creates a symlink from `/nix/store` to the persistent volume `/.persist-nix-store`
25+
26+
This approach ensures:
27+
- **No data loss**: Existing Nix store is backed up before being replaced
28+
- **Automatic recovery**: Symlinks persist across rebuilds; no repeated setup needed
29+
- **Dependency compatibility**: Only runs if Nix is present; safe to include in devcontainers without Nix
30+
31+
## Example Usage
32+
33+
```json
34+
"features": {
35+
"ghcr.io/ckagerer/devcontainer-features/persist-nix:1": {
36+
"keep_going": false
37+
}
38+
}
39+
```
40+
41+
## Notes
42+
43+
- **Nix dependency**: This feature requires Nix to be installed in the container. If Nix is not present, the feature initializes safely and exits without error.
44+
- **Sudo requirement**: The symlink creation uses `sudo` as it modifies the `/nix` directory structure. Ensure your user can execute `sudo` commands, or run the container with elevated privileges.
45+
- **Volume lifecycle**: The persistent volume is managed by Docker and is not automatically deleted when the container is removed. To clear cached data, manually delete the Docker volume `devcontainer-persist-nix-store`.
46+
47+
## Clearing the cache
48+
49+
To clear the Nix store cache and reclaim disk space:
50+
51+
```bash
52+
docker volume rm devcontainer-persist-nix-store
53+
```
54+
55+
The volume will be automatically recreated on the next container build.
Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
{
2+
"name": "persist-nix",
3+
"id": "persist-nix",
4+
"version": "1.0.0",
5+
"description": "Persist Nix store across container rebuilds to speed up nix-based dotfiles.",
6+
"documentationURL": "https://github.com/ckagerer/devcontainer-features/tree/main/src/persist-nix",
7+
"options": {
8+
"keep_going": {
9+
"type": "boolean",
10+
"default": false,
11+
"description": "Ignore errors during setup and continue execution."
12+
}
13+
},
14+
"mounts": [
15+
{
16+
"source": "devcontainer-persist-nix-store",
17+
"target": "/.persist-nix-store",
18+
"type": "volume"
19+
}
20+
],
21+
"postCreateCommand": "/usr/local/share/persist-nix-init.sh"
22+
}

src/persist-nix/install.sh

Lines changed: 72 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,72 @@
1+
#!/usr/bin/env sh
2+
3+
# Initialize Nix store volume directory
4+
# This script runs as root during container build
5+
6+
if [ "${KEEP_GOING:-false}" = "true" ]; then
7+
set +e
8+
else
9+
set -e
10+
fi
11+
set -x
12+
13+
# Create Nix store mount point with permissions for all users
14+
mkdir -p /.persist-nix-store
15+
chmod 777 /.persist-nix-store
16+
17+
# Generate the postCreateCommand script that runs in user context
18+
INIT_SCRIPT_PATH="/usr/local/share/persist-nix-init.sh"
19+
20+
tee "$INIT_SCRIPT_PATH" >/dev/null <<'EOF'
21+
#!/usr/bin/env sh
22+
23+
# Initialize Nix store symlinks
24+
# This script runs in user context (postCreateCommand)
25+
26+
set -e
27+
set -x
28+
29+
# Check if a path is a mount point
30+
is_mount_point() {
31+
if command -v mountpoint >/dev/null 2>&1; then
32+
mountpoint -q "$1"
33+
else
34+
# Fallback for systems without mountpoint command
35+
mount | grep -q " on $(readlink -f "$1") "
36+
fi
37+
return $?
38+
}
39+
40+
# Only proceed if Nix is installed
41+
if ! command -v nix >/dev/null 2>&1; then
42+
echo "Nix not found, skipping persist-nix initialization"
43+
exit 0
44+
fi
45+
46+
# Handle existing /nix/store directory
47+
if [ -d /nix/store ] && [ ! -L /nix/store ]; then
48+
# Check if it's already a mount point (e.g., from another devcontainer feature)
49+
if is_mount_point /nix/store 2>/dev/null; then
50+
echo "Note: /nix/store is already mounted by another feature, skipping backup"
51+
else
52+
echo "Backing up existing /nix/store to /nix/store.bak"
53+
sudo mv /nix/store /nix/store.bak
54+
fi
55+
fi
56+
57+
# Ensure /nix directory exists
58+
sudo mkdir -p /nix
59+
60+
# Create symlink from persistent volume to /nix/store
61+
if [ ! -L /nix/store ]; then
62+
echo "Creating symlink /nix/store -> /.persist-nix-store"
63+
sudo ln -s /.persist-nix-store /nix/store
64+
else
65+
echo "/nix/store symlink already exists"
66+
fi
67+
68+
echo "persist-nix initialization complete"
69+
EOF
70+
71+
# Make the init script executable
72+
chmod +x "$INIT_SCRIPT_PATH"

test/persist-nix/scenarios.json

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
{
2+
"scenarios": [
3+
{
4+
"name": "default",
5+
"description": "Test persist-nix with default settings"
6+
}
7+
]
8+
}

test/persist-nix/test.sh

Lines changed: 51 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,51 @@
1+
#!/usr/bin/env bash
2+
3+
# Test script for persist-nix feature
4+
# This script verifies that the persist-nix feature correctly sets up
5+
# the Nix store persistence layer
6+
7+
set -e
8+
set -x
9+
10+
# Test 1: Check if the mount point was created
11+
if [ ! -d /.persist-nix-store ]; then
12+
echo "ERROR: /.persist-nix-store directory not found"
13+
exit 1
14+
fi
15+
echo "✓ /.persist-nix-store directory exists"
16+
17+
# Test 2: Check if the init script was created
18+
if [ ! -f /usr/local/share/persist-nix-init.sh ]; then
19+
echo "ERROR: /usr/local/share/persist-nix-init.sh not found"
20+
exit 1
21+
fi
22+
echo "✓ /usr/local/share/persist-nix-init.sh exists"
23+
24+
# Test 3: Check if the init script is executable
25+
if [ ! -x /usr/local/share/persist-nix-init.sh ]; then
26+
echo "ERROR: /usr/local/share/persist-nix-init.sh is not executable"
27+
exit 1
28+
fi
29+
echo "✓ /usr/local/share/persist-nix-init.sh is executable"
30+
31+
# Test 4: If Nix is installed, verify symlink exists
32+
if command -v nix >/dev/null 2>&1; then
33+
if [ -L /nix/store ]; then
34+
echo "✓ /nix/store is a symlink (Nix installed)"
35+
# Verify it points to the persistent volume
36+
if [ "$(readlink /nix/store)" = "/.persist-nix-store" ]; then
37+
echo "✓ /nix/store correctly points to /.persist-nix-store"
38+
else
39+
echo "ERROR: /nix/store does not point to /.persist-nix-store"
40+
exit 1
41+
fi
42+
else
43+
echo "ERROR: /nix/store is not a symlink (Nix is installed but symlink not created)"
44+
exit 1
45+
fi
46+
else
47+
echo "ℹ Nix not installed, symlink test skipped"
48+
fi
49+
50+
echo ""
51+
echo "All tests passed!"

0 commit comments

Comments
 (0)