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
14 changes: 0 additions & 14 deletions .bashrc

This file was deleted.

12 changes: 8 additions & 4 deletions .dockerignore
Original file line number Diff line number Diff line change
@@ -1,5 +1,9 @@
# Exclude everything
# Only the build needs sources + Gradle metadata; ignore everything else.
*
!entrypoint.sh
!.bashrc
!tmux.conf

!gradlew
!settings.gradle.kts
!build.gradle.kts
!gradle.properties
!gradle
!src
25 changes: 0 additions & 25 deletions .env.example

This file was deleted.

44 changes: 29 additions & 15 deletions .github/workflows/build.yml
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
name: "Build and Push Multi-JDK Docker Image"
name: "Build"

on:
push:
Expand All @@ -11,51 +11,65 @@ env:
REGISTRY: ghcr.io

jobs:
build:
gradle:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v6
- uses: actions/setup-java@v5
with:
distribution: liberica
java-version: "25"
- name: "Build"
run: ./gradlew --no-daemon build

docker:
needs: gradle
runs-on: ubuntu-latest
permissions:
contents: read
packages: write

steps:
- uses: actions/checkout@v6
- name: "Set lowercase image name"
run: echo "IMAGE_NAME=$(echo ${{ github.repository }} | tr '[:upper:]' '[:lower:]')" >> $GITHUB_ENV
- uses: docker/setup-qemu-action@v3
- uses: docker/setup-buildx-action@v3
- uses: docker/setup-qemu-action@v4
- uses: docker/setup-buildx-action@v4
- name: "Log in to GHCR"
if: github.event_name != 'pull_request'
uses: docker/login-action@v3
uses: docker/login-action@v4
with:
registry: ${{ env.REGISTRY }}
username: ${{ github.actor }}
password: ${{ secrets.GITHUB_TOKEN }}
- name: "Docker meta"
id: meta
uses: docker/metadata-action@v5
uses: docker/metadata-action@v6
with:
images: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}
tags: |
type=raw,value=latest,enable={{is_default_branch}}
type=semver,pattern={{version}}
type=sha
- name: "Build and push"
uses: docker/build-push-action@v6
uses: docker/build-push-action@v7
with:
context: .
platforms: linux/amd64,linux/arm64
# Multi-arch only when actually pushing. The docker daemon can't `--load` a
# manifest list, so on PRs we build a single arch and load it for the smoke test.
platforms: ${{ github.event_name == 'pull_request' && 'linux/amd64' || 'linux/amd64,linux/arm64' }}
push: ${{ github.event_name != 'pull_request' }}
load: ${{ github.event_name == 'pull_request' }}
tags: ${{ steps.meta.outputs.tags }}
labels: ${{ steps.meta.outputs.labels }}
cache-from: type=gha
cache-to: type=gha,mode=max
load: ${{ github.event_name == 'pull_request' }}

- name: "Smoke tests"
- name: "Smoke test (no socket needed)"
if: github.event_name == 'pull_request'
run: |
set -euo pipefail
image="${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}:${{ steps.meta.outputs.version }}"
for cmd in "java -version" "gradle --version" "node --version" "rg --version" "fd --version"; do
echo "==> $cmd"
docker run --rm "$image" bash -c "source /opt/sdkman/bin/sdkman-init.sh && $cmd"
done
# With both jobs disabled the daemon logs config, finds nothing to do, and exits 0.
out=$(docker run --rm -e KODKOD_AUTOHEAL_ENABLED=false -e KODKOD_UPDATE_ENABLED=false "$image")
echo "$out"
echo "$out" | grep -q "nothing to do"
5 changes: 5 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -1,3 +1,8 @@
.idea
.DS_Store
.env

# Gradle / build output
.gradle
.kotlin
build
24 changes: 8 additions & 16 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,21 +6,13 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/).

## [Unreleased]

### Added
- OCI standard labels in Dockerfile for image metadata
- CI smoke tests verifying key tools (java, gradle, node, rg, fd) after build
- CHANGELOG.md to track project changes

### Changed
- Tightened `/.kodkod` base directory permissions from 777 to 755; only cache subdirectories (m2, gradle, npm, pip, uv, config/*) retain 777 for write access

## [0.1.0] - 2026-02-02
## [0.2.0] - 2026-06-03

### Added
- Initial Dockerfile based on Amazon Linux 2023
- JDK 17, 21, 25 via SDKMAN with Gradle 9.3.1 and Kotlin 2.3.0
- Node.js 24, ripgrep, fd, uv, ralphex
- AI CLI tools: claude-code, codex, gemini-cli
- `run.sh` launcher script and README
- GitHub Actions CI/CD workflow with multi-arch builds (amd64/arm64)
- Apache 2.0 license
- Kotlin 2.4.0 application (Gradle 9.5.1) talking to the Docker Engine API directly
over the unix socket, with a single runtime dependency (kotlinx-serialization-json).
- Autoheal loop: restarts `unhealthy` containers (labels `kodkod.autoheal.enable`,
`kodkod.stop.timeout`).
- Update loop: pulls a container's image tag and recreates it — preserving config, env,
labels and networks — when the image changes (label `kodkod.update.enable`).
- Lightweight multi-stage Dockerfile on BellSoft Liberica (JDK build → JRE Alpine runtime).
161 changes: 20 additions & 141 deletions Dockerfile
Original file line number Diff line number Diff line change
@@ -1,149 +1,28 @@
# AI Agent Development Environment
# Amazon Linux 2023 + JDK, Gradle, Kotlin via SDKMAN
# syntax=docker/dockerfile:1

# --- Stage 1: SDKMAN + JDKs (isolated layer, rebuilds only when SDK versions change) ---
FROM amazonlinux:2023 AS sdkman
# ---- build stage: full JDK to run Gradle ----
FROM bellsoft/liberica-openjdk-alpine:25 AS build
WORKDIR /app

ENV SDKMAN_DIR="/opt/sdkman"
# Resolve dependencies first so this layer is cached across source-only changes.
COPY gradlew settings.gradle.kts build.gradle.kts gradle.properties ./
COPY gradle ./gradle
RUN ./gradlew --no-daemon dependencies > /dev/null 2>&1 || true

RUN --mount=type=cache,id=dnf-sdkman,target=/var/cache/dnf \
dnf update -y && \
dnf install -y --allowerasing curl unzip zip tar gzip findutils which
RUN curl -s "https://get.sdkman.io" | bash
RUN bash -c "source $SDKMAN_DIR/bin/sdkman-init.sh && \
sdk install java 21.0.10-librca && \
sdk install java 17.0.18-librca && \
sdk install java 25.0.2-librca && \
sdk default java 25.0.2-librca && \
sdk install gradle 9.3.1 && \
sdk install kotlin 2.3.0"
RUN chmod -R 755 $SDKMAN_DIR
RUN rm -rf $SDKMAN_DIR/tmp
# Build the runnable distribution (build/install/kodkod).
COPY src ./src
RUN ./gradlew --no-daemon installDist

# --- Stage 2: CLI tools (fd, rg, ralphex) ---
FROM amazonlinux:2023 AS tools
# ---- runtime stage: lightweight Liberica JRE on Alpine ----
FROM bellsoft/liberica-openjre-alpine:25 AS runtime

ENV RIPGREP_VERSION=15.1.0 \
FD_VERSION=10.3.0 \
RALPHEX_VERSION=0.6.0

RUN --mount=type=cache,id=dnf-tools,target=/var/cache/dnf \
dnf update -y && \
dnf install -y --allowerasing curl tar gzip

# Install ripgrep (multi-arch)
RUN ARCH=$(uname -m) && \
if [ "$ARCH" = "aarch64" ]; then RG_ARCH="aarch64-unknown-linux-gnu"; else RG_ARCH="x86_64-unknown-linux-musl"; fi && \
curl -L https://github.com/BurntSushi/ripgrep/releases/download/${RIPGREP_VERSION}/ripgrep-${RIPGREP_VERSION}-${RG_ARCH}.tar.gz -o /tmp/ripgrep.tar.gz && \
tar -xzf /tmp/ripgrep.tar.gz -C /tmp && \
cp /tmp/ripgrep-${RIPGREP_VERSION}-${RG_ARCH}/rg /usr/local/bin/ && \
chmod +x /usr/local/bin/rg

# Install fd (multi-arch)
RUN ARCH=$(uname -m) && \
if [ "$ARCH" = "aarch64" ]; then FD_ARCH="aarch64-unknown-linux-gnu"; else FD_ARCH="x86_64-unknown-linux-musl"; fi && \
curl -L https://github.com/sharkdp/fd/releases/download/v${FD_VERSION}/fd-v${FD_VERSION}-${FD_ARCH}.tar.gz -o /tmp/fd.tar.gz && \
tar -xzf /tmp/fd.tar.gz -C /tmp && \
cp /tmp/fd-v${FD_VERSION}-${FD_ARCH}/fd /usr/local/bin/ && \
chmod +x /usr/local/bin/fd

# Install ralphex (multi-arch)
RUN ARCH=$(uname -m) && \
if [ "$ARCH" = "aarch64" ]; then RX_ARCH="arm64"; else RX_ARCH="amd64"; fi && \
curl -L https://github.com/umputun/ralphex/releases/download/v${RALPHEX_VERSION}/ralphex_${RALPHEX_VERSION}_linux_${RX_ARCH}.tar.gz -o /tmp/ralphex.tar.gz && \
tar -xzf /tmp/ralphex.tar.gz -C /tmp && \
mv /tmp/ralphex /usr/local/bin/ralphex && \
chmod +x /usr/local/bin/ralphex

# --- Stage 3: Node.js via nvm ---
FROM amazonlinux:2023 AS nodejs

ENV NVM_DIR="/opt/nvm" \
NODE_VERSION=24

RUN --mount=type=cache,id=dnf-nvm,target=/var/cache/dnf \
dnf update -y && \
dnf install -y --allowerasing curl tar gzip && \
mkdir -p $NVM_DIR && \
curl -o- https://raw.githubusercontent.com/nvm-sh/nvm/v0.40.4/install.sh | bash && \
bash -c "source $NVM_DIR/nvm.sh && NVM_SYMLINK_CURRENT=true nvm install $NODE_VERSION"

# --- Stage 4: Final image ---
FROM amazonlinux:2023

LABEL org.opencontainers.image.source="https://github.com/umputun/kodkod" \
org.opencontainers.image.description="AI Agent Development Environment — Amazon Linux 2023 with JDK, Gradle, Kotlin, Node.js, and AI CLI tools" \
LABEL org.opencontainers.image.title="kodkod" \
org.opencontainers.image.description="docker-compose companion that restarts unhealthy containers and auto-updates images" \
org.opencontainers.image.source="https://github.com/Heapy/kodkod" \
org.opencontainers.image.licenses="Apache-2.0"

ENV SDKMAN_DIR="/opt/sdkman" \
NVM_DIR="/opt/nvm"

# Install system dependencies
RUN --mount=type=cache,id=dnf-final,target=/var/cache/dnf \
dnf update -y && \
dnf install -y --allowerasing \
git \
curl \
wget \
tar \
gzip \
unzip \
which \
sudo \
jq \
python3 \
python3-pip \
gcc \
make \
tmux \
zip \
findutils \
vim-minimal \
procps-ng \
less

# Copy pre-built SDKMAN with all SDKs from builder stage
COPY --from=sdkman /opt/sdkman /opt/sdkman

ENV JAVA_HOME="$SDKMAN_DIR/candidates/java/current" \
GRADLE_HOME="$SDKMAN_DIR/candidates/gradle/current" \
KOTLIN_HOME="$SDKMAN_DIR/candidates/kotlin/current" \
PATH="$NVM_DIR/current/bin:$SDKMAN_DIR/candidates/java/current/bin:$SDKMAN_DIR/candidates/gradle/current/bin:$SDKMAN_DIR/candidates/kotlin/current/bin:$PATH"

# Copy pre-built CLI tools (rg, fd, ralphex) from tools stage
COPY --from=tools /usr/local/bin/rg /usr/local/bin/fd /usr/local/bin/ralphex /usr/local/bin/

# Copy pre-built Node.js via nvm from builder stage
COPY --from=nodejs /opt/nvm /opt/nvm

# Install uv (Python package manager)
COPY --from=ghcr.io/astral-sh/uv:0.9.28 /uv /uvx /usr/local/bin/

# Install AI CLI tools globally
RUN --mount=type=cache,target=/root/.npm \
npm install -g @anthropic-ai/claude-code || true && \
npm install -g @openai/codex || true && \
npm install -g @google/gemini-cli || true

# Create cache directories that will be used at runtime
# Create cache directories under /.kodkod (mounted from ~/.kodkod on host)
RUN mkdir -p /.kodkod/m2 /.kodkod/gradle /.kodkod/npm /.kodkod/pip /.kodkod/uv \
/.kodkod/config/claude /.kodkod/config/codex /.kodkod/config/gemini-cli && \
chmod -R 755 /.kodkod && \
chmod -R 777 /.kodkod/m2 /.kodkod/gradle /.kodkod/npm /.kodkod/pip /.kodkod/uv \
/.kodkod/config/claude /.kodkod/config/codex /.kodkod/config/gemini-cli

COPY tmux.conf /etc/tmux.conf

# Pre-create home directory for non-root users and allow passwd/group updates at runtime
RUN mkdir -p /home/kodkod && chmod 777 /home/kodkod && \
chmod 666 /etc/passwd /etc/group

COPY .bashrc /home/kodkod/.bashrc
COPY entrypoint.sh /usr/local/bin/entrypoint.sh
RUN chmod +x /usr/local/bin/entrypoint.sh

WORKDIR /workspace
WORKDIR /app
COPY --from=build /app/build/install/kodkod/ ./

ENTRYPOINT ["/usr/local/bin/entrypoint.sh"]
CMD ["/bin/bash"]
# Talks to the Docker Engine API over the mounted unix socket; no extra tooling needed.
ENTRYPOINT ["/app/bin/kodkod"]
Loading
Loading