Last active
June 10, 2026 07:20
-
-
Save torgeir/dbe931351d91644f295ca8a296b8e4fb to your computer and use it in GitHub Desktop.
AI in docker, inspired by https://www.bekk.no/fag/artikkel/putt-aien-i-en-sandkasse--100223
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
| # https://gist.github.com/torgeir/dbe931351d91644f295ca8a296b8e4fb | |
| # usage: ai [--no-state] [name [dir...]] | |
| # ai - fzf pick from all docker-ai-* containers | |
| # ai <name> - create/attach to docker-ai-<name>, mounts pwd | |
| # ai <name> <dir> - create/attach to docker-ai-<name>, mounts dir | |
| # ai <name> <dir1> <dir2>... - same, mounts multiple dirs; cwd is dir1 | |
| # ai --no-state <name> - same, but omits ai state mounts | |
| # ai --no-gradle <name> - same, but omits gradle state mounts | |
| # ai -e VAR <name> - same, expose environment variable VAR to the container | |
| ai() { | |
| # prefer podman, fall back to docker | |
| local runtime | |
| if command -v podman &>/dev/null; then | |
| runtime=podman | |
| elif command -v docker &>/dev/null; then | |
| runtime=docker | |
| else | |
| echo "Neither docker nor podman found" >&2 | |
| return 1 | |
| fi | |
| # parse flags | |
| local no_state=0 no_gradle=0 | |
| local -a env_flags=() | |
| # parse flags | |
| while [[ "${1:-}" == --* || "${1:-}" == -e ]]; do | |
| case "$1" in | |
| --no-state) no_state=1; shift ;; | |
| --no-gradle) no_gradle=1; shift ;; | |
| -e|--env) env_flags+=(-e "$2"); shift 2 ;; | |
| --env=*) env_flags+=(-e "${1#--env=}"); shift ;; | |
| *) break ;; | |
| esac | |
| done | |
| local name="${1//[^a-z0-9_-]/-}" | |
| shift 2>/dev/null || true # remaining args are dirs | |
| # collect dirs; default to pwd if none given | |
| local -a dirs=() | |
| for d in "$@"; do | |
| [[ "$d" = /* ]] && dirs+=("$d") || dirs+=("$PWD/$d") | |
| done | |
| if (( ${#dirs[@]} == 0 )); then | |
| dirs=("$PWD") | |
| fi | |
| # --userns=keep-id maps your host uid straight through inside the container | |
| # (batman uid == your uid), so bind mounts just work without chown tricks. | |
| # only podman supports this; docker gets no --userns flag. | |
| local -a userns_flags=() | |
| if [[ "$runtime" == "podman" ]]; then | |
| userns_flags=(--userns=keep-id) | |
| fi | |
| # amd gpu: /dev/kfd for rocm compute, /dev/dri for rendering | |
| local -a gpu_flags=() | |
| if [[ -e /dev/kfd ]] && [[ -e /dev/dri ]]; then | |
| gpu_flags=(--device /dev/kfd --device /dev/dri) | |
| fi | |
| # TODO nvidia | |
| # hardening defaults (drop linux capabilities and block privilege escalation) | |
| local -a security_flags=(--cap-drop=ALL --security-opt=no-new-privileges) | |
| # mount every requested dir; working dir is the first one | |
| local -a volumes=(-w "${dirs[1]}") | |
| for d in "${dirs[@]}"; do | |
| volumes+=(-v "${d}:${d}") | |
| done | |
| volumes+=( | |
| -v "$HOME/.m2:/home/batman/.m2" | |
| -v "$HOME/.gitconfig.docker-ai:/home/batman/.gitconfig.private:ro" | |
| ) | |
| if (( !no_gradle )); then | |
| volumes+=( | |
| -v "$HOME/.gradle:/home/batman/.gradle" | |
| ) | |
| fi | |
| if (( !no_state )); then | |
| [[ -f "$HOME/.claude.json" ]] || touch "$HOME/.claude.json" | |
| [[ -d "$HOME/.pi" ]] || mkdir -p "$HOME/.pi" | |
| volumes+=( | |
| # opencode | |
| -v "$HOME/.local/share/opencode:/home/batman/.local/share/opencode" | |
| -v "$HOME/.local/state/opencode:/home/batman/.local/state/opencode" | |
| -v "$HOME/.config/opencode/opencode.json:/home/batman/.config/opencode/opencode.json:ro" | |
| # claude: auth token, main state dir, config dir, history/state | |
| -v "$HOME/.claude.json:/home/batman/.claude.json" | |
| -v "$HOME/.claude:/home/batman/.claude" | |
| -v "$HOME/.config/claude:/home/batman/.config/claude" | |
| -v "$HOME/.local/state/claude:/home/batman/.local/state/claude" | |
| # pi: everything lives under ~/.pi/agent/ (sessions, auth, settings) | |
| -v "$HOME/.pi:/home/batman/.pi" | |
| ) | |
| fi | |
| # named container: create or attach | |
| if [[ -n "$name" ]]; then | |
| local container="docker-ai-$name" | |
| if $runtime ps --format '{{.Names}}' | grep -qx "$container"; then | |
| $runtime exec -it "$container" tmux attach | |
| elif $runtime ps -a --format '{{.Names}}' | grep -qx "$container"; then | |
| $runtime start "$container" && $runtime exec -it "$container" tmux attach | |
| else | |
| echo "Starting $container mounting: ${dirs[*]} ($runtime)" | |
| $runtime run -dit --name "$container" \ | |
| "${userns_flags[@]}" "${gpu_flags[@]}" "${security_flags[@]}" \ | |
| "${volumes[@]}" \ | |
| "${env_flags[@]}" \ | |
| docker-ai bash -c "exec tmux" | |
| sleep 0.5 | |
| $runtime exec -it "$container" tmux attach | |
| fi | |
| return | |
| fi | |
| # no args: pick from all docker-ai-* containers with fzf | |
| local selection | |
| selection=$($runtime ps -a --format '{{.Names}}\t{{.Status}}' \ | |
| | grep '^docker-ai-' \ | |
| | fzf --height=40% --reverse --header="container status") || return 0 | |
| local container | |
| container=$(echo "$selection" | awk '{print $1}') | |
| if $runtime start "$container"; then | |
| $runtime exec -it "$container" tmux attach | |
| else | |
| echo "Failed to start $container ($runtime)" >&2 | |
| return 1 | |
| fi | |
| } |
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
| # isolated ai environments | |
| # usage: | |
| # podman build -t docker-ai . --no-cache | |
| # or | |
| # docker build -t docker-ai . --no-cache | |
| # | |
| # testet on macos with docker running as root | |
| # testet on nixos with rootless podman | |
| FROM ubuntu:24.04 | |
| # no prompts during installation | |
| ENV DEBIAN_FRONTEND=noninteractive | |
| # c-x e in opencode | |
| ENV EDITOR=nvim | |
| ENV VISUAL=nvim | |
| ENV GIT_EDITOR=nvim | |
| # upgrade and install | |
| RUN apt-get update && apt-get install -y --no-install-recommends \ | |
| btop \ | |
| curl \ | |
| fd-find \ | |
| git \ | |
| gpg \ | |
| jq \ | |
| neovim \ | |
| ripgrep \ | |
| zoxide \ | |
| tmux \ | |
| tree \ | |
| unzip \ | |
| vim \ | |
| wget \ | |
| zstd \ | |
| pciutils \ | |
| ca-certificates \ | |
| openjdk-17-jdk \ | |
| openjdk-21-jdk \ | |
| python3 python3-pip python3-venv \ | |
| locales \ | |
| && locale-gen nb_NO.UTF-8 \ | |
| && ln -s /usr/bin/fdfind /usr/local/bin/fd \ | |
| && mkdir -p /etc/apt/keyrings && \ | |
| curl -fsSL https://raw.githubusercontent.com/eza-community/eza/main/deb.asc \ | |
| | gpg --dearmor -o /etc/apt/keyrings/gierens.gpg && \ | |
| echo "deb [signed-by=/etc/apt/keyrings/gierens.gpg] https://deb.gierens.de stable main" \ | |
| > /etc/apt/sources.list.d/gierens.list && \ | |
| apt-get update && apt-get install -y eza \ | |
| && rm -rf /var/lib/apt/lists/* | |
| # java | |
| ENV JAVA_HOME=/usr/lib/jvm/java-21-openjdk-arm64 | |
| ENV PATH="$JAVA_HOME/bin:$PATH" | |
| # norsk | |
| ENV LANG=nb_NO.UTF-8 | |
| ENV LANGUAGE=nb_NO:nb | |
| ENV LC_ALL=nb_NO.UTF-8 | |
| # tmux, and markdown rendering in opencode | |
| ENV TERM=xterm-256color | |
| ENV COLORTERM=truecolor | |
| # ollama needs root | |
| #RUN curl -fsSL https://ollama.com/install.sh | sh | |
| # non root user | |
| RUN userdel -r ubuntu \ | |
| && useradd --uid 1000 --create-home --shell /bin/bash batman \ | |
| && groupadd -r render \ | |
| && usermod -aG video,render batman | |
| ENV PATH="/home/batman/.local/bin:$PATH" | |
| USER batman | |
| WORKDIR /home/batman | |
| # conf | |
| RUN git clone https://github.com/torgeir/dotfiles && \ | |
| ln -s dotfiles/fzfrc .fzfrc && \ | |
| ln -s dotfiles/zshrc .zshrc && \ | |
| ln -s dotfiles/vimrc .vimrc && \ | |
| ln -s dotfiles/inputrc .inputrc && \ | |
| ln -s dotfiles/tmux.conf .tmux.conf && \ | |
| ln -s dotfiles/gitconfig .gitconfig && \ | |
| echo 'source $HOME/dotfiles/source/aliases' >> /home/batman/.bashrc | |
| # batman ownership of foldlers we later mount using -v | |
| RUN mkdir -p /home/batman/.local/state && \ | |
| mkdir -p /home/batman/.local/share && \ | |
| mkdir -p /home/batman/.config && \ | |
| mkdir -p /home/batman/.config/opencode && \ | |
| mkdir -p /home/batman/.pi && \ | |
| mkdir -p /home/batman/.claude && \ | |
| mkdir -p /home/batman/.m2 && \ | |
| mkdir -p /home/batman/.gradle | |
| # ai | |
| RUN curl -fsSL "https://raw.githubusercontent.com/nvm-sh/nvm/v0.40.4/install.sh" | bash \ | |
| && export NVM_DIR=/home/batman/.nvm \ | |
| && . "$NVM_DIR/nvm.sh" \ | |
| && nvm install 24 \ | |
| && nvm alias default 24 \ | |
| && curl -fsSL https://bun.com/install | bash \ | |
| && curl -fsSL https://opencode.ai/install | bash | |
| #&& curl -fsSL https://claude.ai/install.sh | bash \ | |
| #&& CI=true curl -fsSL https://pi.dev/install.sh | sh \ | |
| #&& npm install -g @github/copilot | |
| # last line must not have \ | |
| # startup | |
| CMD ["/bin/bash"] |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment