From 39a43474e7bf171a3882205d187b960445eba948 Mon Sep 17 00:00:00 2001 From: Gordon Beeming Date: Mon, 29 Jun 2026 08:17:24 +1000 Subject: [PATCH 1/3] feat: add Python pip & pipx to the base image (#121) Every image already carried python3 transitively (via software-properties-common) but shipped no pip, so pip-distributed CLIs like APM couldn't be installed. Add python3-pip, python3-venv and pipx to the shared system-packages snippet, so all 22 image variants pick them up. Debian 12 enforces PEP 668, so pipx is the supported way to install CLI apps; pip still works inside venvs. Put ~/.local/bin on PATH (ENV for the run command and non-login shells, plus an /etc/profile.d drop-in for login shells) so pipx-installed tools are reachable without a per-session 'pipx ensurepath'. Regenerated all docker/generated Dockerfiles and updated README plus docs/docker-images.md. Co-authored-by: Claude Co-authored-by: GitButler --- README.md | 4 ++-- docker/generated/Dockerfile.default | 14 ++++++++++++++ docker/generated/Dockerfile.dotnet | 14 ++++++++++++++ docker/generated/Dockerfile.dotnet-10 | 14 ++++++++++++++ docker/generated/Dockerfile.dotnet-8 | 14 ++++++++++++++ docker/generated/Dockerfile.dotnet-9 | 14 ++++++++++++++ docker/generated/Dockerfile.dotnet-playwright | 14 ++++++++++++++ docker/generated/Dockerfile.dotnet-rust | 14 ++++++++++++++ docker/generated/Dockerfile.golang | 14 ++++++++++++++ docker/generated/Dockerfile.java | 14 ++++++++++++++ docker/generated/Dockerfile.playwright | 14 ++++++++++++++ docker/generated/Dockerfile.rust | 14 ++++++++++++++ docker/snippets/system-packages.Dockerfile | 14 ++++++++++++++ docs/docker-images.md | 1 + 14 files changed, 171 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index 60ae800..6f28a2c 100644 --- a/README.md +++ b/README.md @@ -40,7 +40,7 @@ Choose your platform below. The scripts include both **Safe Mode** (asks for con All images support both **AMD64** (x86_64) and **ARM64** (Apple Silicon, etc.) architectures. All functions support switching between Docker image variants using flags: -- **No flag** - Base image (Node.js, Git, basic tools) +- **No flag** - Base image (Node.js, Python with pip & pipx, Git, basic tools) - **`--dotnet`** (`-d`) - .NET image (includes .NET 8, 9 & 10 SDKs) - **`--dotnet8`** (`-d8`) - .NET 8 image (includes .NET 8 SDK) - **`--dotnet9`** (`-d9`) - .NET 9 image (includes .NET 9 SDK) @@ -669,7 +669,7 @@ This project provides multiple Docker image variants for different development s | Tag | Flag | Description | |-----|------|-------------| -| `latest` | *(default)* | Base image with Node.js 20, Git, and essential tools | +| `latest` | *(default)* | Base image with Node.js 20, Python (pip & pipx), Git, and essential tools | | `dotnet` | `--dotnet` | .NET 8, 9 & 10 SDKs | | `dotnet-8` | `--dotnet8` | .NET 8 SDK only | | `dotnet-9` | `--dotnet9` | .NET 9 SDK only | diff --git a/docker/generated/Dockerfile.default b/docker/generated/Dockerfile.default index 023f4aa..62bc9ba 100644 --- a/docker/generated/Dockerfile.default +++ b/docker/generated/Dockerfile.default @@ -10,6 +10,8 @@ FROM node:20-slim ENV DEBIAN_FRONTEND=noninteractive # Install git, curl, gpg, gosu, nano, xdg-utils, zsh, and related utilities for the entrypoint script and testing. +# Python tooling (pip + venv + pipx) ships here so every image can install pip-distributed +# CLIs; Debian 12 enforces PEP 668, so `pipx install ` is the supported path. RUN apt-get update && apt-get install -y \ apt-transport-https \ curl \ @@ -17,12 +19,24 @@ RUN apt-get update && apt-get install -y \ gosu \ gpg \ nano \ + pipx \ + python3-pip \ + python3-venv \ software-properties-common \ wget \ xdg-utils \ zsh \ && rm -rf /var/lib/apt/lists/* +# pipx installs apps into the runtime user's ~/.local/bin. The entrypoint always runs as +# appuser with HOME=/home/appuser, so put that dir on PATH up front and pipx-installed CLIs +# (e.g. `pipx install apm`) are reachable without a per-session `pipx ensurepath`. +# ENV covers the exec'd command and non-login shells; the profile.d drop-in re-adds it for +# login shells, which otherwise reset PATH via /etc/profile (Debian zsh sources it too). +ENV PATH="/home/appuser/.local/bin:${PATH}" +RUN echo 'case ":${PATH}:" in *:/home/appuser/.local/bin:*) ;; *) PATH="/home/appuser/.local/bin:${PATH}" ;; esac' \ + > /etc/profile.d/copilot-local-bin.sh + # --- snippet: docker-cli --- # Install Docker CLI to support brokered Docker socket scenarios. # Only the CLI is installed; the daemon is the host's, mediated by copilot_here's broker. diff --git a/docker/generated/Dockerfile.dotnet b/docker/generated/Dockerfile.dotnet index 11bfba2..7679c61 100644 --- a/docker/generated/Dockerfile.dotnet +++ b/docker/generated/Dockerfile.dotnet @@ -10,6 +10,8 @@ FROM node:20-slim ENV DEBIAN_FRONTEND=noninteractive # Install git, curl, gpg, gosu, nano, xdg-utils, zsh, and related utilities for the entrypoint script and testing. +# Python tooling (pip + venv + pipx) ships here so every image can install pip-distributed +# CLIs; Debian 12 enforces PEP 668, so `pipx install ` is the supported path. RUN apt-get update && apt-get install -y \ apt-transport-https \ curl \ @@ -17,12 +19,24 @@ RUN apt-get update && apt-get install -y \ gosu \ gpg \ nano \ + pipx \ + python3-pip \ + python3-venv \ software-properties-common \ wget \ xdg-utils \ zsh \ && rm -rf /var/lib/apt/lists/* +# pipx installs apps into the runtime user's ~/.local/bin. The entrypoint always runs as +# appuser with HOME=/home/appuser, so put that dir on PATH up front and pipx-installed CLIs +# (e.g. `pipx install apm`) are reachable without a per-session `pipx ensurepath`. +# ENV covers the exec'd command and non-login shells; the profile.d drop-in re-adds it for +# login shells, which otherwise reset PATH via /etc/profile (Debian zsh sources it too). +ENV PATH="/home/appuser/.local/bin:${PATH}" +RUN echo 'case ":${PATH}:" in *:/home/appuser/.local/bin:*) ;; *) PATH="/home/appuser/.local/bin:${PATH}" ;; esac' \ + > /etc/profile.d/copilot-local-bin.sh + # --- snippet: docker-cli --- # Install Docker CLI to support brokered Docker socket scenarios. # Only the CLI is installed; the daemon is the host's, mediated by copilot_here's broker. diff --git a/docker/generated/Dockerfile.dotnet-10 b/docker/generated/Dockerfile.dotnet-10 index b202b4f..c88ff37 100644 --- a/docker/generated/Dockerfile.dotnet-10 +++ b/docker/generated/Dockerfile.dotnet-10 @@ -10,6 +10,8 @@ FROM node:20-slim ENV DEBIAN_FRONTEND=noninteractive # Install git, curl, gpg, gosu, nano, xdg-utils, zsh, and related utilities for the entrypoint script and testing. +# Python tooling (pip + venv + pipx) ships here so every image can install pip-distributed +# CLIs; Debian 12 enforces PEP 668, so `pipx install ` is the supported path. RUN apt-get update && apt-get install -y \ apt-transport-https \ curl \ @@ -17,12 +19,24 @@ RUN apt-get update && apt-get install -y \ gosu \ gpg \ nano \ + pipx \ + python3-pip \ + python3-venv \ software-properties-common \ wget \ xdg-utils \ zsh \ && rm -rf /var/lib/apt/lists/* +# pipx installs apps into the runtime user's ~/.local/bin. The entrypoint always runs as +# appuser with HOME=/home/appuser, so put that dir on PATH up front and pipx-installed CLIs +# (e.g. `pipx install apm`) are reachable without a per-session `pipx ensurepath`. +# ENV covers the exec'd command and non-login shells; the profile.d drop-in re-adds it for +# login shells, which otherwise reset PATH via /etc/profile (Debian zsh sources it too). +ENV PATH="/home/appuser/.local/bin:${PATH}" +RUN echo 'case ":${PATH}:" in *:/home/appuser/.local/bin:*) ;; *) PATH="/home/appuser/.local/bin:${PATH}" ;; esac' \ + > /etc/profile.d/copilot-local-bin.sh + # --- snippet: docker-cli --- # Install Docker CLI to support brokered Docker socket scenarios. # Only the CLI is installed; the daemon is the host's, mediated by copilot_here's broker. diff --git a/docker/generated/Dockerfile.dotnet-8 b/docker/generated/Dockerfile.dotnet-8 index c332b5b..6e3520e 100644 --- a/docker/generated/Dockerfile.dotnet-8 +++ b/docker/generated/Dockerfile.dotnet-8 @@ -10,6 +10,8 @@ FROM node:20-slim ENV DEBIAN_FRONTEND=noninteractive # Install git, curl, gpg, gosu, nano, xdg-utils, zsh, and related utilities for the entrypoint script and testing. +# Python tooling (pip + venv + pipx) ships here so every image can install pip-distributed +# CLIs; Debian 12 enforces PEP 668, so `pipx install ` is the supported path. RUN apt-get update && apt-get install -y \ apt-transport-https \ curl \ @@ -17,12 +19,24 @@ RUN apt-get update && apt-get install -y \ gosu \ gpg \ nano \ + pipx \ + python3-pip \ + python3-venv \ software-properties-common \ wget \ xdg-utils \ zsh \ && rm -rf /var/lib/apt/lists/* +# pipx installs apps into the runtime user's ~/.local/bin. The entrypoint always runs as +# appuser with HOME=/home/appuser, so put that dir on PATH up front and pipx-installed CLIs +# (e.g. `pipx install apm`) are reachable without a per-session `pipx ensurepath`. +# ENV covers the exec'd command and non-login shells; the profile.d drop-in re-adds it for +# login shells, which otherwise reset PATH via /etc/profile (Debian zsh sources it too). +ENV PATH="/home/appuser/.local/bin:${PATH}" +RUN echo 'case ":${PATH}:" in *:/home/appuser/.local/bin:*) ;; *) PATH="/home/appuser/.local/bin:${PATH}" ;; esac' \ + > /etc/profile.d/copilot-local-bin.sh + # --- snippet: docker-cli --- # Install Docker CLI to support brokered Docker socket scenarios. # Only the CLI is installed; the daemon is the host's, mediated by copilot_here's broker. diff --git a/docker/generated/Dockerfile.dotnet-9 b/docker/generated/Dockerfile.dotnet-9 index fb1a75b..1ce6e5b 100644 --- a/docker/generated/Dockerfile.dotnet-9 +++ b/docker/generated/Dockerfile.dotnet-9 @@ -10,6 +10,8 @@ FROM node:20-slim ENV DEBIAN_FRONTEND=noninteractive # Install git, curl, gpg, gosu, nano, xdg-utils, zsh, and related utilities for the entrypoint script and testing. +# Python tooling (pip + venv + pipx) ships here so every image can install pip-distributed +# CLIs; Debian 12 enforces PEP 668, so `pipx install ` is the supported path. RUN apt-get update && apt-get install -y \ apt-transport-https \ curl \ @@ -17,12 +19,24 @@ RUN apt-get update && apt-get install -y \ gosu \ gpg \ nano \ + pipx \ + python3-pip \ + python3-venv \ software-properties-common \ wget \ xdg-utils \ zsh \ && rm -rf /var/lib/apt/lists/* +# pipx installs apps into the runtime user's ~/.local/bin. The entrypoint always runs as +# appuser with HOME=/home/appuser, so put that dir on PATH up front and pipx-installed CLIs +# (e.g. `pipx install apm`) are reachable without a per-session `pipx ensurepath`. +# ENV covers the exec'd command and non-login shells; the profile.d drop-in re-adds it for +# login shells, which otherwise reset PATH via /etc/profile (Debian zsh sources it too). +ENV PATH="/home/appuser/.local/bin:${PATH}" +RUN echo 'case ":${PATH}:" in *:/home/appuser/.local/bin:*) ;; *) PATH="/home/appuser/.local/bin:${PATH}" ;; esac' \ + > /etc/profile.d/copilot-local-bin.sh + # --- snippet: docker-cli --- # Install Docker CLI to support brokered Docker socket scenarios. # Only the CLI is installed; the daemon is the host's, mediated by copilot_here's broker. diff --git a/docker/generated/Dockerfile.dotnet-playwright b/docker/generated/Dockerfile.dotnet-playwright index c31907d..3c048f9 100644 --- a/docker/generated/Dockerfile.dotnet-playwright +++ b/docker/generated/Dockerfile.dotnet-playwright @@ -10,6 +10,8 @@ FROM node:20-slim ENV DEBIAN_FRONTEND=noninteractive # Install git, curl, gpg, gosu, nano, xdg-utils, zsh, and related utilities for the entrypoint script and testing. +# Python tooling (pip + venv + pipx) ships here so every image can install pip-distributed +# CLIs; Debian 12 enforces PEP 668, so `pipx install ` is the supported path. RUN apt-get update && apt-get install -y \ apt-transport-https \ curl \ @@ -17,12 +19,24 @@ RUN apt-get update && apt-get install -y \ gosu \ gpg \ nano \ + pipx \ + python3-pip \ + python3-venv \ software-properties-common \ wget \ xdg-utils \ zsh \ && rm -rf /var/lib/apt/lists/* +# pipx installs apps into the runtime user's ~/.local/bin. The entrypoint always runs as +# appuser with HOME=/home/appuser, so put that dir on PATH up front and pipx-installed CLIs +# (e.g. `pipx install apm`) are reachable without a per-session `pipx ensurepath`. +# ENV covers the exec'd command and non-login shells; the profile.d drop-in re-adds it for +# login shells, which otherwise reset PATH via /etc/profile (Debian zsh sources it too). +ENV PATH="/home/appuser/.local/bin:${PATH}" +RUN echo 'case ":${PATH}:" in *:/home/appuser/.local/bin:*) ;; *) PATH="/home/appuser/.local/bin:${PATH}" ;; esac' \ + > /etc/profile.d/copilot-local-bin.sh + # --- snippet: docker-cli --- # Install Docker CLI to support brokered Docker socket scenarios. # Only the CLI is installed; the daemon is the host's, mediated by copilot_here's broker. diff --git a/docker/generated/Dockerfile.dotnet-rust b/docker/generated/Dockerfile.dotnet-rust index 83f05d4..932630b 100644 --- a/docker/generated/Dockerfile.dotnet-rust +++ b/docker/generated/Dockerfile.dotnet-rust @@ -10,6 +10,8 @@ FROM node:20-slim ENV DEBIAN_FRONTEND=noninteractive # Install git, curl, gpg, gosu, nano, xdg-utils, zsh, and related utilities for the entrypoint script and testing. +# Python tooling (pip + venv + pipx) ships here so every image can install pip-distributed +# CLIs; Debian 12 enforces PEP 668, so `pipx install ` is the supported path. RUN apt-get update && apt-get install -y \ apt-transport-https \ curl \ @@ -17,12 +19,24 @@ RUN apt-get update && apt-get install -y \ gosu \ gpg \ nano \ + pipx \ + python3-pip \ + python3-venv \ software-properties-common \ wget \ xdg-utils \ zsh \ && rm -rf /var/lib/apt/lists/* +# pipx installs apps into the runtime user's ~/.local/bin. The entrypoint always runs as +# appuser with HOME=/home/appuser, so put that dir on PATH up front and pipx-installed CLIs +# (e.g. `pipx install apm`) are reachable without a per-session `pipx ensurepath`. +# ENV covers the exec'd command and non-login shells; the profile.d drop-in re-adds it for +# login shells, which otherwise reset PATH via /etc/profile (Debian zsh sources it too). +ENV PATH="/home/appuser/.local/bin:${PATH}" +RUN echo 'case ":${PATH}:" in *:/home/appuser/.local/bin:*) ;; *) PATH="/home/appuser/.local/bin:${PATH}" ;; esac' \ + > /etc/profile.d/copilot-local-bin.sh + # --- snippet: docker-cli --- # Install Docker CLI to support brokered Docker socket scenarios. # Only the CLI is installed; the daemon is the host's, mediated by copilot_here's broker. diff --git a/docker/generated/Dockerfile.golang b/docker/generated/Dockerfile.golang index 812a9f0..5fe9210 100644 --- a/docker/generated/Dockerfile.golang +++ b/docker/generated/Dockerfile.golang @@ -10,6 +10,8 @@ FROM node:20-slim ENV DEBIAN_FRONTEND=noninteractive # Install git, curl, gpg, gosu, nano, xdg-utils, zsh, and related utilities for the entrypoint script and testing. +# Python tooling (pip + venv + pipx) ships here so every image can install pip-distributed +# CLIs; Debian 12 enforces PEP 668, so `pipx install ` is the supported path. RUN apt-get update && apt-get install -y \ apt-transport-https \ curl \ @@ -17,12 +19,24 @@ RUN apt-get update && apt-get install -y \ gosu \ gpg \ nano \ + pipx \ + python3-pip \ + python3-venv \ software-properties-common \ wget \ xdg-utils \ zsh \ && rm -rf /var/lib/apt/lists/* +# pipx installs apps into the runtime user's ~/.local/bin. The entrypoint always runs as +# appuser with HOME=/home/appuser, so put that dir on PATH up front and pipx-installed CLIs +# (e.g. `pipx install apm`) are reachable without a per-session `pipx ensurepath`. +# ENV covers the exec'd command and non-login shells; the profile.d drop-in re-adds it for +# login shells, which otherwise reset PATH via /etc/profile (Debian zsh sources it too). +ENV PATH="/home/appuser/.local/bin:${PATH}" +RUN echo 'case ":${PATH}:" in *:/home/appuser/.local/bin:*) ;; *) PATH="/home/appuser/.local/bin:${PATH}" ;; esac' \ + > /etc/profile.d/copilot-local-bin.sh + # --- snippet: docker-cli --- # Install Docker CLI to support brokered Docker socket scenarios. # Only the CLI is installed; the daemon is the host's, mediated by copilot_here's broker. diff --git a/docker/generated/Dockerfile.java b/docker/generated/Dockerfile.java index b252e8e..e658467 100644 --- a/docker/generated/Dockerfile.java +++ b/docker/generated/Dockerfile.java @@ -10,6 +10,8 @@ ENV DEBIAN_FRONTEND=noninteractive # Install git, curl, gpg, gosu, nano, xdg-utils, zsh, and related utilities for the entrypoint script and testing. +# Python tooling (pip + venv + pipx) ships here so every image can install pip-distributed +# CLIs; Debian 12 enforces PEP 668, so `pipx install ` is the supported path. RUN apt-get update && apt-get install -y \ apt-transport-https \ curl \ @@ -17,12 +19,24 @@ gosu \ gpg \ nano \ + pipx \ + python3-pip \ + python3-venv \ software-properties-common \ wget \ xdg-utils \ zsh \ && rm -rf /var/lib/apt/lists/* +# pipx installs apps into the runtime user's ~/.local/bin. The entrypoint always runs as +# appuser with HOME=/home/appuser, so put that dir on PATH up front and pipx-installed CLIs +# (e.g. `pipx install apm`) are reachable without a per-session `pipx ensurepath`. +# ENV covers the exec'd command and non-login shells; the profile.d drop-in re-adds it for +# login shells, which otherwise reset PATH via /etc/profile (Debian zsh sources it too). +ENV PATH="/home/appuser/.local/bin:${PATH}" +RUN echo 'case ":${PATH}:" in *:/home/appuser/.local/bin:*) ;; *) PATH="/home/appuser/.local/bin:${PATH}" ;; esac' \ + > /etc/profile.d/copilot-local-bin.sh + # --- snippet: docker-cli --- # Install Docker CLI to support brokered Docker socket scenarios. # Only the CLI is installed; the daemon is the host's, mediated by copilot_here's broker. diff --git a/docker/generated/Dockerfile.playwright b/docker/generated/Dockerfile.playwright index 37ff6e8..c64b107 100644 --- a/docker/generated/Dockerfile.playwright +++ b/docker/generated/Dockerfile.playwright @@ -10,6 +10,8 @@ FROM node:20-slim ENV DEBIAN_FRONTEND=noninteractive # Install git, curl, gpg, gosu, nano, xdg-utils, zsh, and related utilities for the entrypoint script and testing. +# Python tooling (pip + venv + pipx) ships here so every image can install pip-distributed +# CLIs; Debian 12 enforces PEP 668, so `pipx install ` is the supported path. RUN apt-get update && apt-get install -y \ apt-transport-https \ curl \ @@ -17,12 +19,24 @@ RUN apt-get update && apt-get install -y \ gosu \ gpg \ nano \ + pipx \ + python3-pip \ + python3-venv \ software-properties-common \ wget \ xdg-utils \ zsh \ && rm -rf /var/lib/apt/lists/* +# pipx installs apps into the runtime user's ~/.local/bin. The entrypoint always runs as +# appuser with HOME=/home/appuser, so put that dir on PATH up front and pipx-installed CLIs +# (e.g. `pipx install apm`) are reachable without a per-session `pipx ensurepath`. +# ENV covers the exec'd command and non-login shells; the profile.d drop-in re-adds it for +# login shells, which otherwise reset PATH via /etc/profile (Debian zsh sources it too). +ENV PATH="/home/appuser/.local/bin:${PATH}" +RUN echo 'case ":${PATH}:" in *:/home/appuser/.local/bin:*) ;; *) PATH="/home/appuser/.local/bin:${PATH}" ;; esac' \ + > /etc/profile.d/copilot-local-bin.sh + # --- snippet: docker-cli --- # Install Docker CLI to support brokered Docker socket scenarios. # Only the CLI is installed; the daemon is the host's, mediated by copilot_here's broker. diff --git a/docker/generated/Dockerfile.rust b/docker/generated/Dockerfile.rust index d133e5d..1322385 100644 --- a/docker/generated/Dockerfile.rust +++ b/docker/generated/Dockerfile.rust @@ -10,6 +10,8 @@ FROM node:20-slim ENV DEBIAN_FRONTEND=noninteractive # Install git, curl, gpg, gosu, nano, xdg-utils, zsh, and related utilities for the entrypoint script and testing. +# Python tooling (pip + venv + pipx) ships here so every image can install pip-distributed +# CLIs; Debian 12 enforces PEP 668, so `pipx install ` is the supported path. RUN apt-get update && apt-get install -y \ apt-transport-https \ curl \ @@ -17,12 +19,24 @@ RUN apt-get update && apt-get install -y \ gosu \ gpg \ nano \ + pipx \ + python3-pip \ + python3-venv \ software-properties-common \ wget \ xdg-utils \ zsh \ && rm -rf /var/lib/apt/lists/* +# pipx installs apps into the runtime user's ~/.local/bin. The entrypoint always runs as +# appuser with HOME=/home/appuser, so put that dir on PATH up front and pipx-installed CLIs +# (e.g. `pipx install apm`) are reachable without a per-session `pipx ensurepath`. +# ENV covers the exec'd command and non-login shells; the profile.d drop-in re-adds it for +# login shells, which otherwise reset PATH via /etc/profile (Debian zsh sources it too). +ENV PATH="/home/appuser/.local/bin:${PATH}" +RUN echo 'case ":${PATH}:" in *:/home/appuser/.local/bin:*) ;; *) PATH="/home/appuser/.local/bin:${PATH}" ;; esac' \ + > /etc/profile.d/copilot-local-bin.sh + # --- snippet: docker-cli --- # Install Docker CLI to support brokered Docker socket scenarios. # Only the CLI is installed; the daemon is the host's, mediated by copilot_here's broker. diff --git a/docker/snippets/system-packages.Dockerfile b/docker/snippets/system-packages.Dockerfile index 8d88e75..55585f7 100644 --- a/docker/snippets/system-packages.Dockerfile +++ b/docker/snippets/system-packages.Dockerfile @@ -2,6 +2,8 @@ ENV DEBIAN_FRONTEND=noninteractive # Install git, curl, gpg, gosu, nano, xdg-utils, zsh, and related utilities for the entrypoint script and testing. +# Python tooling (pip + venv + pipx) ships here so every image can install pip-distributed +# CLIs; Debian 12 enforces PEP 668, so `pipx install ` is the supported path. RUN apt-get update && apt-get install -y \ apt-transport-https \ curl \ @@ -9,8 +11,20 @@ RUN apt-get update && apt-get install -y \ gosu \ gpg \ nano \ + pipx \ + python3-pip \ + python3-venv \ software-properties-common \ wget \ xdg-utils \ zsh \ && rm -rf /var/lib/apt/lists/* + +# pipx installs apps into the runtime user's ~/.local/bin. The entrypoint always runs as +# appuser with HOME=/home/appuser, so put that dir on PATH up front and pipx-installed CLIs +# (e.g. `pipx install apm`) are reachable without a per-session `pipx ensurepath`. +# ENV covers the exec'd command and non-login shells; the profile.d drop-in re-adds it for +# login shells, which otherwise reset PATH via /etc/profile (Debian zsh sources it too). +ENV PATH="/home/appuser/.local/bin:${PATH}" +RUN echo 'case ":${PATH}:" in *:/home/appuser/.local/bin:*) ;; *) PATH="/home/appuser/.local/bin:${PATH}" ;; esac' \ + > /etc/profile.d/copilot-local-bin.sh diff --git a/docs/docker-images.md b/docs/docker-images.md index 0daa6d3..9e06457 100644 --- a/docs/docker-images.md +++ b/docs/docker-images.md @@ -7,6 +7,7 @@ This repository publishes multiple Docker image variants to optimize for size an The standard copilot_here image with: - Node.js 20 +- Python 3 with pip & pipx (Debian 12 enforces PEP 668, so `pipx install ` is the supported path for pip-distributed CLIs) - GitHub Copilot CLI - Git, curl, gpg, gosu From 3a123cf1c46d4c9bd5e9de7a01df9dbf231d6e1f Mon Sep 17 00:00:00 2001 From: Gordon Beeming Date: Mon, 29 Jun 2026 09:44:27 +1000 Subject: [PATCH 2/3] fix: address review feedback on system-packages snippet - Declare python3 explicitly in the apt list instead of relying on it being pulled in transitively by software-properties-common, so the Python tooling install is self-contained and won't break if that transitive dep changes. - Use POSIX ${PATH:+:${PATH}} expansion in the profile.d drop-in so an empty or unset PATH doesn't produce a trailing colon (which would implicitly add the cwd to the search path). Co-authored-by: Claude Co-authored-by: GitButler --- docker/generated/Dockerfile.default | 3 ++- docker/generated/Dockerfile.dotnet | 3 ++- docker/generated/Dockerfile.dotnet-10 | 3 ++- docker/generated/Dockerfile.dotnet-8 | 3 ++- docker/generated/Dockerfile.dotnet-9 | 3 ++- docker/generated/Dockerfile.dotnet-playwright | 3 ++- docker/generated/Dockerfile.dotnet-rust | 3 ++- docker/generated/Dockerfile.golang | 3 ++- docker/generated/Dockerfile.java | 3 ++- docker/generated/Dockerfile.playwright | 3 ++- docker/generated/Dockerfile.rust | 3 ++- docker/snippets/system-packages.Dockerfile | 3 ++- 12 files changed, 24 insertions(+), 12 deletions(-) diff --git a/docker/generated/Dockerfile.default b/docker/generated/Dockerfile.default index 62bc9ba..01174f2 100644 --- a/docker/generated/Dockerfile.default +++ b/docker/generated/Dockerfile.default @@ -20,6 +20,7 @@ RUN apt-get update && apt-get install -y \ gpg \ nano \ pipx \ + python3 \ python3-pip \ python3-venv \ software-properties-common \ @@ -34,7 +35,7 @@ RUN apt-get update && apt-get install -y \ # ENV covers the exec'd command and non-login shells; the profile.d drop-in re-adds it for # login shells, which otherwise reset PATH via /etc/profile (Debian zsh sources it too). ENV PATH="/home/appuser/.local/bin:${PATH}" -RUN echo 'case ":${PATH}:" in *:/home/appuser/.local/bin:*) ;; *) PATH="/home/appuser/.local/bin:${PATH}" ;; esac' \ +RUN echo 'case ":${PATH}:" in *:/home/appuser/.local/bin:*) ;; *) PATH="/home/appuser/.local/bin${PATH:+:${PATH}}" ;; esac' \ > /etc/profile.d/copilot-local-bin.sh # --- snippet: docker-cli --- diff --git a/docker/generated/Dockerfile.dotnet b/docker/generated/Dockerfile.dotnet index 7679c61..ee6bb07 100644 --- a/docker/generated/Dockerfile.dotnet +++ b/docker/generated/Dockerfile.dotnet @@ -20,6 +20,7 @@ RUN apt-get update && apt-get install -y \ gpg \ nano \ pipx \ + python3 \ python3-pip \ python3-venv \ software-properties-common \ @@ -34,7 +35,7 @@ RUN apt-get update && apt-get install -y \ # ENV covers the exec'd command and non-login shells; the profile.d drop-in re-adds it for # login shells, which otherwise reset PATH via /etc/profile (Debian zsh sources it too). ENV PATH="/home/appuser/.local/bin:${PATH}" -RUN echo 'case ":${PATH}:" in *:/home/appuser/.local/bin:*) ;; *) PATH="/home/appuser/.local/bin:${PATH}" ;; esac' \ +RUN echo 'case ":${PATH}:" in *:/home/appuser/.local/bin:*) ;; *) PATH="/home/appuser/.local/bin${PATH:+:${PATH}}" ;; esac' \ > /etc/profile.d/copilot-local-bin.sh # --- snippet: docker-cli --- diff --git a/docker/generated/Dockerfile.dotnet-10 b/docker/generated/Dockerfile.dotnet-10 index c88ff37..bd64651 100644 --- a/docker/generated/Dockerfile.dotnet-10 +++ b/docker/generated/Dockerfile.dotnet-10 @@ -20,6 +20,7 @@ RUN apt-get update && apt-get install -y \ gpg \ nano \ pipx \ + python3 \ python3-pip \ python3-venv \ software-properties-common \ @@ -34,7 +35,7 @@ RUN apt-get update && apt-get install -y \ # ENV covers the exec'd command and non-login shells; the profile.d drop-in re-adds it for # login shells, which otherwise reset PATH via /etc/profile (Debian zsh sources it too). ENV PATH="/home/appuser/.local/bin:${PATH}" -RUN echo 'case ":${PATH}:" in *:/home/appuser/.local/bin:*) ;; *) PATH="/home/appuser/.local/bin:${PATH}" ;; esac' \ +RUN echo 'case ":${PATH}:" in *:/home/appuser/.local/bin:*) ;; *) PATH="/home/appuser/.local/bin${PATH:+:${PATH}}" ;; esac' \ > /etc/profile.d/copilot-local-bin.sh # --- snippet: docker-cli --- diff --git a/docker/generated/Dockerfile.dotnet-8 b/docker/generated/Dockerfile.dotnet-8 index 6e3520e..a16c6eb 100644 --- a/docker/generated/Dockerfile.dotnet-8 +++ b/docker/generated/Dockerfile.dotnet-8 @@ -20,6 +20,7 @@ RUN apt-get update && apt-get install -y \ gpg \ nano \ pipx \ + python3 \ python3-pip \ python3-venv \ software-properties-common \ @@ -34,7 +35,7 @@ RUN apt-get update && apt-get install -y \ # ENV covers the exec'd command and non-login shells; the profile.d drop-in re-adds it for # login shells, which otherwise reset PATH via /etc/profile (Debian zsh sources it too). ENV PATH="/home/appuser/.local/bin:${PATH}" -RUN echo 'case ":${PATH}:" in *:/home/appuser/.local/bin:*) ;; *) PATH="/home/appuser/.local/bin:${PATH}" ;; esac' \ +RUN echo 'case ":${PATH}:" in *:/home/appuser/.local/bin:*) ;; *) PATH="/home/appuser/.local/bin${PATH:+:${PATH}}" ;; esac' \ > /etc/profile.d/copilot-local-bin.sh # --- snippet: docker-cli --- diff --git a/docker/generated/Dockerfile.dotnet-9 b/docker/generated/Dockerfile.dotnet-9 index 1ce6e5b..8d21c09 100644 --- a/docker/generated/Dockerfile.dotnet-9 +++ b/docker/generated/Dockerfile.dotnet-9 @@ -20,6 +20,7 @@ RUN apt-get update && apt-get install -y \ gpg \ nano \ pipx \ + python3 \ python3-pip \ python3-venv \ software-properties-common \ @@ -34,7 +35,7 @@ RUN apt-get update && apt-get install -y \ # ENV covers the exec'd command and non-login shells; the profile.d drop-in re-adds it for # login shells, which otherwise reset PATH via /etc/profile (Debian zsh sources it too). ENV PATH="/home/appuser/.local/bin:${PATH}" -RUN echo 'case ":${PATH}:" in *:/home/appuser/.local/bin:*) ;; *) PATH="/home/appuser/.local/bin:${PATH}" ;; esac' \ +RUN echo 'case ":${PATH}:" in *:/home/appuser/.local/bin:*) ;; *) PATH="/home/appuser/.local/bin${PATH:+:${PATH}}" ;; esac' \ > /etc/profile.d/copilot-local-bin.sh # --- snippet: docker-cli --- diff --git a/docker/generated/Dockerfile.dotnet-playwright b/docker/generated/Dockerfile.dotnet-playwright index 3c048f9..fcc469a 100644 --- a/docker/generated/Dockerfile.dotnet-playwright +++ b/docker/generated/Dockerfile.dotnet-playwright @@ -20,6 +20,7 @@ RUN apt-get update && apt-get install -y \ gpg \ nano \ pipx \ + python3 \ python3-pip \ python3-venv \ software-properties-common \ @@ -34,7 +35,7 @@ RUN apt-get update && apt-get install -y \ # ENV covers the exec'd command and non-login shells; the profile.d drop-in re-adds it for # login shells, which otherwise reset PATH via /etc/profile (Debian zsh sources it too). ENV PATH="/home/appuser/.local/bin:${PATH}" -RUN echo 'case ":${PATH}:" in *:/home/appuser/.local/bin:*) ;; *) PATH="/home/appuser/.local/bin:${PATH}" ;; esac' \ +RUN echo 'case ":${PATH}:" in *:/home/appuser/.local/bin:*) ;; *) PATH="/home/appuser/.local/bin${PATH:+:${PATH}}" ;; esac' \ > /etc/profile.d/copilot-local-bin.sh # --- snippet: docker-cli --- diff --git a/docker/generated/Dockerfile.dotnet-rust b/docker/generated/Dockerfile.dotnet-rust index 932630b..2b41672 100644 --- a/docker/generated/Dockerfile.dotnet-rust +++ b/docker/generated/Dockerfile.dotnet-rust @@ -20,6 +20,7 @@ RUN apt-get update && apt-get install -y \ gpg \ nano \ pipx \ + python3 \ python3-pip \ python3-venv \ software-properties-common \ @@ -34,7 +35,7 @@ RUN apt-get update && apt-get install -y \ # ENV covers the exec'd command and non-login shells; the profile.d drop-in re-adds it for # login shells, which otherwise reset PATH via /etc/profile (Debian zsh sources it too). ENV PATH="/home/appuser/.local/bin:${PATH}" -RUN echo 'case ":${PATH}:" in *:/home/appuser/.local/bin:*) ;; *) PATH="/home/appuser/.local/bin:${PATH}" ;; esac' \ +RUN echo 'case ":${PATH}:" in *:/home/appuser/.local/bin:*) ;; *) PATH="/home/appuser/.local/bin${PATH:+:${PATH}}" ;; esac' \ > /etc/profile.d/copilot-local-bin.sh # --- snippet: docker-cli --- diff --git a/docker/generated/Dockerfile.golang b/docker/generated/Dockerfile.golang index 5fe9210..13551ae 100644 --- a/docker/generated/Dockerfile.golang +++ b/docker/generated/Dockerfile.golang @@ -20,6 +20,7 @@ RUN apt-get update && apt-get install -y \ gpg \ nano \ pipx \ + python3 \ python3-pip \ python3-venv \ software-properties-common \ @@ -34,7 +35,7 @@ RUN apt-get update && apt-get install -y \ # ENV covers the exec'd command and non-login shells; the profile.d drop-in re-adds it for # login shells, which otherwise reset PATH via /etc/profile (Debian zsh sources it too). ENV PATH="/home/appuser/.local/bin:${PATH}" -RUN echo 'case ":${PATH}:" in *:/home/appuser/.local/bin:*) ;; *) PATH="/home/appuser/.local/bin:${PATH}" ;; esac' \ +RUN echo 'case ":${PATH}:" in *:/home/appuser/.local/bin:*) ;; *) PATH="/home/appuser/.local/bin${PATH:+:${PATH}}" ;; esac' \ > /etc/profile.d/copilot-local-bin.sh # --- snippet: docker-cli --- diff --git a/docker/generated/Dockerfile.java b/docker/generated/Dockerfile.java index e658467..d53b431 100644 --- a/docker/generated/Dockerfile.java +++ b/docker/generated/Dockerfile.java @@ -20,6 +20,7 @@ gpg \ nano \ pipx \ + python3 \ python3-pip \ python3-venv \ software-properties-common \ @@ -34,7 +35,7 @@ # ENV covers the exec'd command and non-login shells; the profile.d drop-in re-adds it for # login shells, which otherwise reset PATH via /etc/profile (Debian zsh sources it too). ENV PATH="/home/appuser/.local/bin:${PATH}" -RUN echo 'case ":${PATH}:" in *:/home/appuser/.local/bin:*) ;; *) PATH="/home/appuser/.local/bin:${PATH}" ;; esac' \ +RUN echo 'case ":${PATH}:" in *:/home/appuser/.local/bin:*) ;; *) PATH="/home/appuser/.local/bin${PATH:+:${PATH}}" ;; esac' \ > /etc/profile.d/copilot-local-bin.sh # --- snippet: docker-cli --- diff --git a/docker/generated/Dockerfile.playwright b/docker/generated/Dockerfile.playwright index c64b107..fea9ff8 100644 --- a/docker/generated/Dockerfile.playwright +++ b/docker/generated/Dockerfile.playwright @@ -20,6 +20,7 @@ RUN apt-get update && apt-get install -y \ gpg \ nano \ pipx \ + python3 \ python3-pip \ python3-venv \ software-properties-common \ @@ -34,7 +35,7 @@ RUN apt-get update && apt-get install -y \ # ENV covers the exec'd command and non-login shells; the profile.d drop-in re-adds it for # login shells, which otherwise reset PATH via /etc/profile (Debian zsh sources it too). ENV PATH="/home/appuser/.local/bin:${PATH}" -RUN echo 'case ":${PATH}:" in *:/home/appuser/.local/bin:*) ;; *) PATH="/home/appuser/.local/bin:${PATH}" ;; esac' \ +RUN echo 'case ":${PATH}:" in *:/home/appuser/.local/bin:*) ;; *) PATH="/home/appuser/.local/bin${PATH:+:${PATH}}" ;; esac' \ > /etc/profile.d/copilot-local-bin.sh # --- snippet: docker-cli --- diff --git a/docker/generated/Dockerfile.rust b/docker/generated/Dockerfile.rust index 1322385..2772bfd 100644 --- a/docker/generated/Dockerfile.rust +++ b/docker/generated/Dockerfile.rust @@ -20,6 +20,7 @@ RUN apt-get update && apt-get install -y \ gpg \ nano \ pipx \ + python3 \ python3-pip \ python3-venv \ software-properties-common \ @@ -34,7 +35,7 @@ RUN apt-get update && apt-get install -y \ # ENV covers the exec'd command and non-login shells; the profile.d drop-in re-adds it for # login shells, which otherwise reset PATH via /etc/profile (Debian zsh sources it too). ENV PATH="/home/appuser/.local/bin:${PATH}" -RUN echo 'case ":${PATH}:" in *:/home/appuser/.local/bin:*) ;; *) PATH="/home/appuser/.local/bin:${PATH}" ;; esac' \ +RUN echo 'case ":${PATH}:" in *:/home/appuser/.local/bin:*) ;; *) PATH="/home/appuser/.local/bin${PATH:+:${PATH}}" ;; esac' \ > /etc/profile.d/copilot-local-bin.sh # --- snippet: docker-cli --- diff --git a/docker/snippets/system-packages.Dockerfile b/docker/snippets/system-packages.Dockerfile index 55585f7..d5224dd 100644 --- a/docker/snippets/system-packages.Dockerfile +++ b/docker/snippets/system-packages.Dockerfile @@ -12,6 +12,7 @@ RUN apt-get update && apt-get install -y \ gpg \ nano \ pipx \ + python3 \ python3-pip \ python3-venv \ software-properties-common \ @@ -26,5 +27,5 @@ RUN apt-get update && apt-get install -y \ # ENV covers the exec'd command and non-login shells; the profile.d drop-in re-adds it for # login shells, which otherwise reset PATH via /etc/profile (Debian zsh sources it too). ENV PATH="/home/appuser/.local/bin:${PATH}" -RUN echo 'case ":${PATH}:" in *:/home/appuser/.local/bin:*) ;; *) PATH="/home/appuser/.local/bin:${PATH}" ;; esac' \ +RUN echo 'case ":${PATH}:" in *:/home/appuser/.local/bin:*) ;; *) PATH="/home/appuser/.local/bin${PATH:+:${PATH}}" ;; esac' \ > /etc/profile.d/copilot-local-bin.sh From 7b378507820ee2d6986ef0c1b7acd6d837cd0650 Mon Sep 17 00:00:00 2001 From: Gordon Beeming Date: Mon, 29 Jun 2026 09:46:33 +1000 Subject: [PATCH 3/3] fix: append ~/.local/bin to PATH instead of prepending (security) The entrypoint runs as root and resolves tools (getent, groupadd, node, ...) via PATH before dropping to appuser. Prepending the user-writable (and possibly bind-mounted) /home/appuser/.local/bin meant a planted binary there could be executed as root. Append it instead so system binaries always take precedence; pipx-installed apps still resolve since their names don't collide with system binaries. Co-authored-by: Claude Co-authored-by: GitButler --- docker/generated/Dockerfile.default | 15 ++++++++++----- docker/generated/Dockerfile.dotnet | 15 ++++++++++----- docker/generated/Dockerfile.dotnet-10 | 15 ++++++++++----- docker/generated/Dockerfile.dotnet-8 | 15 ++++++++++----- docker/generated/Dockerfile.dotnet-9 | 15 ++++++++++----- docker/generated/Dockerfile.dotnet-playwright | 15 ++++++++++----- docker/generated/Dockerfile.dotnet-rust | 15 ++++++++++----- docker/generated/Dockerfile.golang | 15 ++++++++++----- docker/generated/Dockerfile.java | 15 ++++++++++----- docker/generated/Dockerfile.playwright | 15 ++++++++++----- docker/generated/Dockerfile.rust | 15 ++++++++++----- docker/snippets/system-packages.Dockerfile | 15 ++++++++++----- 12 files changed, 120 insertions(+), 60 deletions(-) diff --git a/docker/generated/Dockerfile.default b/docker/generated/Dockerfile.default index 01174f2..d972272 100644 --- a/docker/generated/Dockerfile.default +++ b/docker/generated/Dockerfile.default @@ -29,13 +29,18 @@ RUN apt-get update && apt-get install -y \ zsh \ && rm -rf /var/lib/apt/lists/* -# pipx installs apps into the runtime user's ~/.local/bin. The entrypoint always runs as -# appuser with HOME=/home/appuser, so put that dir on PATH up front and pipx-installed CLIs -# (e.g. `pipx install apm`) are reachable without a per-session `pipx ensurepath`. +# pipx installs apps into the runtime user's ~/.local/bin, so that dir needs to be on PATH +# for pipx-installed CLIs (e.g. `pipx install apm`) to be reachable without a per-session +# `pipx ensurepath`. The entrypoint always runs as appuser with HOME=/home/appuser. +# Append (not prepend): the entrypoint runs as root and resolves tools like getent/groupadd/ +# node via PATH before dropping to appuser, so a system binary must always win over anything +# in the user-writable (and possibly bind-mounted) ~/.local/bin — otherwise a planted binary +# there could execute as root. Appending keeps pipx apps reachable while system paths stay +# authoritative; pipx app names don't collide with system binaries. # ENV covers the exec'd command and non-login shells; the profile.d drop-in re-adds it for # login shells, which otherwise reset PATH via /etc/profile (Debian zsh sources it too). -ENV PATH="/home/appuser/.local/bin:${PATH}" -RUN echo 'case ":${PATH}:" in *:/home/appuser/.local/bin:*) ;; *) PATH="/home/appuser/.local/bin${PATH:+:${PATH}}" ;; esac' \ +ENV PATH="${PATH}:/home/appuser/.local/bin" +RUN echo 'case ":${PATH}:" in *:/home/appuser/.local/bin:*) ;; *) PATH="${PATH:+${PATH}:}/home/appuser/.local/bin" ;; esac' \ > /etc/profile.d/copilot-local-bin.sh # --- snippet: docker-cli --- diff --git a/docker/generated/Dockerfile.dotnet b/docker/generated/Dockerfile.dotnet index ee6bb07..8a1756c 100644 --- a/docker/generated/Dockerfile.dotnet +++ b/docker/generated/Dockerfile.dotnet @@ -29,13 +29,18 @@ RUN apt-get update && apt-get install -y \ zsh \ && rm -rf /var/lib/apt/lists/* -# pipx installs apps into the runtime user's ~/.local/bin. The entrypoint always runs as -# appuser with HOME=/home/appuser, so put that dir on PATH up front and pipx-installed CLIs -# (e.g. `pipx install apm`) are reachable without a per-session `pipx ensurepath`. +# pipx installs apps into the runtime user's ~/.local/bin, so that dir needs to be on PATH +# for pipx-installed CLIs (e.g. `pipx install apm`) to be reachable without a per-session +# `pipx ensurepath`. The entrypoint always runs as appuser with HOME=/home/appuser. +# Append (not prepend): the entrypoint runs as root and resolves tools like getent/groupadd/ +# node via PATH before dropping to appuser, so a system binary must always win over anything +# in the user-writable (and possibly bind-mounted) ~/.local/bin — otherwise a planted binary +# there could execute as root. Appending keeps pipx apps reachable while system paths stay +# authoritative; pipx app names don't collide with system binaries. # ENV covers the exec'd command and non-login shells; the profile.d drop-in re-adds it for # login shells, which otherwise reset PATH via /etc/profile (Debian zsh sources it too). -ENV PATH="/home/appuser/.local/bin:${PATH}" -RUN echo 'case ":${PATH}:" in *:/home/appuser/.local/bin:*) ;; *) PATH="/home/appuser/.local/bin${PATH:+:${PATH}}" ;; esac' \ +ENV PATH="${PATH}:/home/appuser/.local/bin" +RUN echo 'case ":${PATH}:" in *:/home/appuser/.local/bin:*) ;; *) PATH="${PATH:+${PATH}:}/home/appuser/.local/bin" ;; esac' \ > /etc/profile.d/copilot-local-bin.sh # --- snippet: docker-cli --- diff --git a/docker/generated/Dockerfile.dotnet-10 b/docker/generated/Dockerfile.dotnet-10 index bd64651..744e08f 100644 --- a/docker/generated/Dockerfile.dotnet-10 +++ b/docker/generated/Dockerfile.dotnet-10 @@ -29,13 +29,18 @@ RUN apt-get update && apt-get install -y \ zsh \ && rm -rf /var/lib/apt/lists/* -# pipx installs apps into the runtime user's ~/.local/bin. The entrypoint always runs as -# appuser with HOME=/home/appuser, so put that dir on PATH up front and pipx-installed CLIs -# (e.g. `pipx install apm`) are reachable without a per-session `pipx ensurepath`. +# pipx installs apps into the runtime user's ~/.local/bin, so that dir needs to be on PATH +# for pipx-installed CLIs (e.g. `pipx install apm`) to be reachable without a per-session +# `pipx ensurepath`. The entrypoint always runs as appuser with HOME=/home/appuser. +# Append (not prepend): the entrypoint runs as root and resolves tools like getent/groupadd/ +# node via PATH before dropping to appuser, so a system binary must always win over anything +# in the user-writable (and possibly bind-mounted) ~/.local/bin — otherwise a planted binary +# there could execute as root. Appending keeps pipx apps reachable while system paths stay +# authoritative; pipx app names don't collide with system binaries. # ENV covers the exec'd command and non-login shells; the profile.d drop-in re-adds it for # login shells, which otherwise reset PATH via /etc/profile (Debian zsh sources it too). -ENV PATH="/home/appuser/.local/bin:${PATH}" -RUN echo 'case ":${PATH}:" in *:/home/appuser/.local/bin:*) ;; *) PATH="/home/appuser/.local/bin${PATH:+:${PATH}}" ;; esac' \ +ENV PATH="${PATH}:/home/appuser/.local/bin" +RUN echo 'case ":${PATH}:" in *:/home/appuser/.local/bin:*) ;; *) PATH="${PATH:+${PATH}:}/home/appuser/.local/bin" ;; esac' \ > /etc/profile.d/copilot-local-bin.sh # --- snippet: docker-cli --- diff --git a/docker/generated/Dockerfile.dotnet-8 b/docker/generated/Dockerfile.dotnet-8 index a16c6eb..677e39e 100644 --- a/docker/generated/Dockerfile.dotnet-8 +++ b/docker/generated/Dockerfile.dotnet-8 @@ -29,13 +29,18 @@ RUN apt-get update && apt-get install -y \ zsh \ && rm -rf /var/lib/apt/lists/* -# pipx installs apps into the runtime user's ~/.local/bin. The entrypoint always runs as -# appuser with HOME=/home/appuser, so put that dir on PATH up front and pipx-installed CLIs -# (e.g. `pipx install apm`) are reachable without a per-session `pipx ensurepath`. +# pipx installs apps into the runtime user's ~/.local/bin, so that dir needs to be on PATH +# for pipx-installed CLIs (e.g. `pipx install apm`) to be reachable without a per-session +# `pipx ensurepath`. The entrypoint always runs as appuser with HOME=/home/appuser. +# Append (not prepend): the entrypoint runs as root and resolves tools like getent/groupadd/ +# node via PATH before dropping to appuser, so a system binary must always win over anything +# in the user-writable (and possibly bind-mounted) ~/.local/bin — otherwise a planted binary +# there could execute as root. Appending keeps pipx apps reachable while system paths stay +# authoritative; pipx app names don't collide with system binaries. # ENV covers the exec'd command and non-login shells; the profile.d drop-in re-adds it for # login shells, which otherwise reset PATH via /etc/profile (Debian zsh sources it too). -ENV PATH="/home/appuser/.local/bin:${PATH}" -RUN echo 'case ":${PATH}:" in *:/home/appuser/.local/bin:*) ;; *) PATH="/home/appuser/.local/bin${PATH:+:${PATH}}" ;; esac' \ +ENV PATH="${PATH}:/home/appuser/.local/bin" +RUN echo 'case ":${PATH}:" in *:/home/appuser/.local/bin:*) ;; *) PATH="${PATH:+${PATH}:}/home/appuser/.local/bin" ;; esac' \ > /etc/profile.d/copilot-local-bin.sh # --- snippet: docker-cli --- diff --git a/docker/generated/Dockerfile.dotnet-9 b/docker/generated/Dockerfile.dotnet-9 index 8d21c09..1ebb7dc 100644 --- a/docker/generated/Dockerfile.dotnet-9 +++ b/docker/generated/Dockerfile.dotnet-9 @@ -29,13 +29,18 @@ RUN apt-get update && apt-get install -y \ zsh \ && rm -rf /var/lib/apt/lists/* -# pipx installs apps into the runtime user's ~/.local/bin. The entrypoint always runs as -# appuser with HOME=/home/appuser, so put that dir on PATH up front and pipx-installed CLIs -# (e.g. `pipx install apm`) are reachable without a per-session `pipx ensurepath`. +# pipx installs apps into the runtime user's ~/.local/bin, so that dir needs to be on PATH +# for pipx-installed CLIs (e.g. `pipx install apm`) to be reachable without a per-session +# `pipx ensurepath`. The entrypoint always runs as appuser with HOME=/home/appuser. +# Append (not prepend): the entrypoint runs as root and resolves tools like getent/groupadd/ +# node via PATH before dropping to appuser, so a system binary must always win over anything +# in the user-writable (and possibly bind-mounted) ~/.local/bin — otherwise a planted binary +# there could execute as root. Appending keeps pipx apps reachable while system paths stay +# authoritative; pipx app names don't collide with system binaries. # ENV covers the exec'd command and non-login shells; the profile.d drop-in re-adds it for # login shells, which otherwise reset PATH via /etc/profile (Debian zsh sources it too). -ENV PATH="/home/appuser/.local/bin:${PATH}" -RUN echo 'case ":${PATH}:" in *:/home/appuser/.local/bin:*) ;; *) PATH="/home/appuser/.local/bin${PATH:+:${PATH}}" ;; esac' \ +ENV PATH="${PATH}:/home/appuser/.local/bin" +RUN echo 'case ":${PATH}:" in *:/home/appuser/.local/bin:*) ;; *) PATH="${PATH:+${PATH}:}/home/appuser/.local/bin" ;; esac' \ > /etc/profile.d/copilot-local-bin.sh # --- snippet: docker-cli --- diff --git a/docker/generated/Dockerfile.dotnet-playwright b/docker/generated/Dockerfile.dotnet-playwright index fcc469a..40a0ecd 100644 --- a/docker/generated/Dockerfile.dotnet-playwright +++ b/docker/generated/Dockerfile.dotnet-playwright @@ -29,13 +29,18 @@ RUN apt-get update && apt-get install -y \ zsh \ && rm -rf /var/lib/apt/lists/* -# pipx installs apps into the runtime user's ~/.local/bin. The entrypoint always runs as -# appuser with HOME=/home/appuser, so put that dir on PATH up front and pipx-installed CLIs -# (e.g. `pipx install apm`) are reachable without a per-session `pipx ensurepath`. +# pipx installs apps into the runtime user's ~/.local/bin, so that dir needs to be on PATH +# for pipx-installed CLIs (e.g. `pipx install apm`) to be reachable without a per-session +# `pipx ensurepath`. The entrypoint always runs as appuser with HOME=/home/appuser. +# Append (not prepend): the entrypoint runs as root and resolves tools like getent/groupadd/ +# node via PATH before dropping to appuser, so a system binary must always win over anything +# in the user-writable (and possibly bind-mounted) ~/.local/bin — otherwise a planted binary +# there could execute as root. Appending keeps pipx apps reachable while system paths stay +# authoritative; pipx app names don't collide with system binaries. # ENV covers the exec'd command and non-login shells; the profile.d drop-in re-adds it for # login shells, which otherwise reset PATH via /etc/profile (Debian zsh sources it too). -ENV PATH="/home/appuser/.local/bin:${PATH}" -RUN echo 'case ":${PATH}:" in *:/home/appuser/.local/bin:*) ;; *) PATH="/home/appuser/.local/bin${PATH:+:${PATH}}" ;; esac' \ +ENV PATH="${PATH}:/home/appuser/.local/bin" +RUN echo 'case ":${PATH}:" in *:/home/appuser/.local/bin:*) ;; *) PATH="${PATH:+${PATH}:}/home/appuser/.local/bin" ;; esac' \ > /etc/profile.d/copilot-local-bin.sh # --- snippet: docker-cli --- diff --git a/docker/generated/Dockerfile.dotnet-rust b/docker/generated/Dockerfile.dotnet-rust index 2b41672..9d9ab21 100644 --- a/docker/generated/Dockerfile.dotnet-rust +++ b/docker/generated/Dockerfile.dotnet-rust @@ -29,13 +29,18 @@ RUN apt-get update && apt-get install -y \ zsh \ && rm -rf /var/lib/apt/lists/* -# pipx installs apps into the runtime user's ~/.local/bin. The entrypoint always runs as -# appuser with HOME=/home/appuser, so put that dir on PATH up front and pipx-installed CLIs -# (e.g. `pipx install apm`) are reachable without a per-session `pipx ensurepath`. +# pipx installs apps into the runtime user's ~/.local/bin, so that dir needs to be on PATH +# for pipx-installed CLIs (e.g. `pipx install apm`) to be reachable without a per-session +# `pipx ensurepath`. The entrypoint always runs as appuser with HOME=/home/appuser. +# Append (not prepend): the entrypoint runs as root and resolves tools like getent/groupadd/ +# node via PATH before dropping to appuser, so a system binary must always win over anything +# in the user-writable (and possibly bind-mounted) ~/.local/bin — otherwise a planted binary +# there could execute as root. Appending keeps pipx apps reachable while system paths stay +# authoritative; pipx app names don't collide with system binaries. # ENV covers the exec'd command and non-login shells; the profile.d drop-in re-adds it for # login shells, which otherwise reset PATH via /etc/profile (Debian zsh sources it too). -ENV PATH="/home/appuser/.local/bin:${PATH}" -RUN echo 'case ":${PATH}:" in *:/home/appuser/.local/bin:*) ;; *) PATH="/home/appuser/.local/bin${PATH:+:${PATH}}" ;; esac' \ +ENV PATH="${PATH}:/home/appuser/.local/bin" +RUN echo 'case ":${PATH}:" in *:/home/appuser/.local/bin:*) ;; *) PATH="${PATH:+${PATH}:}/home/appuser/.local/bin" ;; esac' \ > /etc/profile.d/copilot-local-bin.sh # --- snippet: docker-cli --- diff --git a/docker/generated/Dockerfile.golang b/docker/generated/Dockerfile.golang index 13551ae..81c885f 100644 --- a/docker/generated/Dockerfile.golang +++ b/docker/generated/Dockerfile.golang @@ -29,13 +29,18 @@ RUN apt-get update && apt-get install -y \ zsh \ && rm -rf /var/lib/apt/lists/* -# pipx installs apps into the runtime user's ~/.local/bin. The entrypoint always runs as -# appuser with HOME=/home/appuser, so put that dir on PATH up front and pipx-installed CLIs -# (e.g. `pipx install apm`) are reachable without a per-session `pipx ensurepath`. +# pipx installs apps into the runtime user's ~/.local/bin, so that dir needs to be on PATH +# for pipx-installed CLIs (e.g. `pipx install apm`) to be reachable without a per-session +# `pipx ensurepath`. The entrypoint always runs as appuser with HOME=/home/appuser. +# Append (not prepend): the entrypoint runs as root and resolves tools like getent/groupadd/ +# node via PATH before dropping to appuser, so a system binary must always win over anything +# in the user-writable (and possibly bind-mounted) ~/.local/bin — otherwise a planted binary +# there could execute as root. Appending keeps pipx apps reachable while system paths stay +# authoritative; pipx app names don't collide with system binaries. # ENV covers the exec'd command and non-login shells; the profile.d drop-in re-adds it for # login shells, which otherwise reset PATH via /etc/profile (Debian zsh sources it too). -ENV PATH="/home/appuser/.local/bin:${PATH}" -RUN echo 'case ":${PATH}:" in *:/home/appuser/.local/bin:*) ;; *) PATH="/home/appuser/.local/bin${PATH:+:${PATH}}" ;; esac' \ +ENV PATH="${PATH}:/home/appuser/.local/bin" +RUN echo 'case ":${PATH}:" in *:/home/appuser/.local/bin:*) ;; *) PATH="${PATH:+${PATH}:}/home/appuser/.local/bin" ;; esac' \ > /etc/profile.d/copilot-local-bin.sh # --- snippet: docker-cli --- diff --git a/docker/generated/Dockerfile.java b/docker/generated/Dockerfile.java index d53b431..5a39ca2 100644 --- a/docker/generated/Dockerfile.java +++ b/docker/generated/Dockerfile.java @@ -29,13 +29,18 @@ zsh \ && rm -rf /var/lib/apt/lists/* -# pipx installs apps into the runtime user's ~/.local/bin. The entrypoint always runs as -# appuser with HOME=/home/appuser, so put that dir on PATH up front and pipx-installed CLIs -# (e.g. `pipx install apm`) are reachable without a per-session `pipx ensurepath`. +# pipx installs apps into the runtime user's ~/.local/bin, so that dir needs to be on PATH +# for pipx-installed CLIs (e.g. `pipx install apm`) to be reachable without a per-session +# `pipx ensurepath`. The entrypoint always runs as appuser with HOME=/home/appuser. +# Append (not prepend): the entrypoint runs as root and resolves tools like getent/groupadd/ +# node via PATH before dropping to appuser, so a system binary must always win over anything +# in the user-writable (and possibly bind-mounted) ~/.local/bin — otherwise a planted binary +# there could execute as root. Appending keeps pipx apps reachable while system paths stay +# authoritative; pipx app names don't collide with system binaries. # ENV covers the exec'd command and non-login shells; the profile.d drop-in re-adds it for # login shells, which otherwise reset PATH via /etc/profile (Debian zsh sources it too). -ENV PATH="/home/appuser/.local/bin:${PATH}" -RUN echo 'case ":${PATH}:" in *:/home/appuser/.local/bin:*) ;; *) PATH="/home/appuser/.local/bin${PATH:+:${PATH}}" ;; esac' \ +ENV PATH="${PATH}:/home/appuser/.local/bin" +RUN echo 'case ":${PATH}:" in *:/home/appuser/.local/bin:*) ;; *) PATH="${PATH:+${PATH}:}/home/appuser/.local/bin" ;; esac' \ > /etc/profile.d/copilot-local-bin.sh # --- snippet: docker-cli --- diff --git a/docker/generated/Dockerfile.playwright b/docker/generated/Dockerfile.playwright index fea9ff8..ee8ffc2 100644 --- a/docker/generated/Dockerfile.playwright +++ b/docker/generated/Dockerfile.playwright @@ -29,13 +29,18 @@ RUN apt-get update && apt-get install -y \ zsh \ && rm -rf /var/lib/apt/lists/* -# pipx installs apps into the runtime user's ~/.local/bin. The entrypoint always runs as -# appuser with HOME=/home/appuser, so put that dir on PATH up front and pipx-installed CLIs -# (e.g. `pipx install apm`) are reachable without a per-session `pipx ensurepath`. +# pipx installs apps into the runtime user's ~/.local/bin, so that dir needs to be on PATH +# for pipx-installed CLIs (e.g. `pipx install apm`) to be reachable without a per-session +# `pipx ensurepath`. The entrypoint always runs as appuser with HOME=/home/appuser. +# Append (not prepend): the entrypoint runs as root and resolves tools like getent/groupadd/ +# node via PATH before dropping to appuser, so a system binary must always win over anything +# in the user-writable (and possibly bind-mounted) ~/.local/bin — otherwise a planted binary +# there could execute as root. Appending keeps pipx apps reachable while system paths stay +# authoritative; pipx app names don't collide with system binaries. # ENV covers the exec'd command and non-login shells; the profile.d drop-in re-adds it for # login shells, which otherwise reset PATH via /etc/profile (Debian zsh sources it too). -ENV PATH="/home/appuser/.local/bin:${PATH}" -RUN echo 'case ":${PATH}:" in *:/home/appuser/.local/bin:*) ;; *) PATH="/home/appuser/.local/bin${PATH:+:${PATH}}" ;; esac' \ +ENV PATH="${PATH}:/home/appuser/.local/bin" +RUN echo 'case ":${PATH}:" in *:/home/appuser/.local/bin:*) ;; *) PATH="${PATH:+${PATH}:}/home/appuser/.local/bin" ;; esac' \ > /etc/profile.d/copilot-local-bin.sh # --- snippet: docker-cli --- diff --git a/docker/generated/Dockerfile.rust b/docker/generated/Dockerfile.rust index 2772bfd..8349a89 100644 --- a/docker/generated/Dockerfile.rust +++ b/docker/generated/Dockerfile.rust @@ -29,13 +29,18 @@ RUN apt-get update && apt-get install -y \ zsh \ && rm -rf /var/lib/apt/lists/* -# pipx installs apps into the runtime user's ~/.local/bin. The entrypoint always runs as -# appuser with HOME=/home/appuser, so put that dir on PATH up front and pipx-installed CLIs -# (e.g. `pipx install apm`) are reachable without a per-session `pipx ensurepath`. +# pipx installs apps into the runtime user's ~/.local/bin, so that dir needs to be on PATH +# for pipx-installed CLIs (e.g. `pipx install apm`) to be reachable without a per-session +# `pipx ensurepath`. The entrypoint always runs as appuser with HOME=/home/appuser. +# Append (not prepend): the entrypoint runs as root and resolves tools like getent/groupadd/ +# node via PATH before dropping to appuser, so a system binary must always win over anything +# in the user-writable (and possibly bind-mounted) ~/.local/bin — otherwise a planted binary +# there could execute as root. Appending keeps pipx apps reachable while system paths stay +# authoritative; pipx app names don't collide with system binaries. # ENV covers the exec'd command and non-login shells; the profile.d drop-in re-adds it for # login shells, which otherwise reset PATH via /etc/profile (Debian zsh sources it too). -ENV PATH="/home/appuser/.local/bin:${PATH}" -RUN echo 'case ":${PATH}:" in *:/home/appuser/.local/bin:*) ;; *) PATH="/home/appuser/.local/bin${PATH:+:${PATH}}" ;; esac' \ +ENV PATH="${PATH}:/home/appuser/.local/bin" +RUN echo 'case ":${PATH}:" in *:/home/appuser/.local/bin:*) ;; *) PATH="${PATH:+${PATH}:}/home/appuser/.local/bin" ;; esac' \ > /etc/profile.d/copilot-local-bin.sh # --- snippet: docker-cli --- diff --git a/docker/snippets/system-packages.Dockerfile b/docker/snippets/system-packages.Dockerfile index d5224dd..7856a09 100644 --- a/docker/snippets/system-packages.Dockerfile +++ b/docker/snippets/system-packages.Dockerfile @@ -21,11 +21,16 @@ RUN apt-get update && apt-get install -y \ zsh \ && rm -rf /var/lib/apt/lists/* -# pipx installs apps into the runtime user's ~/.local/bin. The entrypoint always runs as -# appuser with HOME=/home/appuser, so put that dir on PATH up front and pipx-installed CLIs -# (e.g. `pipx install apm`) are reachable without a per-session `pipx ensurepath`. +# pipx installs apps into the runtime user's ~/.local/bin, so that dir needs to be on PATH +# for pipx-installed CLIs (e.g. `pipx install apm`) to be reachable without a per-session +# `pipx ensurepath`. The entrypoint always runs as appuser with HOME=/home/appuser. +# Append (not prepend): the entrypoint runs as root and resolves tools like getent/groupadd/ +# node via PATH before dropping to appuser, so a system binary must always win over anything +# in the user-writable (and possibly bind-mounted) ~/.local/bin — otherwise a planted binary +# there could execute as root. Appending keeps pipx apps reachable while system paths stay +# authoritative; pipx app names don't collide with system binaries. # ENV covers the exec'd command and non-login shells; the profile.d drop-in re-adds it for # login shells, which otherwise reset PATH via /etc/profile (Debian zsh sources it too). -ENV PATH="/home/appuser/.local/bin:${PATH}" -RUN echo 'case ":${PATH}:" in *:/home/appuser/.local/bin:*) ;; *) PATH="/home/appuser/.local/bin${PATH:+:${PATH}}" ;; esac' \ +ENV PATH="${PATH}:/home/appuser/.local/bin" +RUN echo 'case ":${PATH}:" in *:/home/appuser/.local/bin:*) ;; *) PATH="${PATH:+${PATH}:}/home/appuser/.local/bin" ;; esac' \ > /etc/profile.d/copilot-local-bin.sh