diff --git a/.github/workflows/docker-publish.yml b/.github/workflows/docker-publish.yml index 53c6b86..9018438 100644 --- a/.github/workflows/docker-publish.yml +++ b/.github/workflows/docker-publish.yml @@ -6,50 +6,77 @@ on: tags: - v* pull_request: + schedule: + # Weekly rebuild so the image picks up the latest tool versions + # even when nothing has been committed. + - cron: '0 6 * * 1' + workflow_dispatch: env: IMAGE_NAME: polyglot jobs: test: - if: github.ref_name == 'develop' + name: Test build + if: github.event_name == 'pull_request' || github.ref_name == 'develop' runs-on: ubuntu-latest steps: - - uses: actions/checkout@v3 - - name: Run tests - run: | - make build + - uses: actions/checkout@v4 + + - name: Set up Docker Buildx + uses: docker/setup-buildx-action@v3 + + - name: Build image (no push, registry-cache backed) + uses: docker/build-push-action@v5 + with: + context: . + file: ./Dockerfile + platforms: linux/amd64 + push: false + tags: ${{ env.IMAGE_NAME }}:test + cache-from: type=gha + cache-to: type=gha,mode=max publish: - if: github.ref_name == 'main' + name: Publish image + if: github.ref_name == 'main' || startsWith(github.ref, 'refs/tags/v') runs-on: ubuntu-latest + permissions: + contents: read + packages: write steps: - - uses: actions/checkout@v3 + - uses: actions/checkout@v4 - - name: Build image - run: docker build . --file Dockerfile --tag $IMAGE_NAME + - name: Set up Docker Buildx + uses: docker/setup-buildx-action@v3 - - name: Log into GitHub Container Registry - run: echo "${{ secrets.CR_PAT }}" | docker login https://ghcr.io -u ${{ github.actor }} --password-stdin + - name: Log in to GitHub Container Registry + uses: docker/login-action@v3 + with: + registry: ghcr.io + username: ${{ github.actor }} + password: ${{ secrets.CR_PAT }} - - name: Push image to GitHub Container Registry + - name: Compute image tag + id: meta run: | - IMAGE_ID=ghcr.io/${{ github.repository_owner }}/$IMAGE_NAME - - # Change all uppercase to lowercase + IMAGE_ID=ghcr.io/${{ github.repository_owner }}/${{ env.IMAGE_NAME }} IMAGE_ID=$(echo $IMAGE_ID | tr '[A-Z]' '[a-z]') - # Strip git ref prefix from version VERSION=$(echo "${{ github.ref }}" | sed -e 's,.*/\(.*\),\1,') - - # Strip "v" prefix from tag name [[ "${{ github.ref }}" == "refs/tags/"* ]] && VERSION=$(echo $VERSION | sed -e 's/^v//') - - # Use Docker `latest` tag convention [ "$VERSION" == "main" ] && VERSION=latest - echo IMAGE_ID=$IMAGE_ID - echo VERSION=$VERSION + echo "image_id=$IMAGE_ID" >> "$GITHUB_OUTPUT" + echo "version=$VERSION" >> "$GITHUB_OUTPUT" - docker tag $IMAGE_NAME $IMAGE_ID:$VERSION - docker push $IMAGE_ID:$VERSION + - name: Build and push image + uses: docker/build-push-action@v5 + with: + context: . + file: ./Dockerfile + platforms: linux/amd64 + push: true + tags: ${{ steps.meta.outputs.image_id }}:${{ steps.meta.outputs.version }} + cache-from: type=gha + cache-to: type=gha,mode=max diff --git a/Dockerfile b/Dockerfile index 4bf19a6..e742a06 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,105 +1,154 @@ +# syntax=docker/dockerfile:1.7 + # Stage 1: Base system with common dependencies FROM ubuntu:24.04 AS base - -SHELL ["/bin/bash", "-c"] +SHELL ["/bin/bash", "-o", "pipefail", "-c"] ENV TZ='America/New_York' \ DEBIAN_FRONTEND="noninteractive" \ - PATH="/root/.cargo/bin:/root/.nvm/versions/node/v22.16.0/bin:/usr/local/go/bin:${PATH}" \ NVM_DIR="/root/.nvm" \ - NODE_VERSION="lts/jod" - -# Install common packages in a single layer with proper cleanup -RUN apt-get update && \ + PATH="/root/.cargo/bin:/root/.nvm/current/bin:/usr/local/go/bin:${PATH}" + +# Disable apt's automatic post-install cache wipe so BuildKit cache mounts can +# actually retain downloads across builds. +RUN rm -f /etc/apt/apt.conf.d/docker-clean && \ + echo 'Binary::apt::APT::Keep-Downloaded-Packages "true";' \ + > /etc/apt/apt.conf.d/keep-cache + +# Install common packages. /var/cache/apt and /var/lib/apt are mounted as +# BuildKit caches so .deb files and package indexes persist outside the image +# layer; no manual apt-get clean / rm -rf is needed afterwards. +RUN --mount=type=cache,target=/var/cache/apt,sharing=locked \ + --mount=type=cache,target=/var/lib/apt,sharing=locked \ + apt-get update && \ apt-get upgrade -y && \ apt-get install -y --no-install-recommends \ - apt-transport-https \ - apt-utils \ - ca-certificates \ - curl \ - g++ \ - gcc \ - git \ - gpg \ - gpg-agent \ - jq \ - less \ - # libc6-dev \ - make \ - software-properties-common \ - sudo \ - tree \ - unzip \ - zip \ - vim \ - wget \ - # xz-utils \ - zsh && \ - # Clean up in the same layer to reduce image size - apt-get clean && \ - rm -rf /var/lib/apt/lists/* /tmp/* /var/tmp/* - -# Stage 2: Language-specific installations + apt-transport-https \ + apt-utils \ + ca-certificates \ + curl \ + g++ \ + gcc \ + git \ + gpg \ + gpg-agent \ + jq \ + less \ + make \ + software-properties-common \ + sudo \ + tree \ + unzip \ + zip \ + vim \ + wget \ + zsh + +# Stage 2: Language-specific installations. +# Versions resolved dynamically; smaller-footprint variants chosen where safe: +# - Java: openjdk-N-jdk-headless (no Swing/AWT, ~200MB smaller) +# - .NET: SDK only (the SDK already ships with the matching runtime) FROM base AS languages -# Add repositories and install programming languages in a single layer -RUN apt-get update && \ - # Python - apt-get install -y --no-install-recommends python3 python3-dev python3-venv python3-pip && \ +RUN --mount=type=cache,target=/var/cache/apt,sharing=locked \ + --mount=type=cache,target=/var/lib/apt,sharing=locked \ + apt-get update && \ + apt-get install -y --no-install-recommends \ + python3 python3-dev python3-venv python3-pip && \ ln -s /usr/bin/python3 /usr/bin/python && \ - # Java - apt-get install -y --no-install-recommends openjdk-25-jdk && \ - # .NET - apt-get install -y --no-install-recommends dotnet-sdk-8.0 dotnet-runtime-8.0 && \ - # Ruby - apt-get install -y --no-install-recommends ruby-full rbenv && \ - # Clean up - apt-get clean && \ - rm -rf /var/lib/apt/lists/* /tmp/* /var/tmp/* - -# Stage 3: Tool installations (Go, Node, Rust, Docker) + JDK_PKG=$(apt-cache pkgnames openjdk- | grep -E '^openjdk-[0-9]+-jdk-headless$' | sort -V | tail -1) && \ + echo "Installing Java (headless): $JDK_PKG" && \ + apt-get install -y --no-install-recommends "$JDK_PKG" && \ + DOTNET_VER=$(apt-cache pkgnames dotnet-sdk- | grep -E '^dotnet-sdk-[0-9]+\.[0-9]+$' | sed 's/^dotnet-sdk-//' | sort -V | tail -1) && \ + echo "Installing .NET SDK: $DOTNET_VER" && \ + apt-get install -y --no-install-recommends "dotnet-sdk-$DOTNET_VER" && \ + apt-get install -y --no-install-recommends ruby-full rbenv + +# Stage 3: Tool installations (Go, Node, Rust, Docker CLI) FROM languages AS tools -# Install Go -RUN curl -OL https://go.dev/dl/go1.25.6.linux-amd64.tar.gz && \ - tar -C /usr/local -xf go1.25.6.linux-amd64.tar.gz && \ - rm go1.25.6.linux-amd64.tar.gz && \ +# Install Go (auto-detect the latest stable version from go.dev), then strip +# the bundled docs/test/api directories which are not needed at runtime +# (~100MB smaller). +RUN GO_VERSION=$(curl -fsSL https://go.dev/VERSION?m=text | head -n1 | sed 's/^go//') && \ + GO_TARBALL="go${GO_VERSION}.linux-amd64.tar.gz" && \ + echo "Installing Go: ${GO_VERSION}" && \ + curl -OL "https://go.dev/dl/${GO_TARBALL}" && \ + tar -C /usr/local -xf "${GO_TARBALL}" && \ + rm "${GO_TARBALL}" && \ + rm -rf /usr/local/go/doc /usr/local/go/test /usr/local/go/api && \ ln -sf /usr/local/go/bin/go /usr/bin/go -# Install Node.js with nvm -RUN curl -o- https://raw.githubusercontent.com/nvm-sh/nvm/v0.40.3/install.sh | bash && \ +# Install Node.js with nvm (latest LTS) and clear nvm's tarball cache after. +RUN NVM_VERSION=$(curl -fsSL https://api.github.com/repos/nvm-sh/nvm/releases/latest | jq -r '.tag_name') && \ + echo "Installing nvm: ${NVM_VERSION}" && \ + curl -o- "https://raw.githubusercontent.com/nvm-sh/nvm/${NVM_VERSION}/install.sh" | bash && \ . $NVM_DIR/nvm.sh && \ - nvm install $NODE_VERSION && \ - nvm alias default $NODE_VERSION && \ + nvm install --lts --no-progress && \ + nvm alias default 'lts/*' && \ nvm use default && \ - ln -sf $(. $NVM_DIR/nvm.sh && which node) /usr/bin/node && \ - ln -sf $(. $NVM_DIR/nvm.sh && which npm) /usr/bin/npm - -# Install Rust -RUN curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | bash -s -- -y - -# Install Docker -RUN curl -fsSL https://get.docker.com | sh + NODE_VER=$(nvm version default) && \ + ln -sfn "$NVM_DIR/versions/node/$NODE_VER" "$NVM_DIR/current" && \ + ln -sf "$NVM_DIR/current/bin/node" /usr/bin/node && \ + ln -sf "$NVM_DIR/current/bin/npm" /usr/bin/npm && \ + nvm cache clear + +# Install Rust with the minimal profile (drops rust-docs, ~700MB smaller), +# then add clippy and rustfmt which most dev workflows expect. +RUN curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | \ + bash -s -- -y --profile minimal --default-toolchain stable --no-modify-path && \ + . /root/.cargo/env && \ + rustup component add clippy rustfmt + +# Install Docker CLI + buildx + compose plugins from Docker's official apt +# repo. The daemon (docker-ce) and containerd are intentionally omitted; the +# typical use is to mount the host docker socket into the container, and +# skipping them saves ~600MB. +RUN --mount=type=cache,target=/var/cache/apt,sharing=locked \ + --mount=type=cache,target=/var/lib/apt,sharing=locked \ + install -m 0755 -d /etc/apt/keyrings && \ + curl -fsSL https://download.docker.com/linux/ubuntu/gpg \ + -o /etc/apt/keyrings/docker.asc && \ + chmod a+r /etc/apt/keyrings/docker.asc && \ + UBUNTU_CODENAME=$(. /etc/os-release && echo "$VERSION_CODENAME") && \ + echo "deb [arch=$(dpkg --print-architecture) signed-by=/etc/apt/keyrings/docker.asc] https://download.docker.com/linux/ubuntu $UBUNTU_CODENAME stable" \ + > /etc/apt/sources.list.d/docker.list && \ + apt-get update && \ + apt-get install -y --no-install-recommends \ + docker-ce-cli \ + docker-buildx-plugin \ + docker-compose-plugin # Stage 4: Python packages and configuration FROM tools AS python-config -RUN python -m pip install --no-cache-dir pipx poetry pipenv --break-system-packages +RUN --mount=type=cache,target=/root/.cache/pip \ + python -m pip install pipx poetry pipenv --break-system-packages + +# Stage 5: Agentic AI coding tools. +# Each installer is the upstream "latest" curl|bash script, so the image always +# picks up the newest release on rebuild. The tools drop binaries under +# /root/.local/bin or /root/.opencode/bin; we mirror those into ENV PATH so +# the binaries are usable from any shell. +FROM python-config AS ai-tools + +ENV PATH="/root/.local/bin:/root/.opencode/bin:${PATH}" -# Stage 5: Final image with configurations -FROM python-config AS final +RUN curl -fsSL https://claude.ai/install.sh | bash && \ + curl -fsSL https://antigravity.google/cli/install.sh | bash && \ + curl -fsSL https://opencode.ai/install | bash -# Git Configuration +# Stage 6: Final image with shell configurations +FROM ai-tools AS final + +# Git config + Oh My Zsh in a single layer. RUN git config --global pull.rebase true && \ git config --global init.defaultbranch main && \ git config --global fetch.prune true && \ - git config --global diff.colorMoved zebra - -# Zshell Configuration -RUN sh -c "$(curl -fsSL https://raw.githubusercontent.com/ohmyzsh/ohmyzsh/master/tools/install.sh)" "" --unattended && \ - chsh -s $(which zsh) + git config --global diff.colorMoved zebra && \ + sh -c "$(curl -fsSL https://raw.githubusercontent.com/ohmyzsh/ohmyzsh/master/tools/install.sh)" "" --unattended && \ + chsh -s "$(which zsh)" -# Copy theme file COPY polyglot.zsh-theme /root/.oh-my-zsh/themes/polyglot.zsh-theme RUN sed -i 's/ZSH_THEME="robbyrussell"/ZSH_THEME="polyglot"/' /root/.zshrc @@ -120,7 +169,10 @@ RUN echo "Tool versions:" && \ ruby --version && \ gem --version && \ rbenv --version && \ - docker --version + docker --version && \ + echo "Agentic AI tools:" && \ + command -v claude && \ + command -v opencode && \ + command -v agy -# Set default shell to zsh CMD ["zsh"] diff --git a/README.md b/README.md index af61742..857bd5f 100644 --- a/README.md +++ b/README.md @@ -10,24 +10,73 @@ Multi programming language container image built for interactive development env - [GitLab](https://about.gitlab.com) - etc. -_NOTE:_ This image is **large**, around 5GB in total. This makes it far too big for most large scale uses. But in small scale it seems to work quite well despite its size. +_NOTE:_ This image is **large**, around 3.5GB in total. This makes it far too big for most large scale uses. But in small scale it seems to work quite well despite its size. + +The `Dockerfile` is built with several size and speed optimizations: + +- **BuildKit cache mounts** on `/var/cache/apt`, `/var/lib/apt`, and `/root/.cache/pip` so repeated builds skip re-downloading packages and pip wheels (cached rebuilds with no Dockerfile changes take ~1–2 seconds). +- **Rust** is installed with `--profile minimal` and only `clippy` + `rustfmt` are added back, dropping the ~700MB `rust-docs` component. +- **Docker** is the CLI + `buildx` + `compose` plugins from Docker's official apt repo only — the daemon and `containerd` are intentionally omitted (~600MB savings); typical use is to mount the host's docker socket into the container. +- **Java** uses the highest `openjdk-N-jdk-headless` available (no Swing/AWT, ~200MB savings). +- **.NET** installs the SDK only — the SDK already ships with the matching runtime, so the runtime package is redundant. +- **Go's** bundled `doc/`, `test/`, and `api/` directories are stripped after install (~100MB savings). ## Base Image -This image is based on Ubuntu (latest LTS) +This image is based on **Ubuntu 24.04 LTS** (Noble Numbat) — the only version that is +explicitly pinned in the `Dockerfile`. All other tools are resolved dynamically on +every build (see below). ## Included Languages & Tools +> **No tool versions are pinned in the `Dockerfile`.** Every `make build` resolves and +> installs the latest available release of each language and tool from upstream, so +> a freshly built image is always current. The `Docker` GitHub Actions workflow also +> runs on a weekly schedule to republish the image with the newest versions even when +> nothing has been committed. + +How "latest" is determined for each tool: + +| Tool | How the latest version is resolved at build time | +| -------------------------- | ----------------------------------------------------------------- | +| Go | Queries `https://go.dev/VERSION?m=text` for the current stable | +| nvm | Queries the GitHub releases API for `nvm-sh/nvm` | +| Node.js | `nvm install --lts` (current LTS line) | +| Java | Highest `openjdk-N-jdk-headless` available in the Ubuntu apt repos | +| .NET | Highest `dotnet-sdk-X.Y` available in the Ubuntu apt repos (SDK only; bundles its own runtime) | +| Python | `python3` from the Ubuntu apt repos | +| Ruby | `ruby-full` from the Ubuntu apt repos | +| Rust | `rustup` (`--profile minimal` + `clippy` + `rustfmt`) | +| Docker | `docker-ce-cli`, `docker-buildx-plugin`, `docker-compose-plugin` from Docker's official apt repo | +| poetry / pipenv / pipx | Latest from PyPI via `pip` | +| Claude Code (`claude`) | Anthropic's official installer at `https://claude.ai/install.sh` | +| Antigravity CLI (`agy`) | Google's official installer at `https://antigravity.google/cli/install.sh` | +| Opencode (`opencode`) | Official installer at `https://opencode.ai/install` | + +The table below is **auto-regenerated by `make build`** and reflects the versions +that were actually resolved during the most recent build of this image. + | Language Ecosystem | Version | Included Tools | | ------------------ | ------- | -------------------- | -| Node | 22.16.0 | nvm, npm | +| Node | 24.16.0 | nvm, npm | | Python | 3.12.3 | poetry, pipenv, pipx | -| Java | 21.0.7 | | -| Dotnet | 8.0.117 | | -| GO | 1.24.4 | | +| Java | 25.0.2 | | +| Dotnet | 10.0.107 | | +| GO | 1.26.3 | | | Ruby | 3.2.3 | rbenv, gem | -| Rust | 1.87.0 | | -| Docker | 28.2.2 | | +| Rust | 1.95.0 | | +| Docker | 29.5.2 | | + +### Agentic AI coding tools + +The image also bundles several agentic AI coding CLIs. Each is installed from +its official upstream installer script, so every build pulls the newest release. + +| Tool | Invoke as | Installer | +| ---------------- | ---------- | -------------------------------------------------- | +| Claude Code | `claude` | `curl -fsSL https://claude.ai/install.sh \| bash` | +| Antigravity CLI | `agy` | `curl -fsSL https://antigravity.google/cli/install.sh \| bash` | +| Opencode | `opencode` | `curl -fsSL https://opencode.ai/install \| bash` | # Build Instructions @@ -38,7 +87,8 @@ This image is based on Ubuntu (latest LTS) ## Build -Builds the image. +Builds the image. Each invocation pulls the **latest available version** of every +tool listed above and then refreshes the version table in this `README.md` to match. ```bash make build