Skip to content

Instantly share code, notes, and snippets.

@torgeir
Last active June 10, 2026 07:20
Show Gist options
  • Select an option

  • Save torgeir/dbe931351d91644f295ca8a296b8e4fb to your computer and use it in GitHub Desktop.

Select an option

Save torgeir/dbe931351d91644f295ca8a296b8e4fb to your computer and use it in GitHub Desktop.
# 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
}
# 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