Skip to content

Instantly share code, notes, and snippets.

@GeekTrainer
Last active March 8, 2026 22:51
Show Gist options
  • Select an option

  • Save GeekTrainer/949f8d198ef9a2b9f90253f17ec8ce1c to your computer and use it in GitHub Desktop.

Select an option

Save GeekTrainer/949f8d198ef9a2b9f90253f17ec8ce1c to your computer and use it in GitHub Desktop.
Docker Sandbox setup for GitHub Copilot CLI — reusable template with pre-installed deps on ext4

Docker Sandbox for GitHub Copilot CLI

Run GitHub Copilot CLI in an isolated Docker Sandbox with pre-installed dependencies. This avoids native binary conflicts when node_modules is synced between your host and the Linux-based sandbox via virtiofs.

Prerequisites

  • Docker Desktop 4.58+ with Sandboxes enabled
  • Python 3 (for config parsing in the startup script)

Setup

  1. Place all three files in a .dev/ directory at your project root.
  2. Edit sandbox.json with your project-specific values (see comments in that file).
  3. Edit Dockerfile.sandbox to install the system packages and dependencies your project needs.
  4. Run the sandbox:
.dev/start-copilot-sandbox.sh

How it works

  • Dockerfile.sandbox — Builds a custom image on top of docker/sandbox-templates:copilot. It pre-installs system packages and Node.js dependencies on the container's native ext4 filesystem, avoiding crashes from macOS/Windows native binaries synced via virtiofs.

  • start-copilot-sandbox.sh — Checks if the Docker image needs rebuilding (based on file timestamps), builds it if needed, then starts or reuses the sandbox.

  • sandbox.json — Project-specific config consumed by the startup script. Keeps the script itself fully generic and reusable across repos.

Why not just npm install inside the sandbox?

Docker Sandbox syncs your workspace bidirectionally. If node_modules exists on the host with native binaries built for macOS/Windows, those get synced into the Linux VM and crash. There's no .dockerignore-style exclusion for sandbox file sync. Pre-installing on ext4 in the image and symlinking is the workaround.

# Base image for Copilot CLI sandbox
FROM docker/sandbox-templates:copilot
USER root
# CUSTOMIZE: Install system-level dependencies your project needs.
# Examples:
# Python/Flask:
# RUN apt-get update && apt-get install -y --no-install-recommends \
# python3-flask python3-sqlalchemy && rm -rf /var/lib/apt/lists/*
# Ruby:
# RUN apt-get update && apt-get install -y ruby-full && rm -rf /var/lib/apt/lists/*
# CUSTOMIZE: Install test tooling if needed (e.g., Playwright browsers).
# RUN mkdir -p /usr/local/share/npm-global/lib && npx playwright install --with-deps chromium
USER agent
# Pre-install Node.js dependencies on native ext4 to avoid virtiofs binary conflicts.
# The startup script (via the sandbox-npm-install skill) will symlink node_modules
# from /home/agent/project-deps/node_modules into your project directory.
#
# CUSTOMIZE: Update the COPY path to match where your package.json lives.
COPY --chown=agent:agent package.json package-lock.json /home/agent/project-deps/
RUN cd /home/agent/project-deps && npm ci
{
// CUSTOMIZE: Set a unique image name for your project
"image_name": "my-project-sandbox",
// CUSTOMIZE: List dependency/config files that should trigger an image rebuild
// when changed. The Dockerfile.sandbox is always watched automatically.
"watch_files": [
"package.json",
"package-lock.json"
]
}
#!/usr/bin/env bash
# Generic startup script for Docker Sandbox with Copilot CLI.
# Reads project-specific config from sandbox.json in the same directory.
# No modifications needed — customize sandbox.json and Dockerfile.sandbox instead.
set -euo pipefail
SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
REPO_ROOT="$(cd "$SCRIPT_DIR/.." && pwd)"
CONFIG_FILE="$SCRIPT_DIR/sandbox.json"
DOCKERFILE="$SCRIPT_DIR/Dockerfile.sandbox"
cd "$REPO_ROOT"
if [ ! -f "$CONFIG_FILE" ]; then
echo "Error: sandbox.json not found in $SCRIPT_DIR" >&2
exit 1
fi
# Read config from sandbox.json
IMAGE_NAME=$(python3 -c "import signal; signal.signal(signal.SIGPIPE, signal.SIG_DFL); import json; print(json.load(open('$CONFIG_FILE'))['image_name'])")
WATCH_FILES=()
while IFS= read -r f; do
WATCH_FILES+=("$f")
done < <(python3 -c "
import signal; signal.signal(signal.SIGPIPE, signal.SIG_DFL)
import json
for f in json.load(open('$CONFIG_FILE')).get('watch_files', []):
print(f)
")
WATCH_FILES+=("$DOCKERFILE")
needs_build() {
# Build if image doesn't exist
if ! docker image inspect "$IMAGE_NAME" &>/dev/null; then
echo "Image '$IMAGE_NAME' not found."
return 0
fi
# Get image creation timestamp
image_time=$(docker image inspect "$IMAGE_NAME" --format '{{.Created}}')
image_epoch=$(date -d "$image_time" +%s 2>/dev/null || date -jf "%Y-%m-%dT%H:%M:%S" "${image_time%%.*}" +%s 2>/dev/null)
# Check if any watched files are newer than the image
for f in "${WATCH_FILES[@]}"; do
if [ -f "$f" ]; then
file_epoch=$(stat -c %Y "$f" 2>/dev/null || stat -f %m "$f" 2>/dev/null)
if [ "$file_epoch" -gt "$image_epoch" ]; then
echo "Rebuild needed: '$f' is newer than image."
return 0
fi
fi
done
echo "Image '$IMAGE_NAME' is up to date."
return 1
}
REBUILT=false
if needs_build; then
echo "Building sandbox image..."
docker build -t "$IMAGE_NAME" -f "$DOCKERFILE" .
echo "Build complete."
REBUILT=true
fi
echo "Starting Copilot sandbox..."
# If sandbox already exists and image was rebuilt, remove the old sandbox
if [ "$REBUILT" = true ] && docker sandbox ls 2>/dev/null | grep -q "copilot"; then
echo "Removing old sandbox to apply new image..."
docker sandbox rm copilot 2>/dev/null || true
fi
# Use --template only when sandbox doesn't exist yet
if docker sandbox ls 2>/dev/null | grep -q "copilot"; then
exec docker sandbox run copilot
else
exec docker sandbox run --template "$IMAGE_NAME" copilot "$REPO_ROOT"
fi
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment