Created
November 13, 2025 15:59
-
-
Save nickpascucci/030a351ba9d8ebd2235b69bdbe0391c2 to your computer and use it in GitHub Desktop.
Claude Code Development Container - Multi-language Docker container with DNS filtering
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| #!/usr/bin/env bash | |
| set -euo pipefail | |
| # claude-polyglot: Run the Claude Code Polyglot development container | |
| # Usage: claude-polyglot [OPTIONS] | |
| # | |
| # Options: | |
| # --build Build the container image before running | |
| # --no-config Don't mount Claude Code configuration | |
| # --no-history Don't mount command history | |
| # --network Disable DNS filtering (allow all network access) | |
| # --yolo Run claude --dangerously-skip-permissions | |
| # --help Show this help message | |
| # | |
| # Examples: | |
| # claude-polyglot # Run with DNS filtering (Claude API only) | |
| # claude-polyglot --build # Rebuild and run | |
| # claude-polyglot --network # Run with full network access | |
| # claude-polyglot --yolo # Run Claude without permission checks | |
| # claude-polyglot zsh # Run with zsh command | |
| # claude-polyglot cargo build # Run Rust cargo | |
| # claude-polyglot python3 script.py # Run Python script | |
| # claude-polyglot swipl -s file.pl # Run Prolog file | |
| # claude-polyglot ruff check . # Run ruff linter | |
| # END_HELP | |
| IMAGE_NAME="claude-polyglot:latest" | |
| BUILD=false | |
| MOUNT_CONFIG=true | |
| MOUNT_HISTORY=true | |
| UNRESTRICTED_NETWORK=false | |
| YOLO_MODE=false | |
| DOCKER_ARGS=() | |
| COMMAND_ARGS=() | |
| # Whitelisted domains for DNS filtering | |
| WHITELISTED_DOMAINS=( | |
| "api.anthropic.com" | |
| "claude.ai" | |
| ) | |
| # Parse arguments | |
| while [[ $# -gt 0 ]]; do | |
| case $1 in | |
| --build) | |
| BUILD=true | |
| shift | |
| ;; | |
| --no-config) | |
| MOUNT_CONFIG=false | |
| shift | |
| ;; | |
| --no-history) | |
| MOUNT_HISTORY=false | |
| shift | |
| ;; | |
| --network) | |
| UNRESTRICTED_NETWORK=true | |
| shift | |
| ;; | |
| --yolo) | |
| YOLO_MODE=true | |
| shift | |
| ;; | |
| --help) | |
| sed -n '/^# claude-polyglot:/,/^# END_HELP/p' "$0" | sed 's/^# //' | sed 's/^#$//' | grep -v '^END_HELP$' | |
| exit 0 | |
| ;; | |
| *) | |
| COMMAND_ARGS+=("$1") | |
| shift | |
| ;; | |
| esac | |
| done | |
| # Build if requested | |
| if [ "$BUILD" = true ]; then | |
| SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" | |
| echo "Building $IMAGE_NAME..." | |
| docker build -t "$IMAGE_NAME" "$SCRIPT_DIR" | |
| fi | |
| # Check if image exists | |
| if ! docker image inspect "$IMAGE_NAME" &>/dev/null; then | |
| echo "Error: Image $IMAGE_NAME not found." | |
| echo "Build it with: docker build -t $IMAGE_NAME /path/to/container/polyglot" | |
| echo "Or run: $(basename "$0") --build" | |
| exit 1 | |
| fi | |
| # Setup volume mounts | |
| DOCKER_ARGS+=( | |
| "-v" "$(pwd):/workspace" | |
| ) | |
| if [ "$MOUNT_CONFIG" = true ] && [ -d "$HOME/.claude" ]; then | |
| DOCKER_ARGS+=("-v" "$HOME/.claude:/home/node/.claude") | |
| fi | |
| if [ "$MOUNT_HISTORY" = true ]; then | |
| DOCKER_ARGS+=("-v" "claude-polyglot-history:/commandhistory") | |
| fi | |
| # DNS filtering by default, unrestricted network when --network is specified | |
| if [ "$UNRESTRICTED_NETWORK" = false ]; then | |
| # Enable network but filter DNS - only allow whitelisted domains | |
| echo "Enabling DNS filtering (Claude API access only)..." | |
| # Resolve whitelisted domains and add as host entries | |
| for domain in "${WHITELISTED_DOMAINS[@]}"; do | |
| # Resolve domain to IP addresses | |
| ips=$(dig +short "$domain" 2>/dev/null | grep -E '^[0-9]+\.[0-9]+\.[0-9]+\.[0-9]+$') | |
| if [ -n "$ips" ]; then | |
| # Add first IP as host entry | |
| first_ip=$(echo "$ips" | head -1) | |
| DOCKER_ARGS+=("--add-host" "${domain}:${first_ip}") | |
| echo " ✓ ${domain} -> ${first_ip}" | |
| else | |
| echo " ⚠ Warning: Could not resolve ${domain}" | |
| fi | |
| done | |
| # Set DNS to non-existent server to block all other lookups | |
| DOCKER_ARGS+=("--dns" "0.0.0.0") | |
| else | |
| echo "DNS filtering disabled - full network access enabled" | |
| fi | |
| # If no command specified, run interactive shell or Claude in YOLO mode | |
| if [ ${#COMMAND_ARGS[@]} -eq 0 ]; then | |
| DOCKER_ARGS+=("-it") | |
| if [ "$YOLO_MODE" = true ]; then | |
| COMMAND_ARGS=("claude" "--dangerously-skip-permissions") | |
| else | |
| COMMAND_ARGS=("zsh") | |
| fi | |
| else | |
| # If command is provided, determine if we need interactive mode | |
| case "${COMMAND_ARGS[0]}" in | |
| bash|zsh|sh|claude) | |
| DOCKER_ARGS+=("-it") | |
| ;; | |
| swipl|python|python3|ipython) | |
| # Only interactive if no additional arguments | |
| if [ ${#COMMAND_ARGS[@]} -eq 1 ]; then | |
| DOCKER_ARGS+=("-it") | |
| fi | |
| ;; | |
| esac | |
| fi | |
| # Run the container | |
| exec docker run --rm "${DOCKER_ARGS[@]}" "$IMAGE_NAME" "${COMMAND_ARGS[@]}" |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| FROM node:20 | |
| ARG TZ | |
| ENV TZ="$TZ" | |
| ARG CLAUDE_CODE_VERSION=latest | |
| ARG RUST_VERSION=1.82.0 | |
| ARG SWIPLDIR=/usr/lib/swi-prolog | |
| ARG ALLOY_VERSION=6.2.0 | |
| # Install basic development tools and all language environments | |
| RUN apt-get update && apt-get install -y --no-install-recommends \ | |
| less \ | |
| git \ | |
| procps \ | |
| sudo \ | |
| fzf \ | |
| zsh \ | |
| man-db \ | |
| unzip \ | |
| gnupg2 \ | |
| gh \ | |
| iptables \ | |
| ipset \ | |
| iproute2 \ | |
| dnsutils \ | |
| aggregate \ | |
| jq \ | |
| nano \ | |
| vim \ | |
| curl \ | |
| wget \ | |
| build-essential \ | |
| pkg-config \ | |
| libssl-dev \ | |
| python3 \ | |
| python3-pip \ | |
| python3-venv \ | |
| swi-prolog \ | |
| openjdk-17-jre-headless \ | |
| && apt-get clean && rm -rf /var/lib/apt/lists/* | |
| # Ensure default node user has access to /usr/local/share | |
| RUN mkdir -p /usr/local/share/npm-global && \ | |
| chown -R node:node /usr/local/share | |
| ARG USERNAME=node | |
| # Install Rust and Python tools as the node user | |
| USER $USERNAME | |
| # Install Rust toolchain | |
| RUN curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh -s -- -y --default-toolchain ${RUST_VERSION} | |
| ENV PATH="/home/node/.cargo/bin:${PATH}" | |
| # Install uv (fast Python package installer) | |
| RUN curl -LsSf https://astral.sh/uv/install.sh | sh | |
| # Install ruff and mypy globally via uv | |
| RUN /home/node/.local/bin/uv tool install ruff && \ | |
| /home/node/.local/bin/uv tool install mypy | |
| ENV PATH="/home/node/.local/bin:${PATH}" | |
| # Switch back to root for remaining setup | |
| USER root | |
| # Persist bash history | |
| RUN SNIPPET="export PROMPT_COMMAND='history -a' && export HISTFILE=/commandhistory/.bash_history" \ | |
| && mkdir /commandhistory \ | |
| && touch /commandhistory/.bash_history \ | |
| && chown -R $USERNAME /commandhistory | |
| # Set `DEVCONTAINER` environment variable to help with orientation | |
| ENV DEVCONTAINER=true | |
| # Create workspace and config directories and set permissions | |
| RUN mkdir -p /workspace /home/node/.claude && \ | |
| chown -R node:node /workspace /home/node/.claude | |
| WORKDIR /workspace | |
| # Install Alloy Analyzer | |
| RUN wget -q "https://github.com/AlloyTools/org.alloytools.alloy/releases/download/v${ALLOY_VERSION}/org.alloytools.alloy.dist.jar" \ | |
| -O /usr/local/lib/alloy.jar && \ | |
| echo '#!/bin/bash' > /usr/local/bin/alloy && \ | |
| echo 'exec java -jar /usr/local/lib/alloy.jar "$@"' >> /usr/local/bin/alloy && \ | |
| chmod +x /usr/local/bin/alloy | |
| # Install git-delta for better diffs | |
| ARG GIT_DELTA_VERSION=0.18.2 | |
| RUN ARCH=$(dpkg --print-architecture) && \ | |
| wget "https://github.com/dandavison/delta/releases/download/${GIT_DELTA_VERSION}/git-delta_${GIT_DELTA_VERSION}_${ARCH}.deb" && \ | |
| dpkg -i "git-delta_${GIT_DELTA_VERSION}_${ARCH}.deb" && \ | |
| rm "git-delta_${GIT_DELTA_VERSION}_${ARCH}.deb" | |
| # Install zsh-in-docker | |
| ARG ZSH_IN_DOCKER_VERSION=1.2.0 | |
| RUN wget "https://github.com/deluan/zsh-in-docker/releases/download/v${ZSH_IN_DOCKER_VERSION}/zsh-in-docker.sh" && \ | |
| chmod +x zsh-in-docker.sh && \ | |
| ./zsh-in-docker.sh \ | |
| -t robbyrussell \ | |
| -p git \ | |
| -p ssh-agent \ | |
| -p https://github.com/zsh-users/zsh-autosuggestions \ | |
| -p https://github.com/zsh-users/zsh-completions && \ | |
| rm zsh-in-docker.sh | |
| # Install Claude Code CLI (as root for global install) | |
| RUN npm install -g @anthropic-ai/claude-code@${CLAUDE_CODE_VERSION} | |
| # Switch to node user for runtime | |
| USER $USERNAME | |
| # Set environment variables for Claude Code | |
| ENV CLAUDE_CONFIG_DIR="/home/node/.claude" | |
| ENV NODE_OPTIONS="--max-old-space-size=4096" | |
| # Verify installations | |
| RUN rustc --version && \ | |
| cargo --version && \ | |
| python3 --version && \ | |
| uv --version && \ | |
| ruff --version && \ | |
| mypy --version && \ | |
| swipl --version && \ | |
| java -version && \ | |
| claude --version |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment