A feature-rich containerized development environment for safely using Claude Code CLI and other AI assistants in "dangerous mode" for multi-language development.
Container Images:
ghcr.io/gherlein/localdev:latest- Lightweight (Node.js LTS, Go, essential tools)ghcr.io/gherlein/localfull:latest- Full-featured (Java 17, Atlassian CLI, multiple Node versions)
Choose one of these methods to get started:
# Install launcher scripts
curl -fsSL https://raw.githubusercontent.com/gherlein/localdev/main/install.sh | bash
# Pull container image
podman pull ghcr.io/gherlein/localdev:latest
# Run
localdev# Pull container image
podman pull ghcr.io/gherlein/localdev:latest
# Extract launcher scripts from the image
podman run --rm -v ~/bin:/output ghcr.io/gherlein/localdev:latest \
sh -c 'cp /opt/localdev/bin/* /output/ && chmod +x /output/*'
# Run
localdev# Clone the repo
git clone https://github.com/gherlein/localdev.git
cd localdev
# Pull or build
make pull # Pull pre-built image
# OR
make build # Build locally
# Install scripts
make install
# Run
localdevThis Podman/Docker container provides an isolated environment where Claude Code and other AI assistants can operate with elevated permissions (--dangerously-skip-permissions) without compromising your host system. The container includes a comprehensive development toolchain for modern software development.
Two container variants are available:
| Container | Script | Image | Description |
|---|---|---|---|
| localdev (default) | localdev |
ghcr.io/gherlein/localdev:latest |
Lightweight, fast-starting container with Node.js LTS, Go, and essential tools. Uses isolated container networking. |
| localdevnet | localdevnet |
ghcr.io/gherlein/localdev:latest |
Same as localdev but runs with --network host, sharing the host's network stack directly. Use when the container needs to reach localhost services or LAN addresses. |
| localfull | localfull |
ghcr.io/gherlein/localfull:latest |
Full-featured container with Java 17, Atlassian CLI, multiple Node.js versions. Uses isolated container networking. |
- Debian Bookworm slim base (fast startup)
- Node.js LTS only (via nvm)
- Go toolchain with essential tools
- Claude Code CLI, TypeScript, pnpm, eslint, prettier
- No Java, no Atlassian CLI
- Eclipse Temurin 17 JDK base
- Multiple Node.js versions (14, 18, LTS)
- Full Go toolchain with all tools
- Atlassian CLI (acli)
- Marp CLI, mermaid-cli, Hugo, and more
- Security: Non-root user (developer) for best practices
- Architecture Support: AMD64 and ARM64
- USB Passthrough: Access to
/dev/bus/usbfor hardware development (Linux only, automatically disabled on macOS)
- Full Go toolchain with proper GOPATH configuration
- Development tools: goimports, godoc, gofumpt, govulncheck
- Linters: golangci-lint, staticcheck
- Debugging: Delve (dlv)
- Code generation: wire, gomodifytags, impl, gotests
- Mock generation: mockgen
- Formatting: golines (optional, may fail on some architectures)
- localdev: Node.js LTS only
- localfull: Node.js 14.16.0, 18.18.2, and LTS
- Package managers: npm, pnpm
- TypeScript with ts-node
- Code quality: eslint, prettier
- localfull only: webpack, esbuild, jest, vitest, nodemon, npm-check-updates
- Python 3 with pip
- uv package manager
- md2pdf tool
- Eclipse Temurin JDK 17 (base image, provides JVM for Atlassian CLI and other tools)
- Claude Code CLI (
@anthropic-ai/claude-code)- Global
~/.claudeconfiguration is natively available inside the container - Convenient alias:
clauded(runs with--dangerously-skip-permissions) - Alternative alias:
copilotd(runs with--allow-all-tools)
- Global
- GitHub Copilot CLI (
@github/copilot)
- Version Control: Git, GitHub CLI (gh)
- Atlassian: Atlassian CLI (acli) - localfull only
- Containerization: Podman with rootless configuration
- Documentation: Marp CLI, mermaid-cli, md-to-pdf, pdf2md, Hugo - localfull only
- Utilities: jq, tree, curl, build-essential, file, xxd, zip, unzip
- Multimedia: ffmpeg, imagemagick, qpdf
- Network: libpcap-dev
- USB: usbutils, libusb-1.0, udev
- Package Management: Homebrew - localfull only
This project uses Podman instead of Docker for license compatibility and rootless container support.
# Ubuntu/Debian
sudo apt-get install podman
# Or use the Makefile
make preSee podman.io for other platforms.
On macOS, Podman runs inside a virtual machine. The default memory allocation (2GB) is insufficient for building this container, which includes memory-intensive Go tool compilations (particularly buf and protoc-gen-go).
Recommended: 16GB for optimal build performance. Adjust based on your host system's available RAM.
# Stop the Podman machine
podman machine stop
# Set memory to 16GB (16384 MB)
podman machine set --memory 16384
# Start the machine
podman machine start
# Verify the configuration
podman machine listGuidelines for memory allocation:
- 16GB or less host RAM: Allocate 8GB (
--memory 8192) - 32GB or more host RAM: Allocate 16GB (
--memory 16384) (recommended) - 64GB or more host RAM: Allocate 24GB+ (
--memory 24576) for maximum performance
If you encounter "signal: killed" errors during Go tool installations, your Podman machine memory is too low.
make buildThis builds the lightweight ghcr.io/gherlein/localdev:latest container with:
- Memory limit: 16GB
- Automatic architecture detection (amd64/arm64)
- Pull latest base images
make build-fullThis builds the ghcr.io/gherlein/localfull:latest container with Java, Atlassian CLI, and additional tools.
make allmake no-cache # Both containers
make no-cache-default # Default container only
make no-cache-full # Full container onlymake default # Same as 'make build'
make full # Same as 'make build-full'# Detect architecture
ARCH=$(uname -m)
if [ "$ARCH" = "x86_64" ]; then TARGETARCH=amd64; else TARGETARCH=arm64; fi
# Build default (lightweight) container
podman build -t ghcr.io/gherlein/localdev:latest --memory=16g --build-arg TARGETARCH=$TARGETARCH --pull .
# Build full container
podman build -t ghcr.io/gherlein/localfull:latest --memory=16g --build-arg TARGETARCH=$TARGETARCH --pull -f Containerfile.full .After building locally, you can publish the containers to GitHub Container Registry (ghcr.io).
Note: These instructions are for project contributors who need to publish container images. If you're just using the containers, skip to Pulling and Running on Another Host.
You need:
- Push permissions to the
gherlein/localdevrepository - Authentication to GitHub Container Registry with package write permissions
If you already use gh CLI, this is the easiest method:
# Check if you're already authenticated
gh auth status
# Add package scopes to your existing token
gh auth refresh -s write:packages,read:packages
# This will open a browser - follow the prompts to authorize the additional scopesAfter authorizing, login to ghcr.io:
# Login using your gh token
gh auth token | podman login ghcr.io -u gherlein --password-stdinVerify the login:
podman login ghcr.io
# Should show: "Logged in to ghcr.io"If you don't use gh CLI or prefer a separate token:
- Create a Personal Access Token (classic) at: https://github.com/settings/tokens/new
- Give it a descriptive name (e.g., "localdev container publishing")
- Select these scopes:
- ✅
write:packages- Upload packages to GitHub Package Registry - ✅
read:packages- Download packages from GitHub Package Registry
- ✅
- Click "Generate token" and copy it immediately
Then login:
podman login ghcr.io -u gherlein
# When prompted for password, paste your Personal Access Token# Check stored credentials
cat ~/.config/containers/auth.json | grep ghcr.io
# Or verify you can pull (public images work without auth, but this confirms the credentials are valid)
podman pull ghcr.io/gherlein/localdev:latestOnce authenticated, you can publish containers:
# Publish latest (default)
make publish # Publishes ghcr.io/gherlein/localdev:latest
make publish-full # Publishes ghcr.io/gherlein/localfull:latest
make publish-all # Publish both containers
# Publish with semantic version (recommended for releases)
make publish VERSION=v1.0.0 # Publishes both v1.0.0 and latest
make publish-full VERSION=v1.2.0 # Publishes both v1.2.0 and latestSemantic Versioning:
- When
VERSIONis specified (e.g.,v1.0.0), the Makefile creates two tags: the version tag andlatest - Both tags are pushed to the registry
- Users can pin to specific versions (
ghcr.io/gherlein/localdev:v1.0.0) or use latest - Follow semantic versioning:
vMAJOR.MINOR.PATCH
Each publish command:
- Builds the container if not already built (or if source changed)
- Tags with specified VERSION (or
latestif not specified) - If VERSION is not
latest, also tags and pushes aslatest - Pushes all tags to
ghcr.io/gherlein/localdevorghcr.io/gherlein/localfull - Makes the image available for others to pull
Note: Authentication credentials persist in ~/.config/containers/auth.json, so you only need to login once per machine.
403 Forbidden error:
Error: trying to reuse blob sha256:... received unexpected HTTP status: 403 Forbidden
This means authentication failed. Solutions:
- Check you're logged in:
podman login ghcr.io - Verify your token has
write:packagesscope - Try logging out and back in:
podman logout ghcr.io && gh auth token | podman login ghcr.io -u gherlein --password-stdin
401 Unauthorized error:
Your token may have expired. Re-run the authentication steps above.
Image already exists:
If you need to republish the same version, the push should still work (it will reuse existing layers). If you get conflicts, you may need to delete the package version from GitHub and republish.
When creating a new release:
-
Update version-specific documentation (if any)
-
Build and test locally:
make build VERSION=v1.0.0 make run VERSION=v1.0.0 # Test the container thoroughly -
Publish to registry:
make publish VERSION=v1.0.0
-
Create GitHub Release:
-
Tag:
v1.0.0 -
Title:
Release v1.0.0 -
Description should include:
## Container Images ```bash # Pull specific version podman pull ghcr.io/gherlein/localdev:v1.0.0 podman pull ghcr.io/gherlein/localfull:v1.0.0 # Or pull latest podman pull ghcr.io/gherlein/localdev:latest podman pull ghcr.io/gherlein/localfull:latest
- List of changes...
-
Verify on registry:
- Check https://github.com/gherlein/localdev/pkgs/container/localdev
- Verify both
v1.0.0andlatesttags are visible
The containers include OCI annotations with standard metadata:
# View all labels/annotations
podman inspect ghcr.io/gherlein/localdev:latest | jq '.[0].Labels'
# View specific annotations
podman inspect ghcr.io/gherlein/localdev:latest | jq '.[0].Labels."org.opencontainers.image.source"'
podman inspect ghcr.io/gherlein/localdev:latest | jq '.[0].Labels."org.opencontainers.image.description"'You can pull and use the pre-built containers on any host without building locally.
# Pull latest version (default)
make pull
make pull-full
# Pull specific version
make pull VERSION=v1.0.0
make pull-full VERSION=v1.0.0
# Or manually with podman
podman pull ghcr.io/gherlein/localdev:latest
podman pull ghcr.io/gherlein/localfull:latest
# Pull specific version manually
podman pull ghcr.io/gherlein/localdev:v1.0.0
podman pull ghcr.io/gherlein/localfull:v1.0.0Method 1: One-Liner (Recommended)
# Download and install launcher scripts
curl -fsSL https://raw.githubusercontent.com/gherlein/localdev/main/install.sh | bash
# Pull container images
podman pull ghcr.io/gherlein/localdev:latest
podman pull ghcr.io/gherlein/localfull:latest # optionalMethod 2: Extract from Container
# Pull the image first
podman pull ghcr.io/gherlein/localdev:latest
# Extract scripts from container
podman run --rm -v ~/bin:/output ghcr.io/gherlein/localdev:latest \
sh -c 'cp /opt/localdev/bin/* /output/ && chmod +x /output/*'
# Or use the Makefile (requires cloning repo)
git clone https://github.com/gherlein/localdev.git
cd localdev
make pull
make install-scriptsMethod 3: Clone Repository
git clone https://github.com/gherlein/localdev.git
cd localdev
make pull # Or: make build to build locally
make installAll methods install the launcher scripts (localdev, localdevnet, localfull) to ~/bin/.
Verify Installation:
# Check scripts are installed
which localdev
# Run
localdevIf you just want to run the container without the launcher scripts:
# Pull the image
podman pull ghcr.io/gherlein/localdev:latest
# Run directly
podman run --rm -it \
--userns=keep-id \
-v "$(pwd):/workspace" \
-w /workspace \
ghcr.io/gherlein/localdev:latest \
bashThree scripts provide convenient access to the containers with automatic directory mounting and support for read-only external directories.
localdev- launches the lightweight container with isolated networking (default)localdevnet- launches the same lightweight container with--network host, sharing the host's network stack; use this when the container needs to connect to services running on localhost or your LANlocalfull- launches the full-featured container (Java, Atlassian CLI, multiple Node versions) with isolated networking
# Copy all launchers to your bin directory
make install
# Or manually
cp localdev localdevnet localfull ~/bin/
chmod +x ~/bin/localdev ~/bin/localdevnet ~/bin/localfull# Run in current directory (isolated networking)
./localdev
# Run with host networking (reach localhost/LAN services)
./localdevnet
# Run full container (Java, Atlassian CLI, multiple Node versions)
./localfull
# Or if installed to ~/bin
localdev
localdevnet
localfullThis mounts your current working directory into the container at /<directory-name>.
The first run of a new container will be noticeably slow (30-60+ seconds) due to:
- User namespace setup: Podman's
--userns=keep-idcreates UID/GID mappings on first use - Overlay filesystem initialization: The container's layered filesystem requires initial setup
- Device permission checks: USB passthrough (
--device /dev/bus/usb) adds overhead
Subsequent runs are much faster as these mappings and caches persist between sessions.
The localdev script creates the following mount structure:
Container Filesystem
├── /<project-name>/ # Your current directory (read-write)
├── /home/developer/.claude/ # Host ~/.claude directory (read-write, native path)
│ ├── CLAUDE.md # Global Claude instructions
│ ├── settings.json # Claude settings
│ └── ... # Other Claude configuration
└── /external/ # Additional read-only mounts
├── repo1/
├── repo2/
└── ...
The script automatically mounts your host's ~/.claude directory to the native home directory location inside the container:
- Location:
$HOME/.claude→/home/developer/.claude(i.e.~/.claudeinside the container) - Permissions: Read-write
- Auto-create: The directory is created on the host if it doesn't exist
- Native discovery: Claude Code natively reads from
~/.claude, so no special flags are needed
This enables:
- Global
CLAUDE.mdinstructions available in all projects - Persistent Claude settings across sessions
- Shared slash commands and configurations
The launchers mount additional directories under /external/<directory-name> inside the container. Directories are mounted read-only by default. Use -rw to mount a directory read-write.
Each launcher prints a summary of every mount before starting the container, and writes a ~/mounts file inside the container with the full host→container path mapping.
| Syntax | Effect |
|---|---|
./localdev /path/to/dir |
Mount read-only (default) |
./localdev -ro /path/to/dir |
Mount read-only (explicit) |
./localdev -rw /path/to/dir |
Mount read-write |
Flags apply to the single path that immediately follows them. Multiple paths and flags may be mixed freely.
# Read-only (default) — same as before
./localdev /path/to/reference
# Explicit read-only
./localdev -ro /path/to/reference
# Read-write
./localdev -rw /path/to/shared-output
# Mixed: one read-only, one read-write
./localdev /path/to/reference -rw /path/to/shared-output
# Multiple paths
./localdev /path/to/repo1 /path/to/repo2 -rw /path/to/sharedExample output:
Mounting (read-only): /home/user/reference-code -> /external/reference-code
Mounting (read-write): /home/user/shared-output -> /external/shared-output
Mounting (read-write): /home/user/.claude -> /home/developer/.claude
Mounting (read-write): /home/user/myproject -> /myproject (workspace)
Set LOCALDEV_MOUNTS with semicolon-separated entries. Prefix with rw: for read-write, or ro: / no prefix for read-only.
# Read-only entries (no prefix or ro: prefix)
export LOCALDEV_MOUNTS="/home/user/reference-code;ro:/home/user/docs"
# Read-write entry
export LOCALDEV_MOUNTS="rw:/home/user/shared-output"
# Mixed
export LOCALDEV_MOUNTS="/home/user/reference;rw:/home/user/shared-output"
./localdevexport LOCALDEV_MOUNTS="/home/user/common-libs"
./localdev -rw /home/user/project-specific-outputAfter launching, a file at ~/mounts inside the container lists every external mount:
# localdev mount map
# HOST PATH -> CONTAINER PATH (mode)
/home/user/reference-code -> /external/reference-code (read-only)
/home/user/shared-output -> /external/shared-output (read-write)
/home/user/.claude -> /home/developer/.claude (read-write)
- Paths must be absolute (not relative)
- Directories must exist and be readable
- Invalid paths are skipped with warnings
# Using Makefile (default container)
make run
# Full container
make run-full
# Or manually
podman run --rm -it -v "$(pwd):/workspace" ghcr.io/gherlein/localdev:latest bash # default
podman run --rm -it -v "$(pwd):/workspace" ghcr.io/gherlein/localfull:latest bash # full# Mount specific project directory
podman run --rm -it -v "/path/to/project:/workspace" ghcr.io/gherlein/localdev:latest bash
# Multiple mounts
podman run --rm -it \
-v "$(pwd):/workspace" \
-v "/path/to/libs:/libs:ro" \
-v "$HOME/.claude:/home/developer/.claude:rw" \
ghcr.io/gherlein/localdev:latest bash# Standard invocation (finds ~/.claude config natively)
claude
# With convenient alias (dangerous mode)
clauded
# Or full command
claude --dangerously-skip-permissions
# Alternative for copilot compatibility
copilotdNote: Claude Code natively reads from ~/.claude, which is mounted from your host's ~/.claude directory. No special flags are needed.
If you mounted external directories using the localdev script:
# List external directories
ls -la /external/
# Access specific external directory
cd /external/reference-code
cat /external/docs/api-spec.mdMultiple Node.js versions are only available in the localfull container:
# Switch Node.js versions
nvm use 14
nvm use 18
nvm use default # LTS
# List installed versions
nvm listNote: The default localdev container only includes Node.js LTS.
The container includes USB passthrough for hardware development on Linux systems. USB passthrough is automatically disabled on macOS as /dev/bus/usb is not available on that platform.
# List USB devices (Linux only)
lsusb
# Access USB devices for development
# (requires appropriate permissions on host)Note: On macOS, USB device access from within the container is not supported due to platform limitations.
# Start container with external reference code
./localdev /home/user/api-reference
# Inside container
cd /myproject
# Your global CLAUDE.md is available
cat ~/.claude/CLAUDE.md
# Access external reference
cat /external/api-reference/examples/auth.go
# Use Claude to help with development
clauded
# Build and test
go build ./...
go test ./...
npm test- Filesystem: Only mounted directories are accessible
- Network: Isolated container network (
localdev,localfull) or host network (localdevnet) - User: Runs as non-root
developeruser (UID/GID 1000) - User Namespace:
--userns=keep-idensures files created in container have correct host ownership - Cleanup: Use
--rmflag for automatic container removal
External directories mounted by the localdev script are read-only, preventing accidental modifications to reference code or shared libraries.
The container isolates AI assistants' file operations from your host system. Even in "dangerous mode," Claude can only affect files within mounted directories.
- Only mount directories you need
- Use read-only mounts (
:ro) for reference materials - Review changes before committing from host
- Don't mount sensitive directories like
~/.sshunless necessary - Use version control for all work
/
├── <project-name>/ # Dynamic working directory (your pwd)
├── home/developer/.claude/ # Global Claude configuration (from host ~/.claude)
│ ├── CLAUDE.md # Global instructions
│ ├── settings.json # Claude settings
│ └── commands/ # Custom slash commands
├── external/ # External read-only mounts (via localdev script)
│ ├── repo1/
│ ├── repo2/
│ └── ...
├── usr/local/go/ # Go installation
├── usr/local/nvm/ # Node.js version manager
├── home/developer/ # Developer user home
│ ├── go/ # GOPATH
│ │ ├── bin/
│ │ ├── pkg/
│ │ └── src/
│ └── .local/bin/ # uv and tools
└── home/linuxbrew/ # Homebrew installation
The container sets up these key environment variables:
GOROOT=/usr/local/go
GOPATH=/home/developer/go
NVM_DIR=/usr/local/nvm
PATH includes:
- /usr/local/go/bin
- /home/developer/go/bin
- /usr/local/nvm/versions/node/<version>/bin
- /home/developer/.local/bin
- /home/linuxbrew/.linuxbrew/binThe localdev script runs the container with these options:
| Option | Purpose |
|---|---|
--userns=keep-id |
Maps container user to host user for correct file ownership |
--device /dev/bus/usb |
Enables USB device passthrough (Linux only, automatically skipped on macOS) |
--group-add keep-groups |
Preserves host group memberships |
-e HOST_UID/HOST_GID |
Passes host user IDs for reference |
--network host |
(localdevnet only) Shares host network stack instead of isolated container network |
-v <path>:/external/<name>:ro |
External directory mounted read-only (default, or explicit -ro flag) |
-v <path>:/external/<name>:rw |
External directory mounted read-write (-rw flag) |
-v <tmpfile>:~/mounts:ro |
Host→container path map file, readable at ~/mounts inside the container |
Out of Memory (OOM) errors during build:
- macOS users: The Podman machine needs sufficient memory. See Configuring Podman Machine Memory for setup instructions.
- The Makefile sets
--memory=16gfor the build process - The default
localdevcontainer requires less memory thanlocalfull - If you see "signal: killed" during Go tool installations when building
localfull, increase Podman machine memory to 16GB - Symptoms include compilation failures for
buf,protoc-gen-go, or other Go tools
Architecture detection fails:
- Manually set
TARGETARCH=amd64orTARGETARCH=arm64
External mount not appearing:
- Verify the path is absolute
- Check the directory exists on host
- Look for warning messages from the script
Permission denied errors:
- Ensure mounted directories have appropriate read permissions
- The container runs as UID/GID 1000
- The
--userns=keep-idoption should map permissions correctly
Command not found inside container:
- For npm packages: ensure nvm is loaded (
. $NVM_DIR/nvm.sh) - For Go tools: check
$GOPATH/binis in PATH - The container's
.bashrcshould load these automatically
Claude global config not loading:
- Verify
~/.claudeexists on your host (the launcher auto-creates it) - Check the mount message when starting the container
- Run
ls ~/.claudeinside the container to verify the mount
USB devices not accessible:
- USB passthrough only works on Linux
- On macOS, USB device access from containers is not supported
- On Linux: ensure USB devices are connected before starting the container
- On Linux: check host permissions on
/dev/bus/usb - May require running podman with additional privileges on Linux
This container is ideal for:
- AI-assisted development with Claude Code or GitHub Copilot
- Multi-language projects (Go + TypeScript/Node.js)
- Safe experimentation with AI code generation
- Isolated build and test environments
- Cross-referencing multiple codebases safely
- Hardware/USB development projects
Use localfull for:
- Java/JVM development
- Atlassian CLI integration (Jira, Confluence)
- Documentation generation (Marp, Mermaid, Hugo)
- Projects requiring multiple Node.js versions
This project uses Podman instead of Docker to avoid Docker Desktop licensing requirements for commercial use. Podman is fully open-source and compatible with Docker images and commands.
When modifying the container:
- Test builds on both AMD64 and ARM64 if possible
- Keep memory-intensive operations batched (see npm installs)
- Update this README with new features
- Maintain the security-first approach