Skip to content

Instantly share code, notes, and snippets.

@nickpascucci
Created November 13, 2025 15:59
Show Gist options
  • Select an option

  • Save nickpascucci/030a351ba9d8ebd2235b69bdbe0391c2 to your computer and use it in GitHub Desktop.

Select an option

Save nickpascucci/030a351ba9d8ebd2235b69bdbe0391c2 to your computer and use it in GitHub Desktop.
Claude Code Development Container - Multi-language Docker container with DNS filtering
#!/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[@]}"
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