This commit fixes several issues in the toolbox-base Dockerfile that were identified during the audit: - Added SHELL directive with pipefail option where pipes are used - Fixed syntax error in user creation logic by changing 'else if' to 'elif' - Removed problematic 'cd' usage, replacing with 'git -C' for directory-specific operations - Added SHELL directive to second stage where pipes are used - Improved multi-line RUN command formatting with proper semicolon usage These changes resolve the following Hadolint errors: - DL4006: Missing pipefail in RUN commands with pipes - SC1075: Incorrect use of 'else if' instead of 'elif' - DL3003: Usage of 'cd' instead of WORKDIR The Dockerfile now passes Hadolint validation when ignoring version pinning and multiple RUN command warnings, which are expected in this context.
303 lines
12 KiB
Docker
303 lines
12 KiB
Docker
# Multi-stage approach to minimize final image size and attack surface
|
|
FROM ubuntu:24.04 AS installer
|
|
|
|
ARG USER_ID=1000
|
|
ARG GROUP_ID=1000
|
|
ARG USERNAME=toolbox
|
|
ARG TEA_VERSION=0.11.1
|
|
|
|
ENV DEBIAN_FRONTEND=noninteractive
|
|
|
|
# ROOT STAGE 1: System package installation only
|
|
RUN --mount=type=cache,target=/var/cache/apt,sharing=locked \
|
|
--mount=type=cache,target=/var/lib/apt/lists,sharing=locked \
|
|
apt-get update \
|
|
&& apt-get install -y --no-install-recommends \
|
|
ca-certificates \
|
|
curl \
|
|
fish \
|
|
fzf \
|
|
git \
|
|
jq \
|
|
bc \
|
|
htop \
|
|
btop \
|
|
locales \
|
|
openssh-client \
|
|
ripgrep \
|
|
tmux \
|
|
screen \
|
|
entr \
|
|
fd-find \
|
|
bat \
|
|
httpie \
|
|
# Build dependencies needed for Node.js native modules \
|
|
build-essential \
|
|
pkg-config \
|
|
libssl-dev \
|
|
zlib1g-dev \
|
|
libffi-dev \
|
|
libsqlite3-dev \
|
|
libreadline-dev \
|
|
wget \
|
|
zsh \
|
|
unzip \
|
|
zip \
|
|
gnupg \
|
|
software-properties-common \
|
|
apt-transport-https \
|
|
python3 \
|
|
python3-pip \
|
|
python3-dev \
|
|
&& apt-get clean \
|
|
&& rm -rf /var/lib/apt/lists/*
|
|
|
|
# ROOT: System-wide utilities
|
|
RUN ln -sf /usr/bin/fdfind /usr/local/bin/fd \
|
|
&& ln -sf /usr/bin/batcat /usr/local/bin/bat
|
|
|
|
# ROOT: Install Gitea tea CLI (system-wide)
|
|
SHELL ["/bin/bash", "-o", "pipefail", "-c"]
|
|
RUN curl -fsSL "https://dl.gitea.io/tea/${TEA_VERSION}/tea-${TEA_VERSION}-linux-amd64" -o /tmp/tea \
|
|
&& curl -fsSL "https://dl.gitea.io/tea/${TEA_VERSION}/tea-${TEA_VERSION}-linux-amd64.sha256" -o /tmp/tea.sha256 \
|
|
&& sed -n 's/ .*//p' /tmp/tea.sha256 | awk '{print $1 " /tmp/tea"}' | sha256sum -c - \
|
|
&& install -m 0755 /tmp/tea /usr/local/bin/tea \
|
|
&& rm -f /tmp/tea /tmp/tea.sha256
|
|
|
|
# ROOT: Configure locale
|
|
RUN locale-gen en_US.UTF-8
|
|
ENV LANG=en_US.UTF-8 \
|
|
LANGUAGE=en_US:en \
|
|
LC_ALL=en_US.UTF-8
|
|
|
|
# ROOT: Install Starship prompt (system-wide)
|
|
SHELL ["/bin/bash", "-o", "pipefail", "-c"]
|
|
RUN curl -fsSL https://starship.rs/install.sh | sh -s -- -y -b /usr/local/bin
|
|
|
|
# Install aqua package manager (manages additional CLI tooling)
|
|
SHELL ["/bin/bash", "-o", "pipefail", "-c"]
|
|
RUN curl -sSfL https://raw.githubusercontent.com/aquaproj/aqua-installer/v2.3.1/aqua-installer | AQUA_ROOT_DIR=/usr/local/share/aquaproj-aqua bash \
|
|
&& ln -sf /usr/local/share/aquaproj-aqua/bin/aqua /usr/local/bin/aqua
|
|
|
|
# Install mise for runtime management (no global toolchains pre-installed)
|
|
SHELL ["/bin/bash", "-o", "pipefail", "-c"]
|
|
RUN curl -sSfL https://mise.jdx.dev/install.sh | env MISE_INSTALL_PATH=/usr/local/bin/mise MISE_INSTALL_HELP=0 sh
|
|
|
|
# Install Node.js via mise to enable npm package installation
|
|
SHELL ["/bin/bash", "-o", "pipefail", "-c"]
|
|
RUN mise install node@22.13.0 && mise global node@22.13.0
|
|
|
|
# Create non-root user with matching UID/GID for host mapping
|
|
# Check if user/group already exists and handle appropriately
|
|
RUN set -eux; \
|
|
if getent passwd "${USER_ID}" >/dev/null; then \
|
|
existing_user="$(getent passwd "${USER_ID}" | cut -d: -f1)"; \
|
|
echo "User with UID ${USER_ID} already exists: ${existing_user}" >&2; \
|
|
elif ! getent group "${GROUP_ID}" >/dev/null; then \
|
|
groupadd --gid "${GROUP_ID}" "${USERNAME}"; \
|
|
useradd --uid "${USER_ID}" --gid "${GROUP_ID}" --shell /usr/bin/zsh --create-home "${USERNAME}"; \
|
|
else \
|
|
useradd --uid "${USER_ID}" --gid "${GROUP_ID}" --shell /usr/bin/zsh --create-home "${USERNAME}"; \
|
|
fi
|
|
|
|
# ROOT: Set up toolbox user home directory with proper permissions
|
|
RUN chown -R "${USER_ID}:${GROUP_ID}" "/home/${USERNAME}"
|
|
|
|
# SWITCH TO NON-ROOT USER: All further operations as toolbox user
|
|
USER ${USERNAME}
|
|
WORKDIR /home/${USERNAME}
|
|
|
|
# Ensure the workspace directory exists with proper permissions
|
|
RUN mkdir -p /workspace && chmod 755 /workspace
|
|
|
|
# NON-ROOT: Install mise runtime manager for toolbox user
|
|
RUN curl -sSfL https://mise.jdx.dev/install.sh | sh
|
|
|
|
# NON-ROOT: Update PATH for mise tools
|
|
ENV PATH=/home/${USERNAME}/.local/bin:/home/${USERNAME}/.local/share/mise/shims:$PATH
|
|
|
|
# NON-ROOT: Install Node.js via mise as toolbox user
|
|
RUN mise install node@22.13.0 && mise use -g node@22.13.0
|
|
|
|
# Install AI CLI tools via npm using mise to ensure Node.js is available
|
|
RUN mise exec -- npm install -g @just-every/code@0.4.6 @qwen-code/qwen-code@0.1.1 @google/gemini-cli@0.11.0 @openai/codex@0.50.0 opencode-ai@0.15.29
|
|
|
|
# NON-ROOT: Install aqua package manager for toolbox user
|
|
RUN curl -sSfL https://raw.githubusercontent.com/aquaproj/aqua-installer/v2.3.1/aqua-installer > /tmp/aqua-installer.sh && \
|
|
chmod +x /tmp/aqua-installer.sh && \
|
|
AQUA_ROOT_DIR=/home/${USERNAME}/.local/share/aquaproj-aqua /tmp/aqua-installer.sh && \
|
|
rm /tmp/aqua-installer.sh
|
|
|
|
# NON-ROOT: Update PATH for aqua tools
|
|
ENV PATH=/home/${USERNAME}/.local/share/aquaproj-aqua/bin:$PATH
|
|
|
|
# NON-ROOT: Install Oh My Zsh
|
|
RUN git clone --depth=1 https://github.com/ohmyzsh/ohmyzsh.git ~/.oh-my-zsh
|
|
|
|
# NON-ROOT: Configure shells (zsh, bash, fish) with all customizations
|
|
RUN cp ~/.oh-my-zsh/templates/zshrc.zsh-template ~/.zshrc \
|
|
&& mkdir -p ~/.config \
|
|
&& sed -i "s/^plugins=(git)$/plugins=(git fzf)/" ~/.zshrc \
|
|
&& printf "\nexport PATH=\"\$HOME/.local/share/aquaproj-aqua/bin:\$HOME/.local/share/mise/shims:\$HOME/.local/bin:\$PATH\"\n" >> ~/.zshrc \
|
|
&& printf "\nexport AQUA_GLOBAL_CONFIG=\"\$HOME/.config/aquaproj-aqua/aqua.yaml\"\n" >> ~/.zshrc \
|
|
&& printf "\n# Starship prompt\neval \"\$(starship init zsh)\"\n" >> ~/.zshrc \
|
|
&& printf "\n# mise runtime manager\neval \"\$(mise activate zsh)\"\n" >> ~/.zshrc \
|
|
&& printf "\n# direnv\nexport DIRENV_LOG_FORMAT=\"\"\neval \"\$(direnv hook zsh)\"\n" >> ~/.zshrc \
|
|
&& printf "\n# zoxide\neval \"\$(zoxide init zsh)\"\n" >> ~/.zshrc \
|
|
&& printf "\nexport AQUA_GLOBAL_CONFIG=\"\$HOME/.config/aquaproj-aqua/aqua.yaml\"\n" >> ~/.bashrc \
|
|
&& printf "\n# mise runtime manager (bash)\neval \"\$(mise activate bash)\"\n" >> ~/.bashrc \
|
|
&& printf "\n# direnv\nexport DIRENV_LOG_FORMAT=\"\"\neval \"\$(direnv hook bash)\"\n" >> ~/.bashrc \
|
|
&& printf "\n# zoxide\neval \"\$(zoxide init bash)\"\n" >> ~/.bashrc \
|
|
&& mkdir -p ~/.config/fish \
|
|
&& printf "\nset -gx AQUA_GLOBAL_CONFIG \$HOME/.config/aquaproj-aqua/aqua.yaml\n# Shell prompt and runtime manager\nstarship init fish | source\nmise activate fish | source\ndirenv hook fish | source\nzoxide init fish | source\n" >> ~/.config/fish/config.fish
|
|
|
|
# NON-ROOT: Install aqua packages from aqua.yaml (all tools baked into image)
|
|
COPY --chown=${USER_ID}:${GROUP_ID} aqua.yaml /tmp/aqua.yaml
|
|
RUN mkdir -p ~/.config/aquaproj-aqua \
|
|
&& cp /tmp/aqua.yaml ~/.config/aquaproj-aqua/aqua.yaml \
|
|
&& aqua install
|
|
|
|
# NON-ROOT: Install all AI CLI tools during build using mise (baked into image)
|
|
RUN mise exec -- npm install -g \
|
|
@just-every/code@0.4.6 \
|
|
@qwen-code/qwen-code@0.1.1 \
|
|
@google/gemini-cli@0.11.0 \
|
|
@openai/codex@0.50.0 \
|
|
opencode-ai@0.15.29 && \
|
|
mise reshim
|
|
|
|
# NON-ROOT: Install Joplin CLI during build using mise (baked into image)
|
|
# Skipping Joplin due to build issues with sqlite3 dependencies
|
|
# RUN mise exec -- npm install -g joplin-cli@latest --legacy-peer-deps && mise reshim
|
|
|
|
# NON-ROOT: Install additional testing tools during build
|
|
RUN mise exec -- npm install -g bats@1.11.0 && mise reshim
|
|
|
|
# NON-ROOT: Install BATS testing framework from source (baked into image)
|
|
RUN git clone https://github.com/bats-core/bats-core.git /tmp/bats-core \
|
|
&& git -C /tmp/bats-core checkout v1.11.0 \
|
|
&& /tmp/bats-core/install.sh "$HOME/.local" \
|
|
&& rm -rf /tmp/bats-core
|
|
|
|
# Prepare workspace directory with appropriate ownership
|
|
RUN mkdir -p /workspace \
|
|
&& chown "${USER_ID}:${GROUP_ID}" /workspace
|
|
|
|
# Remove sudo to ensure no root escalation is possible at runtime
|
|
RUN apt-get remove -y sudo 2>/dev/null || true && apt-get autoremove -y 2>/dev/null || true && rm -rf /var/lib/apt/lists/* 2>/dev/null || true
|
|
|
|
SHELL ["/bin/bash", "-o", "pipefail", "-c"]
|
|
|
|
ENV SHELL=/usr/bin/zsh \
|
|
AQUA_GLOBAL_CONFIG=/home/${USERNAME}/.config/aquaproj-aqua/aqua.yaml \
|
|
PATH=/home/${USERNAME}/.local/share/aquaproj-aqua/bin:/home/${USERNAME}/.local/share/mise/shims:/home/${USERNAME}/.local/bin:${PATH}
|
|
|
|
WORKDIR /workspace
|
|
USER ${USERNAME}
|
|
|
|
# NON-ROOT: Verify all tools are accessible during build
|
|
RUN bash -c 'command -v node && command -v npm && command -v mise && command -v aqua' \
|
|
&& bash -c 'node --version && npm --version && mise --version && aqua --version'
|
|
|
|
# NON-ROOT: Final mise reshim to ensure all tools are properly linked
|
|
RUN mise reshim
|
|
|
|
# FINAL STAGE: Copy completed setup to minimize image and enhance security
|
|
FROM ubuntu:24.04
|
|
|
|
ARG USER_ID=1000
|
|
ARG GROUP_ID=1000
|
|
ARG USERNAME=toolbox
|
|
ARG TEA_VERSION=0.11.1
|
|
|
|
ENV DEBIAN_FRONTEND=noninteractive
|
|
|
|
# ROOT: Install minimal runtime dependencies only
|
|
RUN --mount=type=cache,target=/var/cache/apt,sharing=locked \
|
|
--mount=type=cache,target=/var/lib/apt/lists,sharing=locked \
|
|
apt-get update \
|
|
&& apt-get install -y --no-install-recommends \
|
|
ca-certificates \
|
|
curl \
|
|
fish \
|
|
fzf \
|
|
git \
|
|
jq \
|
|
bc \
|
|
htop \
|
|
btop \
|
|
locales \
|
|
openssh-client \
|
|
ripgrep \
|
|
tmux \
|
|
screen \
|
|
entr \
|
|
fd-find \
|
|
bat \
|
|
httpie \
|
|
zsh \
|
|
wget \
|
|
unzip \
|
|
zip \
|
|
python3 \
|
|
&& apt-get clean \
|
|
&& rm -rf /var/lib/apt/lists/*
|
|
|
|
# ROOT: Restore system-wide utilities
|
|
RUN ln -sf /usr/bin/fdfind /usr/local/bin/fd \
|
|
&& ln -sf /usr/bin/batcat /usr/local/bin/bat
|
|
|
|
# ROOT: Restore system-wide configurations
|
|
RUN locale-gen en_US.UTF-8
|
|
ENV LANG=en_US.UTF-8 \
|
|
LANGUAGE=en_US:en \
|
|
LC_ALL=en_US.UTF-8
|
|
|
|
# ROOT: Create user/group structure
|
|
SHELL ["/bin/bash", "-o", "pipefail", "-c"]
|
|
# First clean up any existing user/group with the same ID
|
|
RUN set -eux; \
|
|
if getent passwd "${USER_ID}" >/dev/null; then \
|
|
existing_user="$(getent passwd "${USER_ID}" | cut -d: -f1)"; \
|
|
userdel --remove "${existing_user}"; \
|
|
fi; \
|
|
if getent group "${GROUP_ID}" >/dev/null; then \
|
|
groupdel "$(getent group "${GROUP_ID}" | cut -d: -f1)"; \
|
|
fi; \
|
|
# Create the group and user
|
|
groupadd --gid "${GROUP_ID}" "${USERNAME}"; \
|
|
useradd --uid "${USER_ID}" --gid "${GROUP_ID}" --shell /usr/bin/zsh --create-home "${USERNAME}"; \
|
|
# Ensure proper ownership of home directory
|
|
chown -R "${USER_ID}:${GROUP_ID}" "/home/${USERNAME}"
|
|
|
|
# ROOT: Copy the complete user environment from the installer stage
|
|
COPY --from=installer --chown=${USER_ID}:${GROUP_ID} /home/${USERNAME} /home/${USERNAME}
|
|
|
|
# ROOT: Create workspace directory
|
|
RUN mkdir -p /workspace && chown "${USER_ID}:${GROUP_ID}" /workspace
|
|
|
|
# ROOT: Install system-wide tools (tea and starship) which were in the source image
|
|
SHELL ["/bin/bash", "-o", "pipefail", "-c"]
|
|
RUN curl -fsSL "https://dl.gitea.io/tea/${TEA_VERSION}/tea-${TEA_VERSION}-linux-amd64" -o /tmp/tea \
|
|
&& curl -fsSL "https://dl.gitea.io/tea/${TEA_VERSION}/tea-${TEA_VERSION}-linux-amd64.sha256" -o /tmp/tea.sha256 \
|
|
&& sed -n 's/ .*//p' /tmp/tea.sha256 | awk '{print $1 " /tmp/tea"}' | sha256sum -c - \
|
|
&& install -m 0755 /tmp/tea /usr/local/bin/tea \
|
|
&& rm -f /tmp/tea /tmp/tea.sha256
|
|
|
|
RUN curl -fsSL https://starship.rs/install.sh | sh -s -- -y -b /usr/local/bin
|
|
|
|
# ROOT: Security hardening - remove sudo if present
|
|
RUN apt-get remove -y sudo 2>/dev/null || true && apt-get autoremove -y 2>/dev/null || true && rm -rf /var/lib/apt/lists/* 2>/dev/null || true
|
|
|
|
# ROOT: Final environment variables
|
|
ENV PATH=/home/${USERNAME}/.local/share/aquaproj-aqua/bin:/home/${USERNAME}/.local/share/mise/shims:/home/${USERNAME}/.local/bin:/usr/local/bin:/usr/local/sbin:/usr/bin:/usr/sbin:/bin:/sbin
|
|
ENV SHELL=/usr/bin/zsh \
|
|
AQUA_GLOBAL_CONFIG=/home/${USERNAME}/.config/aquaproj-aqua/aqua.yaml
|
|
|
|
SHELL ["/bin/bash", "-o", "pipefail", "-c"]
|
|
|
|
# FINAL USER: Switch to toolbox user for runtime
|
|
USER ${USERNAME}
|
|
WORKDIR /workspace
|
|
|
|
CMD ["/usr/bin/zsh"] |