Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
75 changes: 51 additions & 24 deletions .github/workflows/docker-publish.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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
206 changes: 129 additions & 77 deletions Dockerfile
Original file line number Diff line number Diff line change
@@ -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

Expand All @@ -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"]
Loading
Loading