FROM ubuntu:24.04 # Avoid interactive prompts during package install ENV DEBIAN_FRONTEND=noninteractive # ── System packages ────────────────────────────────────────────────────────── RUN apt-get update && apt-get install -y --no-install-recommends \ git \ curl \ wget \ openssh-client \ build-essential \ ripgrep \ jq \ sudo \ ca-certificates \ gnupg \ locales \ unzip \ pkg-config \ libssl-dev \ cron \ && rm -rf /var/lib/apt/lists/* # Remove default ubuntu user to free UID 1000 for host-user remapping RUN if id ubuntu >/dev/null 2>&1; then userdel -r ubuntu 2>/dev/null || userdel ubuntu; fi \ && if getent group ubuntu >/dev/null 2>&1; then groupdel ubuntu 2>/dev/null || true; fi # Set UTF-8 locale RUN locale-gen en_US.UTF-8 ENV LANG=en_US.UTF-8 ENV LC_ALL=en_US.UTF-8 # ── GitHub CLI ─────────────────────────────────────────────────────────────── RUN curl -fsSL https://cli.github.com/packages/githubcli-archive-keyring.gpg \ | dd of=/usr/share/keyrings/githubcli-archive-keyring.gpg \ && chmod go+r /usr/share/keyrings/githubcli-archive-keyring.gpg \ && echo "deb [arch=$(dpkg --print-architecture) signed-by=/usr/share/keyrings/githubcli-archive-keyring.gpg] https://cli.github.com/packages stable main" \ > /etc/apt/sources.list.d/github-cli.list \ && apt-get update && apt-get install -y gh \ && rm -rf /var/lib/apt/lists/* # ── Node.js LTS (22.x) + pnpm ─────────────────────────────────────────────── RUN curl -fsSL https://deb.nodesource.com/setup_22.x | bash - \ && apt-get install -y nodejs \ && rm -rf /var/lib/apt/lists/* \ && npm install -g pnpm # ── Python 3 + pip + uv + ruff ────────────────────────────────────────────── RUN apt-get update && apt-get install -y --no-install-recommends \ python3 \ python3-pip \ python3-venv \ && rm -rf /var/lib/apt/lists/* # ── Docker CLI (not daemon) ───────────────────────────────────────────────── RUN install -m 0755 -d /etc/apt/keyrings \ && curl -fsSL https://download.docker.com/linux/ubuntu/gpg \ | gpg --dearmor -o /etc/apt/keyrings/docker.gpg \ && chmod a+r /etc/apt/keyrings/docker.gpg \ && echo "deb [arch=$(dpkg --print-architecture) signed-by=/etc/apt/keyrings/docker.gpg] https://download.docker.com/linux/ubuntu $(. /etc/os-release && echo "$VERSION_CODENAME") stable" \ > /etc/apt/sources.list.d/docker.list \ && apt-get update && apt-get install -y docker-ce-cli \ && rm -rf /var/lib/apt/lists/* # ── AWS CLI v2 ─────────────────────────────────────────────────────────────── RUN ARCH=$(uname -m) && \ curl "https://awscli.amazonaws.com/awscli-exe-linux-${ARCH}.zip" -o "awscliv2.zip" && \ unzip -q awscliv2.zip && \ ./aws/install && \ rm -rf awscliv2.zip aws # ── Non-root user with passwordless sudo ───────────────────────────────────── RUN useradd -m -s /bin/bash -u 1000 claude \ && echo "claude ALL=(ALL) NOPASSWD:ALL" > /etc/sudoers.d/claude \ && chmod 0440 /etc/sudoers.d/claude # ── Mount points (created as root, owned by claude) ────────────────────────── RUN mkdir -p /workspace && chown claude:claude /workspace # ── Rust (installed as claude user) ────────────────────────────────────────── USER claude WORKDIR /home/claude RUN curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh -s -- -y ENV PATH="/home/claude/.cargo/bin:${PATH}" # Install uv and ruff for claude user RUN curl -LsSf https://astral.sh/uv/install.sh | sh \ && curl -LsSf https://astral.sh/ruff/install.sh | sh ENV PATH="/home/claude/.local/bin:/home/claude/.cargo/bin:${PATH}" # ── Claude Code ────────────────────────────────────────────────────────────── RUN curl -fsSL https://claude.ai/install.sh | bash ENV PATH="/home/claude/.claude/bin:${PATH}" RUN mkdir -p /home/claude/.claude /home/claude/.ssh WORKDIR /workspace # ── Switch back to root for entrypoint (handles UID/GID remapping) ───────── USER root COPY entrypoint.sh /usr/local/bin/entrypoint.sh RUN chmod +x /usr/local/bin/entrypoint.sh COPY triple-c-scheduler /usr/local/bin/triple-c-scheduler RUN chmod +x /usr/local/bin/triple-c-scheduler COPY triple-c-task-runner /usr/local/bin/triple-c-task-runner RUN chmod +x /usr/local/bin/triple-c-task-runner ENTRYPOINT ["/usr/local/bin/entrypoint.sh"]