Files
TSYSDevStack/ToolboxStack/output/toolbox-base/Dockerfile
ReachableCEO becd640c86 fix: Address Dockerfile issues identified by toolbox-qadocker audit
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.
2025-10-31 14:56:53 -05:00

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"]