diff --git a/.claude/CLAUDE.md b/.claude/CLAUDE.md index c0f585a..2138c21 100644 --- a/.claude/CLAUDE.md +++ b/.claude/CLAUDE.md @@ -1,289 +1,40 @@ -# AI Agent Instructions +# Project Overview -This document defines patterns, conventions, and guidelines for AI agents working with this repository. +Dockerfiles for custom devcontainer images on GitHub Container Registry (ghcr.io). Each image provides a VS Code Dev Container for a specific development environment. -## Repository Overview +## Platform Constraints -This repository contains Dockerfiles for custom devcontainer images hosted on GitHub Container Registry (ghcr.io). Each image is designed for VS Code Dev Containers with specific development environments. - -## AI Agent Best Practices - -### Always Verify with Official Documentation - -When implementing features or making changes, **ALWAYS** check the latest official documentation: - -- **GitHub Actions**: https://docs.github.com/en/actions - - Runs on Ubuntu (GNU/Linux) - use GNU coreutils syntax, NOT macOS/BSD syntax - - Example: `sed -i "pattern" file` (GNU) NOT `sed -i.bak "pattern" file` (BSD) - - When substituting version strings with `sed`, use `|` as delimiter instead of `/`: - - Correct: `sed -i "s|^ARG FOO=.*|ARG FOO=$NEW_VERSION|"` — safe with any version string - - Incorrect: `sed -i "s/^ARG FOO=.*/ARG FOO=$NEW_VERSION/"` — breaks if version contains `/` - - Check platform-specific tool behavior (sed, grep, awk, etc.) - - Verify workflow syntax with official examples - -- **Docker/Dockerfile**: https://docs.docker.com/reference/dockerfile/ - - Multi-platform builds use `TARGETARCH` (amd64, arm64) - - Verify base image availability and compatibility - -- **Tools and Dependencies**: - - Bun: https://bun.sh/docs - - Hugo: https://gohugo.io/documentation/ - - GitHub CLI: https://cli.github.com/manual/ - - Always verify command syntax from official docs, not assumptions - -### Platform Awareness - -- **GitHub Actions runners**: Ubuntu Linux (use GNU tools) -- **Docker builds**: Multi-platform (linux/amd64, linux/arm64) -- **Base images**: Check Alpine vs Debian (apk vs apt, musl vs glibc) -- **Scripts**: Test for portability (sh vs bash, GNU vs BSD tools) - -### Validation Before Committing - -- YAML syntax validation for workflows -- Dockerfile syntax validation -- Test on target platform (not just local macOS/Windows) -- Verify assumptions about available tools and their versions - -## Directory Structure - -``` -repository-root/ -├── AGENTS.md # AI agent instructions (this file) -├── CLAUDE.md # Symlink to AGENTS.md -├── README.md # Repository documentation -├── .github/ -│ └── workflows/ -│ ├── README.md # Workflow documentation -│ └── build-*.yml # GitHub Actions workflows -├── devcontainer-{name}/ # Devcontainer images (VS Code integration) -│ ├── README.md # Image-specific documentation -│ └── .devcontainer/ -│ ├── Dockerfile # Image definition (source of truth) -│ ├── devcontainer.json # VS Code devcontainer configuration -│ └── *.sh # Optional scripts (e.g., init-firewall.sh) -└── {standalone-name}/ # Standalone Docker images (no devcontainer) - ├── Dockerfile # Image definition - └── README.md # Image documentation -``` +- **GitHub Actions runners**: Ubuntu Linux — use GNU coreutils, NOT macOS/BSD syntax + - `sed -i "pattern" file` (GNU), NOT `sed -i.bak "pattern" file` (BSD) + - Use `|` as sed delimiter: `sed -i "s|^ARG FOO=.*|ARG FOO=$NEW_VERSION|"` — safe with version strings containing `/` +- **Docker builds**: Multi-platform (`linux/amd64`, `linux/arm64`) — use `TARGETARCH` for arch-specific logic +- **Base images**: Check Alpine vs Debian (`apk` vs `apt`, musl vs glibc) ## Naming Conventions -### Image Names -- Devcontainer format: `devcontainer-{primary-tool}` or `devcontainer-{primary-tool}-{secondary-tool}` -- Standalone format: `{base}-{variant}` (e.g., `ralphex-fe`) -- Examples: `devcontainer-bun`, `devcontainer-hugo-bun`, `devcontainer-claude-bun`, `ralphex-fe` +### Image names +- Devcontainer: `devcontainer-{tool}` or `devcontainer-{tool}-{secondary}` +- Standalone: `{base}-{variant}` (e.g., `ralphex-fe`) ### Version ARGs in Dockerfile -- Place at the top of Dockerfile -- Format: `ARG {TOOL}_VERSION={version}` -- Examples: - ```dockerfile - ARG BUN_VERSION=1.3.5 - ARG HUGO_VERSION=0.152.2 - ARG CLAUDE_CODE_VERSION=latest - ``` - -### Image Tags -- Always include `latest` tag -- Devcontainer version-specific tag format: `{tool}{version}-{variant}` - - Examples: `ghcr.io/owner/devcontainer-bun:bun1.3.5-alpine`, `ghcr.io/owner/devcontainer-claude-bun:bun1.3.5-slim` -- Standalone image version-specific tag format: `{primary-version}` (primary tool version only) - - Example: `ghcr.io/owner/ralphex-fe:0.11.0` (ralphex version only; Bun/Hugo versions in README) - -## Dockerfile Patterns - -### Required OCI Labels -All Dockerfiles must include these labels at the end: - -```dockerfile -LABEL org.opencontainers.image.source="https://github.com/{owner}/devcontainer-images" -LABEL org.opencontainers.image.description="{Brief description}" -LABEL org.opencontainers.image.licenses="MIT" -LABEL org.opencontainers.image.title="{image-name}" -LABEL org.opencontainers.image.url="https://github.com/{owner}/devcontainer-images" -``` - -### Base Images -- Prefer official images from Docker Hub -- Use slim/alpine variants when possible -- Bun: `oven/bun:{version}-alpine` or `oven/bun:{version}-slim` -- Node: `node:{version}` or `node:{version}-slim` - -### Common Packages -Minimal images should include: -- `ca-certificates` - HTTPS connections -- `git` - Version control -- `zsh` - Better shell for VS Code integration - -### Alpine Playwright Support - -When adding Playwright to an Alpine/musl-based image, do NOT use `playwright install` — the bundled Chromium binary requires glibc and is incompatible with Alpine's musl libc. +- Place at top of file: `ARG {TOOL}_VERSION={version}` -Instead: -1. Install system Chromium via apk: `apk add --no-cache chromium ttf-freefont` -2. Set env vars in the Dockerfile (combine into one `ENV` instruction to minimize layers): - ```dockerfile - ENV PLAYWRIGHT_SKIP_BROWSER_DOWNLOAD=1 \ - PLAYWRIGHT_CHROMIUM_EXECUTABLE_PATH=/usr/bin/chromium-browser - ``` -3. In the project's `playwright.config.ts`, wire up `executablePath` manually: - ```typescript - launchOptions: { - executablePath: process.env.PLAYWRIGHT_CHROMIUM_EXECUTABLE_PATH, - // --disable-dev-shm-usage: prevents Chromium crashes in Docker (default /dev/shm is 64MB) - args: ['--no-sandbox', '--disable-setuid-sandbox', '--disable-dev-shm-usage'], - } - ``` -4. Projects install only `@playwright/test` (e.g., `bun add -d @playwright/test`) — never `playwright install`. -5. `PLAYWRIGHT_CHROMIUM_EXECUTABLE_PATH` is a project-level convention; Playwright does NOT read it automatically. +### Image tags +- Always include `latest` +- Devcontainer: `{tool}{version}-{variant}` (e.g., `bun1.3.5-alpine`) +- Standalone: `{primary-version}` only (e.g., `0.11.0`) -## devcontainer.json Patterns - -### File Header -```jsonc -// For format details, see https://aka.ms/devcontainer.json. For config options, see the -// README at: {relevant-reference-url} -``` - -### node_modules Mount -Always include to keep node_modules out of host machine: -```jsonc -"mounts": [ - // Keep node_modules out of a host machine - "source=${localWorkspaceFolderBasename}-node_modules,target=${containerWorkspaceFolder}/node_modules,type=volume" -] -``` - -### VS Code Extensions -- Group extensions by category with header comments -- Include description comment for each extension -- Format: -```jsonc -"extensions": [ - // **Category Name** - // Extension Description - "publisher.extension-id", - - // **Another Category** - // Another Extension Description - "another.extension" -] -``` - -### Common Extension Categories -- `**Claude Code**` - AI assistant -- `**Bun**` - Bun runtime support -- `**Code Quality**` - Biome (formatter and linter) -- `**Git**` - GitLens -- `**Tailwind**` - Tailwind CSS tooling -- `**Hugo**` - Hugo static site generator (when applicable) - -### Required VS Code Settings -```jsonc -"settings": { - "terminal.integrated.defaultProfile.linux": "zsh", - // Suppress extension recommendation prompts - "extensions.ignoreRecommendations": true -} -``` - -## GitHub Actions Workflow Patterns - -### Trigger Configuration - -For devcontainer images: -```yaml -on: - push: - branches: - - master - paths: - - '{image-name}/.devcontainer/Dockerfile' - - '{image-name}/.devcontainer/*.sh' # If scripts exist - workflow_dispatch: -``` - -For standalone images: -```yaml -on: - push: - branches: - - master - paths: - - '{image-name}/Dockerfile' - workflow_dispatch: -``` - -### Version Extraction -Extract versions from Dockerfile ARGs: -```yaml -- name: Extract versions from Dockerfile - id: versions - run: | - DOCKERFILE="{image-name}/.devcontainer/Dockerfile" - VERSION=$(grep '^ARG {TOOL}_VERSION=' "$DOCKERFILE" | cut -d'=' -f2) - echo "{tool}=$VERSION" >> $GITHUB_OUTPUT -``` - -### Multi-platform Build -Always build for both architectures: -```yaml -platforms: linux/amd64,linux/arm64 -``` - -### Caching -Use GitHub Actions cache: -```yaml -cache-from: type=gha -cache-to: type=gha,mode=max -``` - -## README Patterns - -### Image README Structure -1. Title and brief description -2. Features list -3. Quick Start (pre-built image usage) -4. Building locally instructions -5. Configuration details -6. Image tags -7. Customization options -8. Resources/links - -### Main README Structure -1. Repository overview -2. Image documentation links -3. Repository structure -4. Available images with usage examples -5. Adding a new image guide -6. Building and publishing info - -## When Adding a New Image - -### Devcontainer Image -1. Create directory: `devcontainer-{name}/` -2. Add `.devcontainer/Dockerfile` following patterns above -3. Add `.devcontainer/devcontainer.json` following patterns above -4. Add `README.md` with image documentation -5. Create `.github/workflows/build-devcontainer-{name}.yml` -6. Update main `README.md` with new image entry +## Code Style -### Standalone Docker Image -1. Create directory: `{image-name}/` -2. Add `Dockerfile` directly in the directory (no `.devcontainer/` subdirectory) -3. Add `README.md` with image documentation -4. Create `.github/workflows/build-{image-name}.yml` -5. Update main `README.md` with new image entry +- 2-space indentation in JSON/YAML +- Use comments in devcontainer.json (JSONC format) +- Dockerfile instruction order: ARG → FROM → packages → user/permissions → tools → LABEL -## Code Style +## Validation -- Use 2-space indentation in JSON/YAML files -- Use comments liberally in devcontainer.json (JSONC) -- Keep Dockerfile instructions organized logically: - 1. ARG declarations - 2. FROM statement - 3. Package installation - 4. User/permission setup - 5. Tool installation - 6. Labels (at the end) +- **Before committing**, check which CI workflows in `.github/workflows/` will run against the changed files and run those checks locally first (e.g., Hadolint for Dockerfiles, linters for YAML/JS, etc.) +- Validate YAML and Dockerfile syntax before committing +- Verify on target platform — not just local macOS/Windows +- Check tool version availability and command syntax from official docs: + - [GitHub Actions](https://docs.github.com/en/actions) · [Dockerfile reference](https://docs.docker.com/reference/dockerfile/) + - [Bun](https://bun.sh/docs) · [Hugo](https://gohugo.io/documentation/) · [GitHub CLI](https://cli.github.com/manual/) diff --git a/.claude/rules/devcontainer.md b/.claude/rules/devcontainer.md new file mode 100644 index 0000000..6b1a9d9 --- /dev/null +++ b/.claude/rules/devcontainer.md @@ -0,0 +1,46 @@ +--- +paths: + - "**/devcontainer.json" +--- + +# devcontainer.json Conventions + +## File Header + +```jsonc +// For format details, see https://aka.ms/devcontainer.json. For config options, see the +// README at: {relevant-reference-url} +``` + +## node_modules Mount + +Always include to keep node_modules off the host: + +```jsonc +"mounts": [ + "source=${localWorkspaceFolderBasename}-node_modules,target=${containerWorkspaceFolder}/node_modules,type=volume" +] +``` + +## VS Code Extensions + +Group by category with header comments: + +```jsonc +"extensions": [ + // **Category Name** + // Extension Description + "publisher.extension-id" +] +``` + +Common categories: `**Claude Code**`, `**Bun**`, `**Code Quality**` (OXC), `**Git**` (GitLens), `**Tailwind**`, `**Hugo**` + +## Required VS Code Settings + +```jsonc +"settings": { + "terminal.integrated.defaultProfile.linux": "fish", + "extensions.ignoreRecommendations": true +} +``` diff --git a/.claude/rules/dockerfile.md b/.claude/rules/dockerfile.md new file mode 100644 index 0000000..66e118d --- /dev/null +++ b/.claude/rules/dockerfile.md @@ -0,0 +1,50 @@ +--- +paths: + - "**/Dockerfile" +--- + +# Dockerfile Conventions + +## Required OCI Labels + +All Dockerfiles must end with these labels: + +```dockerfile +LABEL org.opencontainers.image.source="https://github.com/{owner}/devcontainer-images" +LABEL org.opencontainers.image.description="{Brief description}" +LABEL org.opencontainers.image.licenses="MIT" +LABEL org.opencontainers.image.title="{image-name}" +LABEL org.opencontainers.image.url="https://github.com/{owner}/devcontainer-images" +``` + +## Base Images + +- Prefer official Docker Hub images +- Use slim/alpine variants when possible +- Bun: `oven/bun:{version}-alpine` or `oven/bun:{version}-slim` +- Node: `node:{version}` or `node:{version}-slim` + +## Common Packages + +Minimal images should include: `ca-certificates`, `git`, `zsh` + +## Alpine Playwright Support + +Do NOT use `playwright install` on Alpine — the bundled Chromium requires glibc. + +Instead: +1. Install system Chromium: `apk add --no-cache chromium ttf-freefont` +2. Set env vars (single `ENV` instruction to minimize layers): + ```dockerfile + ENV PLAYWRIGHT_SKIP_BROWSER_DOWNLOAD=1 \ + PLAYWRIGHT_CHROMIUM_EXECUTABLE_PATH=/usr/bin/chromium-browser + ``` +3. In `playwright.config.ts`, wire up `executablePath`: + ```typescript + launchOptions: { + executablePath: process.env.PLAYWRIGHT_CHROMIUM_EXECUTABLE_PATH, + args: ['--no-sandbox', '--disable-setuid-sandbox', '--disable-dev-shm-usage'], + } + ``` +4. Projects install only `@playwright/test` — never run `playwright install` +5. `PLAYWRIGHT_CHROMIUM_EXECUTABLE_PATH` is a project convention; Playwright does NOT read it automatically diff --git a/.claude/rules/new-image.md b/.claude/rules/new-image.md new file mode 100644 index 0000000..e4d1a94 --- /dev/null +++ b/.claude/rules/new-image.md @@ -0,0 +1,16 @@ +# Adding a New Image + +## Devcontainer Image + +1. Create `devcontainer-{name}/.devcontainer/Dockerfile` +2. Create `devcontainer-{name}/.devcontainer/devcontainer.json` +3. Create `devcontainer-{name}/README.md` +4. Create `.github/workflows/build-devcontainer-{name}.yml` +5. Update root `README.md` with new image entry + +## Standalone Docker Image + +1. Create `{image-name}/Dockerfile` (no `.devcontainer/` subdirectory) +2. Create `{image-name}/README.md` +3. Create `.github/workflows/build-{image-name}.yml` +4. Update root `README.md` with new image entry diff --git a/.claude/rules/workflows.md b/.claude/rules/workflows.md new file mode 100644 index 0000000..af8b484 --- /dev/null +++ b/.claude/rules/workflows.md @@ -0,0 +1,57 @@ +--- +paths: + - ".github/workflows/**" +--- + +# GitHub Actions Workflow Conventions + +## Triggers + +Devcontainer images: + +```yaml +on: + push: + branches: [master] + paths: + - '{image-name}/.devcontainer/Dockerfile' + - '{image-name}/.devcontainer/*.sh' + workflow_dispatch: +``` + +Standalone images: + +```yaml +on: + push: + branches: [master] + paths: + - '{image-name}/Dockerfile' + workflow_dispatch: +``` + +## Version Extraction + +```yaml +- name: Extract versions from Dockerfile + id: versions + run: | + DOCKERFILE="{image-name}/.devcontainer/Dockerfile" + VERSION=$(grep '^ARG {TOOL}_VERSION=' "$DOCKERFILE" | cut -d'=' -f2) + echo "{tool}=$VERSION" >> $GITHUB_OUTPUT +``` + +## Multi-platform Build + +Always build for both architectures: + +```yaml +platforms: linux/amd64,linux/arm64 +``` + +## Caching + +```yaml +cache-from: type=gha +cache-to: type=gha,mode=max +``` diff --git a/.devcontainer/claude-sandbox/devcontainer.json b/.devcontainer/claude-sandbox/devcontainer.json index f4fbe15..d4face5 100644 --- a/.devcontainer/claude-sandbox/devcontainer.json +++ b/.devcontainer/claude-sandbox/devcontainer.json @@ -9,7 +9,9 @@ // Capabilities required for iptables firewall setup "capAdd": ["NET_ADMIN", "NET_RAW"], "init": true, + "updateRemoteUserUID": true, "remoteUser": "node", + "otherPortsAttributes": { "onAutoForward": "silent" }, "workspaceMount": "source=${localWorkspaceFolder},target=/workspace,type=bind,consistency=delegated", "workspaceFolder": "/workspace", "customizations": { @@ -29,7 +31,27 @@ "fish": { "path": "fish" }, "bash": { "path": "bash", "icon": "terminal-bash" } }, - "extensions.ignoreRecommendations": true + "extensions.ignoreRecommendations": true, + // ── Formatter settings (customize per project) ────────────────── + // Change "editor.defaultFormatter" to match your tooling: + // Biome: "biomejs.biome" | Prettier: "esbenp.prettier-vscode" + // OXC: "oxc.oxc-vscode" | None: remove these three settings + "editor.formatOnSave": true, + "editor.defaultFormatter": "oxc.oxc-vscode", + "editor.codeActionsOnSave": { + "source.fixAll": "explicit", + "source.organizeImports": "explicit" + }, + // Show workspace folder name in window title + "window.title": "${localWorkspaceFolderBasename}", + // Sandbox visual identity — Claude Dark theme with coral status bar + "workbench.colorTheme": "Claude Dark", + "workbench.colorCustomizations": { + "statusBar.background": "#E8543E", + "statusBar.foreground": "#ffffff" + }, + // Allow Claude Code to skip permission prompts in sandbox + "claudeCode.allowDangerouslySkipPermissions": true } } }, @@ -42,6 +64,7 @@ "containerEnv": { "TZ": "${localEnv:TZ:America/Edmonton}", "CLAUDE_CONFIG_DIR": "/home/node/.claude", + "NODE_OPTIONS": "--max-old-space-size=4096", // Sandbox blocks OAuth login — inject token from host "CLAUDE_CODE_OAUTH_TOKEN": "${localEnv:CLAUDE_CODE_OAUTH_TOKEN}" }, diff --git a/.devcontainer/claude-sandbox/init-firewall.sh b/.devcontainer/claude-sandbox/init-firewall.sh index ab3dd24..b8ae177 100755 --- a/.devcontainer/claude-sandbox/init-firewall.sh +++ b/.devcontainer/claude-sandbox/init-firewall.sh @@ -67,7 +67,10 @@ for domain in \ "statsig.com" \ "marketplace.visualstudio.com" \ "vscode.blob.core.windows.net" \ - "update.code.visualstudio.com"; do + "update.code.visualstudio.com" \ + "auth.openai.com" \ + "api.openai.com" \ + "chatgpt.com"; do echo "Resolving $domain..." ips=$(dig +noall +answer A "$domain" | awk '$4 == "A" {print $5}') if [ -z "$ips" ]; then diff --git a/.devcontainer/devcontainer.json b/.devcontainer/devcontainer.json index e26f1c8..681171d 100644 --- a/.devcontainer/devcontainer.json +++ b/.devcontainer/devcontainer.json @@ -6,7 +6,9 @@ "target": "default" }, "init": true, + "updateRemoteUserUID": true, "remoteUser": "node", + "otherPortsAttributes": { "onAutoForward": "silent" }, "workspaceMount": "source=${localWorkspaceFolder},target=/workspace,type=bind,consistency=delegated", "workspaceFolder": "/workspace", "customizations": { @@ -31,7 +33,19 @@ "bash": { "path": "bash", "icon": "terminal-bash" } }, // Suppress extension recommendation prompts - "extensions.ignoreRecommendations": true + "extensions.ignoreRecommendations": true, + // ── Formatter settings (customize per project) ────────────────── + // Change "editor.defaultFormatter" to match your tooling: + // Biome: "biomejs.biome" | Prettier: "esbenp.prettier-vscode" + // OXC: "oxc.oxc-vscode" | None: remove these three settings + "editor.formatOnSave": true, + "editor.defaultFormatter": "oxc.oxc-vscode", + "editor.codeActionsOnSave": { + "source.fixAll": "explicit", + "source.organizeImports": "explicit" + }, + // Show workspace folder name in window title + "window.title": "${localWorkspaceFolderBasename}" } } }, @@ -43,7 +57,8 @@ ], "containerEnv": { "TZ": "${localEnv:TZ:America/Edmonton}", - "CLAUDE_CONFIG_DIR": "/home/node/.claude" + "CLAUDE_CONFIG_DIR": "/home/node/.claude", + "NODE_OPTIONS": "--max-old-space-size=4096" }, // Initialize Claude Code plugins (runs once when container is created) "postCreateCommand": "sudo chown -R node /home/node/.claude && bash /workspace/.devcontainer/init-plugins.sh", diff --git a/.devcontainer/init-plugins.sh b/.devcontainer/init-plugins.sh index 032533d..e215093 100755 --- a/.devcontainer/init-plugins.sh +++ b/.devcontainer/init-plugins.sh @@ -25,7 +25,7 @@ claude plugin marketplace add umputun/ralphex || { echo "Note: ralphex marketplace may already be added or unavailable" } -# Plugins relevant for a Dockerfiles/infrastructure repo +# Plugins for development workflow (code quality, web dev, analytics) PLUGINS=( "code-review@claude-plugins-official" "code-simplifier@claude-plugins-official" @@ -33,6 +33,10 @@ PLUGINS=( "explanatory-output-style@claude-plugins-official" "claude-md-management@claude-plugins-official" "claude-code-setup@claude-plugins-official" + "frontend-design@claude-plugins-official" + "typescript-lsp@claude-plugins-official" + "playwright@claude-plugins-official" + "posthog@claude-plugins-official" "ralphex@ralphex" ) diff --git a/.github/workflows/build-ralphex-fe.yml b/.github/workflows/build-ralphex-fe.yml index e5f5902..bc2967d 100644 --- a/.github/workflows/build-ralphex-fe.yml +++ b/.github/workflows/build-ralphex-fe.yml @@ -6,8 +6,14 @@ on: - master paths: - 'ralphex-fe/Dockerfile' + - 'ralphex-fe/files/**' workflow_dispatch: +permissions: + contents: read + packages: write + id-token: write + concurrency: group: build-ralphex-fe-${{ github.ref }} cancel-in-progress: true @@ -19,7 +25,7 @@ jobs: version-tag: ${{ steps.versions.outputs.version-tag }} steps: - name: Checkout repository - uses: actions/checkout@v4 + uses: actions/checkout@v6 - name: Extract versions from Dockerfile id: versions @@ -28,47 +34,76 @@ jobs: BUN_VERSION=$(grep '^ARG BUN_VERSION=' "$DOCKERFILE" | cut -d'=' -f2) HUGO_VERSION=$(grep '^ARG HUGO_VERSION=' "$DOCKERFILE" | cut -d'=' -f2) - RALPHEX_VERSION=$(grep '^ARG RALPHEX_VERSION=' "$DOCKERFILE" | cut -d'=' -f2) - if [ -z "$BUN_VERSION" ] || [ -z "$HUGO_VERSION" ] || [ -z "$RALPHEX_VERSION" ]; then + if [ -z "$BUN_VERSION" ] || [ -z "$HUGO_VERSION" ]; then echo "Error: Failed to extract version(s) from Dockerfile" echo " Bun: ${BUN_VERSION:-}" echo " Hugo: ${HUGO_VERSION:-}" - echo " Ralphex: ${RALPHEX_VERSION:-}" exit 1 fi - echo "version-tag=bun${BUN_VERSION}-hugo${HUGO_VERSION}-ralphex${RALPHEX_VERSION}" >> "$GITHUB_OUTPUT" + echo "version-tag=bun${BUN_VERSION}-hugo${HUGO_VERSION}" >> "$GITHUB_OUTPUT" echo "Extracted versions:" echo " Bun: $BUN_VERSION" echo " Hugo: $HUGO_VERSION" - echo " Ralphex: $RALPHEX_VERSION" build: needs: prepare + uses: docker/github-builder/.github/workflows/build.yml@v1 permissions: contents: read packages: write - uses: ./.github/workflows/reusable-docker-build.yml + id-token: write with: - image-name: ralphex-fe + output: image + push: true context: ralphex-fe - dockerfile: ralphex-fe/Dockerfile - version-tag: ${{ needs.prepare.outputs.version-tag }} - verify-command: 'bun --version && hugo version' - extra-verify-script: | - chromium-browser --no-sandbox --version && \ - test "$PLAYWRIGHT_CHROMIUM_EXECUTABLE_PATH" = "/usr/bin/chromium-browser" && \ - echo "OK: PLAYWRIGHT_CHROMIUM_EXECUTABLE_PATH is correct" || \ - (echo "FAIL: expected /usr/bin/chromium-browser, got $PLAYWRIGHT_CHROMIUM_EXECUTABLE_PATH" && exit 1) && \ - test -x "$PLAYWRIGHT_CHROMIUM_EXECUTABLE_PATH" && \ - echo "OK: binary is executable at $PLAYWRIGHT_CHROMIUM_EXECUTABLE_PATH" || \ - (echo "FAIL: binary not executable at $PLAYWRIGHT_CHROMIUM_EXECUTABLE_PATH" && exit 1) && \ - test "$PLAYWRIGHT_SKIP_BROWSER_DOWNLOAD" = "1" && \ - echo "OK: PLAYWRIGHT_SKIP_BROWSER_DOWNLOAD=1" || \ - (echo "FAIL: got PLAYWRIGHT_SKIP_BROWSER_DOWNLOAD=$PLAYWRIGHT_SKIP_BROWSER_DOWNLOAD" && exit 1) && \ - ls /usr/share/fonts/freefont/FreeSans.otf && \ - echo "OK: freefont found" || \ - (echo "FAIL: freefont not found" && exit 1) - secrets: inherit + platforms: linux/amd64,linux/arm64 + meta-images: ghcr.io/gatezh/ralphex-fe + meta-tags: | + type=raw,value=latest + type=raw,value=${{ needs.prepare.outputs.version-tag }} + secrets: + registry-auths: | + - registry: ghcr.io + username: ${{ github.actor }} + password: ${{ secrets.GITHUB_TOKEN }} + + verify: + name: Verify (${{ matrix.arch }}) + needs: build + strategy: + fail-fast: false + matrix: + include: + - runner: ubuntu-24.04 + arch: amd64 + - runner: ubuntu-24.04-arm + arch: arm64 + runs-on: ${{ matrix.runner }} + steps: + - name: Log in to GHCR + uses: docker/login-action@v4 + with: + registry: ghcr.io + username: ${{ github.actor }} + password: ${{ secrets.GITHUB_TOKEN }} + + - name: Verify tools + run: | + docker pull ghcr.io/gatezh/ralphex-fe:latest + docker run --rm --entrypoint sh ghcr.io/gatezh/ralphex-fe:latest -c " + bun --version && + hugo version && + python3 --version && + go version && + node --version && + /srv/ralphex --version && + claude --version && + id app && + test -f /init.sh && + test -f /srv/init.sh && + test -d /home/app/.cache/ms-playwright && + echo 'All checks passed' + " diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 058c4fd..8967281 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -29,7 +29,7 @@ jobs: - ralphex-fe/Dockerfile steps: - name: Checkout repository - uses: actions/checkout@v4 + uses: actions/checkout@v6 - name: Run hadolint uses: hadolint/hadolint-action@v3.0.0 @@ -42,7 +42,7 @@ jobs: runs-on: ubuntu-latest steps: - name: Checkout repository - uses: actions/checkout@v4 + uses: actions/checkout@v6 - name: Run actionlint uses: raven-actions/actionlint@v2 @@ -56,10 +56,10 @@ jobs: has-changes: ${{ steps.set-matrix.outputs.has-changes }} steps: - name: Checkout repository - uses: actions/checkout@v4 + uses: actions/checkout@v6 - name: Check changed paths - uses: dorny/paths-filter@v3 + uses: dorny/paths-filter@v4 id: filter with: filters: | @@ -142,7 +142,8 @@ jobs: } >> "$GITHUB_OUTPUT" fi - # ── Build changed images and verify installed tools ───────────────── + # ── Build changed images (amd64 only) and verify installed tools ──── + # arm64 is tested on native runners at merge time — no QEMU emulation in CI. build-and-verify: name: Build · ${{ matrix.image }} needs: detect-changes @@ -153,16 +154,13 @@ jobs: matrix: ${{ fromJSON(needs.detect-changes.outputs.matrix) }} steps: - name: Checkout repository - uses: actions/checkout@v4 - - - name: Set up QEMU - uses: docker/setup-qemu-action@v3 + uses: actions/checkout@v6 - name: Set up Docker Buildx - uses: docker/setup-buildx-action@v3 + uses: docker/setup-buildx-action@v4 - name: Build amd64 image - uses: docker/build-push-action@v5 + uses: docker/build-push-action@v7 with: context: ${{ matrix.context }} file: ${{ matrix.dockerfile }} @@ -177,12 +175,3 @@ jobs: IMAGE_TAG: ${{ matrix.image }}:test VERIFY_CMD: ${{ matrix.verify }} run: docker run --rm --entrypoint sh "$IMAGE_TAG" -c "$VERIFY_CMD" - - - name: Build arm64 image - uses: docker/build-push-action@v5 - with: - context: ${{ matrix.context }} - file: ${{ matrix.dockerfile }} - platforms: linux/arm64 - cache-from: type=gha,scope=${{ matrix.image }}-arm64 - cache-to: type=gha,mode=max,scope=${{ matrix.image }}-arm64 diff --git a/.hadolint.yaml b/.hadolint.yaml index c78cc9a..7561df7 100644 --- a/.hadolint.yaml +++ b/.hadolint.yaml @@ -19,6 +19,14 @@ ignored: # Anthropic Claude Code devcontainer pattern (anthropics/claude-code). - SC2034 # variable appears unused + # ralphex-fe entrypoint runs as root to handle APP_UID remapping, + # then drops to app user via gosu. No final USER directive is correct. + - DL3002 # last USER should not be root + + # Dev tools (Claude Code) intentionally unpinned — images rebuild daily + # to always get latest. Pinning would defeat the purpose. + - DL3016 # pin versions in npm install + # Pipes inside command substitutions (e.g. grep | cut) are intentional; # empty results are explicitly checked afterwards. - SC2312 # pipe in command substitution masks return value diff --git a/claude-code/README.md b/claude-code/README.md index b5a14e3..49a1823 100644 --- a/claude-code/README.md +++ b/claude-code/README.md @@ -109,7 +109,7 @@ Add to your project's `.devcontainer/devcontainer.json`: "CLAUDE_CODE_OAUTH_TOKEN": "${localEnv:CLAUDE_CODE_OAUTH_TOKEN}" }, // Runs before postStartCommand (firewall), so network is still available for browser downloads. - "postCreateCommand": "mise install && npx playwright install --only-shell", + "postCreateCommand": "mise install && bun install && npx playwright install --only-shell", // Firewall init — script is bind-mounted from the project "postStartCommand": "sudo /usr/local/bin/init-firewall.sh", "waitFor": "postStartCommand" diff --git a/docs/superpowers/plans/2026-03-23-ralphex-fe-debian-rebuild.md b/docs/superpowers/plans/2026-03-23-ralphex-fe-debian-rebuild.md new file mode 100644 index 0000000..2669fc0 --- /dev/null +++ b/docs/superpowers/plans/2026-03-23-ralphex-fe-debian-rebuild.md @@ -0,0 +1,575 @@ +# Ralphex-FE Debian Rebuild — Implementation Plan + +> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking. + +**Goal:** Rebuild the `ralphex-fe` standalone Docker image from `node:24-trixie-slim` (Debian) instead of the Alpine-based `ghcr.io/umputun/ralphex` base image, eliminating all Alpine/musl compatibility hacks while maintaining full compatibility with the `ralphex-dk.sh` docker-wrapper script. + +**Architecture:** A single-stage Dockerfile based on `node:24-trixie-slim` that creates an `app` user (matching the ralphex wrapper's expectations), installs all tools natively on Debian, and ships an init.sh entrypoint adapted from `umputun/baseimage` for APP_UID remapping. Two support scripts (`init.sh` and `init-docker.sh`) are copied into the image. + +**Tech Stack:** Docker, Debian Trixie, Node.js 24, Bun, Hugo Extended, Go, Python 3, Playwright, Claude Code CLI + +**Key decisions documented in this plan:** +- Why Debian over Alpine → see "Background" section +- Why `app` user, not `node` → ralphex wrapper compatibility +- Where init.sh and init-docker.sh come from → source references in each task + +## Background + +The previous `ralphex-fe` image extended `ghcr.io/umputun/ralphex:0.20.0` (Alpine-based). This caused friction: +- `gcompat` needed for Hugo Extended (glibc binary on musl) +- Playwright's bundled Chromium incompatible with Alpine/musl — required system Chromium + manual `executablePath` wiring +- Limited Alpine package ecosystem + +The new approach uses Debian where all tools work natively. The `claude-code` devcontainer image (`claude-code/.devcontainer/Dockerfile`) in this repo served as reference for Debian-based tool installation patterns (git-delta, Playwright, Claude Code via npm, ralphex binary download). + +## Source References + +Scripts and patterns in this image are adapted from external sources. Document these so future maintainers know where to look for upstream changes: + +| File | Source | Notes | +|---|---|---| +| `ralphex-fe/files/init.sh` | [`umputun/baseimage` — `base.alpine/files/init.sh`](https://github.com/umputun/baseimage/blob/master/base.alpine/files/init.sh) | Adapted for Debian: `gosu` replaces `su-exec`, `dumb-init` path adjusted, Alpine-specific `addgroup`/`delgroup` replaced with Debian equivalents | +| `ralphex-fe/files/init-docker.sh` | [`umputun/ralphex` — `scripts/internal/init-docker.sh`](https://github.com/umputun/ralphex/blob/master/scripts/internal/init-docker.sh) | Copied as-is; handles credential copying from mounted volumes | +| Ralphex binary install | [`claude-code/.devcontainer/Dockerfile` lines 104-119](claude-code/.devcontainer/Dockerfile) | Pattern for downloading latest ralphex from GitHub Releases | +| Playwright install | [`claude-code/.devcontainer/Dockerfile` lines 122-134](claude-code/.devcontainer/Dockerfile) | Two-layer strategy: system deps (root) + browser binary (user) | +| Hugo Extended install | [`ralphex-fe/Dockerfile` (current)](ralphex-fe/Dockerfile) | Kept from current image: direct download with checksum verification | + +## File Structure + +``` +ralphex-fe/ +├── Dockerfile # Complete rewrite — Debian-based +├── files/ +│ ├── init.sh # NEW — entrypoint (adapted from umputun/baseimage for Debian) +│ └── init-docker.sh # NEW — credential copier (from umputun/ralphex) +└── README.md # Update to reflect new base and usage + +.github/workflows/ +└── build-ralphex-fe.yml # Update version extraction and verify commands +``` + +--- + +### Task 1: Create entrypoint script (`init.sh`) + +**Files:** +- Create: `ralphex-fe/files/init.sh` + +**Source:** Adapted from [`umputun/baseimage` — `base.alpine/files/init.sh`](https://github.com/umputun/baseimage/blob/master/base.alpine/files/init.sh) + +**Adaptations for Debian:** +- Shebang: `#!/usr/bin/dumb-init /bin/sh` (Alpine uses `/sbin/dinit` which is a symlink) +- `su-exec` → `gosu` (Debian equivalent) +- `addgroup`/`delgroup` → `groupadd`/`groupdel`/`usermod` (Debian uses shadow utils) +- Timezone: uses same `/usr/share/zoneinfo` copy pattern (works on Debian with `tzdata`) + +- [ ] **Step 1: Create the files directory** + +```bash +mkdir -p ralphex-fe/files +``` + +- [ ] **Step 2: Write init.sh** + +Write `ralphex-fe/files/init.sh` — the entrypoint script that handles: +1. Timezone configuration +2. APP_UID remapping (sed on /etc/passwd and /etc/group) +3. DOCKER_GID remapping for Docker socket access +4. Ownership of /srv and /home/app +5. Running /srv/init.sh if it exists (credential copier) +6. Dropping to `app` user via `gosu` to execute CMD + +```sh +#!/usr/bin/dumb-init /bin/sh + +# Entrypoint for ralphex-fe container. +# Adapted from umputun/baseimage (base.alpine/files/init.sh) for Debian. +# Changes: gosu instead of su-exec, groupadd/groupdel instead of addgroup/delgroup, +# dumb-init path adjusted for Debian. +# Source: https://github.com/umputun/baseimage/blob/master/base.alpine/files/init.sh + +uid=$(id -u) + +if [ "${uid}" -eq 0 ]; then + [ "${INIT_QUIET}" != "1" ] && echo "init container" + + # set container's time zone + if [ -f "/usr/share/zoneinfo/${TIME_ZONE}" ]; then + cp "/usr/share/zoneinfo/${TIME_ZONE}" /etc/localtime + echo "${TIME_ZONE}" >/etc/timezone + [ "${INIT_QUIET}" != "1" ] && echo "set timezone ${TIME_ZONE} ($(date))" + fi + + # set UID for user app + if [ "${APP_UID}" != "1001" ]; then + [ "${INIT_QUIET}" != "1" ] && echo "set custom APP_UID=${APP_UID}" + sed -i "s/:1001:1001:/:${APP_UID}:${APP_UID}:/g" /etc/passwd + sed -i "s/:1001:/:${APP_UID}:/g" /etc/group + else + [ "${INIT_QUIET}" != "1" ] && echo "custom APP_UID not defined, using default uid=1001" + fi + + # set GID for docker group + if [ "${DOCKER_GID}" != "999" ]; then + [ "${INIT_QUIET}" != "1" ] && echo "set custom DOCKER_GID=${DOCKER_GID}" + existing_group=$(getent group "${DOCKER_GID}" | cut -d: -f1) + if [ -n "${existing_group}" ] && [ "${existing_group}" != "docker" ]; then + [ "${INIT_QUIET}" != "1" ] && echo "GID ${DOCKER_GID} used by '${existing_group}', adding app to it" + usermod -aG "${existing_group}" app || { echo "error: failed to add app to group '${existing_group}'"; exit 1; } + else + groupdel docker 2>/dev/null || true + groupadd -g "${DOCKER_GID}" docker || { echo "error: failed to create docker group with GID=${DOCKER_GID}"; exit 1; } + usermod -aG docker app || { echo "error: failed to add app to docker group"; exit 1; } + fi + else + [ "${INIT_QUIET}" != "1" ] && echo "custom DOCKER_GID not defined, using default gid=999" + fi + + chown -R app:app /srv + if [ "${SKIP_HOME_CHOWN}" != "1" ]; then + chown -R app:app /home/app + fi +fi + +if [ -f "/srv/init.sh" ]; then + [ "${INIT_QUIET}" != "1" ] && echo "execute /srv/init.sh" + chmod +x /srv/init.sh + /srv/init.sh + if [ "$?" -ne "0" ]; then + echo "/srv/init.sh failed" + exit 1 + fi +fi + +[ "${INIT_QUIET}" != "1" ] && echo "execute $*" +if [ "${uid}" -eq 0 ]; then + exec gosu app "$@" +else + exec "$@" +fi +``` + +- [ ] **Step 3: Make executable** + +```bash +chmod +x ralphex-fe/files/init.sh +``` + +- [ ] **Step 4: Commit** + +```bash +git add ralphex-fe/files/init.sh +git commit -m "feat(ralphex-fe): add Debian-adapted entrypoint from umputun/baseimage" +``` + +--- + +### Task 2: Create credential copier script (`init-docker.sh`) + +**Files:** +- Create: `ralphex-fe/files/init-docker.sh` + +**Source:** Copied from [`umputun/ralphex` — `scripts/internal/init-docker.sh`](https://github.com/umputun/ralphex/blob/master/scripts/internal/init-docker.sh) + +This script is installed as `/srv/init.sh` in the image (not to be confused with the entrypoint `/init.sh`). The entrypoint calls `/srv/init.sh` before running the main command. It copies Claude and Codex credentials from read-only mounts into the app user's home directory. + +- [ ] **Step 1: Write init-docker.sh** + +```sh +#!/bin/sh +# Init script for ralphex docker container. +# The entrypoint (/init.sh) runs /srv/init.sh if it exists before the main command. +# +# Source: https://github.com/umputun/ralphex/blob/master/scripts/internal/init-docker.sh +# Copied as-is from umputun/ralphex. Check upstream for updates. + +# copy only essential claude files (not the entire 2GB directory) +if [ -d /mnt/claude ]; then + mkdir -p /home/app/.claude + # copy config files only (not cache, history, debug, todos, etc.) + for f in .credentials.json settings.json settings.local.json CLAUDE.md format.sh; do + [ -e "/mnt/claude/$f" ] && cp -L "/mnt/claude/$f" "/home/app/.claude/$f" 2>/dev/null || true + done + # copy essential directories (symlinked in dotfiles setups) + for d in commands skills hooks agents plugins; do + [ -d "/mnt/claude/$d" ] && cp -rL "/mnt/claude/$d" "/home/app/.claude/" 2>/dev/null || true + done + chown -R app:app /home/app/.claude +fi + +# copy credentials extracted from macOS keychain (mounted separately) +if [ -f /mnt/claude-credentials.json ]; then + mkdir -p /home/app/.claude + cp /mnt/claude-credentials.json /home/app/.claude/.credentials.json + chown -R app:app /home/app/.claude + chmod 600 /home/app/.claude/.credentials.json +fi + +# copy codex credentials if mounted +if [ -d /mnt/codex ]; then + mkdir -p /home/app/.codex + cp -rL /mnt/codex/* /home/app/.codex/ 2>/dev/null || true + chown -R app:app /home/app/.codex +fi +``` + +- [ ] **Step 2: Make executable** + +```bash +chmod +x ralphex-fe/files/init-docker.sh +``` + +- [ ] **Step 3: Commit** + +```bash +git add ralphex-fe/files/init-docker.sh +git commit -m "feat(ralphex-fe): add credential copier from umputun/ralphex" +``` + +--- + +### Task 3: Rewrite the Dockerfile + +**Files:** +- Rewrite: `ralphex-fe/Dockerfile` + +**References:** +- `claude-code/.devcontainer/Dockerfile` — patterns for Playwright, Claude Code, ralphex binary install +- `umputun/ralphex/Dockerfile` — what the official image ships (user, env vars, CMD) +- `umputun/baseimage/base.alpine/Dockerfile` — user creation, entrypoint setup + +- [ ] **Step 1: Write the new Dockerfile** + +The Dockerfile should follow this structure (in order per project conventions: ARG → FROM → packages → user/permissions → tools → LABEL): + +```dockerfile +ARG BUN_VERSION=1.3.9 +ARG HUGO_VERSION=0.156.0 +ARG PLAYWRIGHT_VERSION=1.58.2 + +FROM node:24-trixie-slim + +ARG BUN_VERSION +ARG HUGO_VERSION +ARG PLAYWRIGHT_VERSION +ARG TARGETARCH + +# ── System packages ────────────────────────────────────────────────────────── +# - ca-certificates: SSL/TLS for HTTPS connections +# - curl: downloading tools and installers +# - dumb-init: PID 1 init for proper signal handling (used by /init.sh entrypoint) +# - git: version control +# - golang-go: required for Hugo Modules to download and manage dependencies +# - gosu: drop privileges to app user (Debian equivalent of Alpine's su-exec) +# - jq: JSON processing +# - python3: scripting (Claude Code uses python for ad-hoc scripts) +# - ripgrep: fast code search +# - tzdata: timezone data for container timezone configuration +# - unzip: required by Bun install script on Linux +# - wget: required to download Hugo Extended binary +RUN apt-get update && apt-get install -y --no-install-recommends \ + ca-certificates \ + curl \ + dumb-init \ + git \ + golang-go \ + gosu \ + jq \ + python3 \ + ripgrep \ + tzdata \ + unzip \ + wget \ + && apt-get clean && rm -rf /var/lib/apt/lists/* + +# ── App user (matches umputun/baseimage convention) ────────────────────────── +# The ralphex docker-wrapper (ralphex-dk.sh) expects: +# - user "app" with home at /home/app +# - UID 1001 (remappable at runtime via APP_UID env var) +# - /srv owned by app +# - docker group GID 999 +# Source: https://github.com/umputun/baseimage/blob/master/base.alpine/Dockerfile +ENV APP_USER=app \ + APP_UID=1001 \ + DOCKER_GID=999 \ + TIME_ZONE=America/Chicago + +# Reserve GID 998 for ping to prevent conflicts (matches umputun/baseimage convention) +RUN groupadd -g 998 ping \ + && groupadd -g ${DOCKER_GID} docker \ + && useradd -m -s /bin/sh -u ${APP_UID} ${APP_USER} \ + && usermod -aG docker ${APP_USER} \ + && mkdir -p /srv /workspace \ + && chown -R ${APP_USER}:${APP_USER} /srv /workspace \ + && cp /usr/share/zoneinfo/${TIME_ZONE} /etc/localtime \ + && echo "${TIME_ZONE}" > /etc/timezone + +# ── Entrypoint and init scripts ────────────────────────────────────────────── +# /init.sh — entrypoint handling APP_UID remapping and privilege drop +# Adapted from: https://github.com/umputun/baseimage/blob/master/base.alpine/files/init.sh +# /srv/init.sh — credential copier run before main command +# Source: https://github.com/umputun/ralphex/blob/master/scripts/internal/init-docker.sh +COPY files/init.sh /init.sh +COPY files/init-docker.sh /srv/init.sh +RUN chmod +x /init.sh /srv/init.sh + +WORKDIR /workspace + +# ── Ralphex binary (latest from GitHub Releases) ───────────────────────────── +# Pattern from: claude-code/.devcontainer/Dockerfile (lines 104-119) +# The wrapper script calls /srv/ralphex as the container command. +RUN set -eux; \ + ARCH="$(uname -m)"; \ + RALPHEX_ARCH=$(echo "$ARCH" | sed 's/x86_64/amd64/;s/aarch64/arm64/'); \ + RALPHEX_VERSION=$(curl -fsSL https://api.github.com/repos/umputun/ralphex/releases/latest \ + | jq -r '.tag_name' | sed 's/^v//'); \ + curl -fsSL "https://github.com/umputun/ralphex/releases/download/v${RALPHEX_VERSION}/ralphex_${RALPHEX_VERSION}_linux_${RALPHEX_ARCH}.tar.gz" \ + | tar -xz -C /srv ralphex; \ + chmod +x /srv/ralphex + +# ── Playwright (native Debian — no Alpine hacks needed) ────────────────────── +# Two-layer strategy from claude-code/.devcontainer/Dockerfile: +# 1. System deps (install-deps) — heavy, needs root +# 2. Browser binary (install --only-shell) — light, version-specific +RUN npx -y playwright@${PLAYWRIGHT_VERSION} install-deps chromium +USER app +RUN npx -y playwright@${PLAYWRIGHT_VERSION} install --only-shell +USER root + +# ── Claude Code CLI ────────────────────────────────────────────────────────── +# npm install (not native installer) to avoid rate-limiting in parallel Docker builds. +# See: claude-code/.devcontainer/Dockerfile for rationale. +RUN npm install -g @anthropic-ai/claude-code + +# ── Bun ────────────────────────────────────────────────────────────────────── +ENV BUN_INSTALL=/usr/local/bun +ENV PATH="$BUN_INSTALL/bin:$PATH" + +RUN curl -fsSL https://bun.sh/install | bash -s "bun-v${BUN_VERSION}" && \ + bun --version + +# ── Hugo Extended (direct download with checksum verification) ─────────────── +# Kept from current ralphex-fe/Dockerfile — works natively on Debian (no gcompat). +RUN set -eux; \ + wget -O hugo.tar.gz "https://github.com/gohugoio/hugo/releases/download/v${HUGO_VERSION}/hugo_extended_${HUGO_VERSION}_linux-${TARGETARCH}.tar.gz"; \ + wget -O hugo_checksums.txt "https://github.com/gohugoio/hugo/releases/download/v${HUGO_VERSION}/hugo_${HUGO_VERSION}_checksums.txt"; \ + EXPECTED_CHECKSUM=$(grep "hugo_extended_${HUGO_VERSION}_linux-${TARGETARCH}.tar.gz" hugo_checksums.txt | cut -d' ' -f1); \ + if [ -z "$EXPECTED_CHECKSUM" ]; then \ + echo "Error: Could not find checksum for hugo_extended_${HUGO_VERSION}_linux-${TARGETARCH}.tar.gz"; \ + exit 1; \ + fi; \ + ACTUAL_CHECKSUM=$(sha256sum hugo.tar.gz | cut -d' ' -f1); \ + if [ "$EXPECTED_CHECKSUM" != "$ACTUAL_CHECKSUM" ]; then \ + echo "Checksum verification failed for Hugo!"; \ + echo "Expected: $EXPECTED_CHECKSUM"; \ + echo "Actual: $ACTUAL_CHECKSUM"; \ + exit 1; \ + fi; \ + echo "Hugo checksum verified: $ACTUAL_CHECKSUM"; \ + tar -xzf hugo.tar.gz -C /usr/local/bin/ hugo; \ + rm hugo.tar.gz hugo_checksums.txt; \ + hugo version + +# ── Environment ────────────────────────────────────────────────────────────── +ENV RALPHEX_DOCKER=1 \ + USE_BUILTIN_RIPGREP=0 \ + PLAYWRIGHT_VERSION=${PLAYWRIGHT_VERSION} + +# Ralphex web dashboard port +EXPOSE 8080 + +# No final USER directive — /init.sh runs as root, handles APP_UID remapping, +# then drops to app user via gosu before executing CMD. +ENTRYPOINT ["/init.sh"] +CMD ["/srv/ralphex"] + +# ── OCI labels ─────────────────────────────────────────────────────────────── +LABEL org.opencontainers.image.source="https://github.com/gatezh/devcontainer-images" +LABEL org.opencontainers.image.description="Ralphex-fe: Frontend development image with Bun, Hugo Extended, Playwright, and Claude Code on Debian" +LABEL org.opencontainers.image.licenses="MIT" +LABEL org.opencontainers.image.title="ralphex-fe" +LABEL org.opencontainers.image.url="https://github.com/gatezh/devcontainer-images" +``` + +- [ ] **Step 2: Verify Dockerfile syntax** + +```bash +docker buildx build --check ralphex-fe/ +``` + +- [ ] **Step 3: Commit** + +```bash +git add ralphex-fe/Dockerfile +git commit -m "feat(ralphex-fe): rewrite Dockerfile from Alpine to Debian (node:24-trixie-slim)" +``` + +--- + +### Task 4: Update the GitHub Actions workflow + +**Files:** +- Modify: `.github/workflows/build-ralphex-fe.yml` + +Changes: +- Drop `RALPHEX_VERSION` extraction (ralphex binary is now fetched as latest at build time, not pinned via base image ARG) +- Version tag becomes `bun${BUN_VERSION}-hugo${HUGO_VERSION}` +- Update verify commands: replace Alpine-specific Chromium/Playwright checks with Debian-appropriate ones + +- [ ] **Step 1: Update the workflow** + +```yaml +name: Build ralphex-fe + +on: + push: + branches: + - master + paths: + - 'ralphex-fe/Dockerfile' + - 'ralphex-fe/files/**' + workflow_dispatch: + +concurrency: + group: build-ralphex-fe-${{ github.ref }} + cancel-in-progress: true + +jobs: + prepare: + runs-on: ubuntu-latest + outputs: + version-tag: ${{ steps.versions.outputs.version-tag }} + steps: + - name: Checkout repository + uses: actions/checkout@v4 + + - name: Extract versions from Dockerfile + id: versions + run: | + DOCKERFILE="ralphex-fe/Dockerfile" + + BUN_VERSION=$(grep '^ARG BUN_VERSION=' "$DOCKERFILE" | cut -d'=' -f2) + HUGO_VERSION=$(grep '^ARG HUGO_VERSION=' "$DOCKERFILE" | cut -d'=' -f2) + + if [ -z "$BUN_VERSION" ] || [ -z "$HUGO_VERSION" ]; then + echo "Error: Failed to extract version(s) from Dockerfile" + echo " Bun: ${BUN_VERSION:-}" + echo " Hugo: ${HUGO_VERSION:-}" + exit 1 + fi + + echo "version-tag=bun${BUN_VERSION}-hugo${HUGO_VERSION}" >> "$GITHUB_OUTPUT" + + echo "Extracted versions:" + echo " Bun: $BUN_VERSION" + echo " Hugo: $HUGO_VERSION" + + build: + needs: prepare + permissions: + contents: read + packages: write + uses: ./.github/workflows/reusable-docker-build.yml + with: + image-name: ralphex-fe + context: ralphex-fe + dockerfile: ralphex-fe/Dockerfile + version-tag: ${{ needs.prepare.outputs.version-tag }} + verify-command: 'bun --version && hugo version && python3 --version && go version && node --version' + extra-verify-script: | + test -x /srv/ralphex && echo "OK: ralphex binary at /srv/ralphex" || \ + (echo "FAIL: /srv/ralphex not found or not executable" && exit 1) && \ + test -f /init.sh && echo "OK: /init.sh entrypoint exists" || \ + (echo "FAIL: /init.sh not found" && exit 1) && \ + test -f /srv/init.sh && echo "OK: /srv/init.sh credential copier exists" || \ + (echo "FAIL: /srv/init.sh not found" && exit 1) && \ + id app && echo "OK: app user exists" || \ + (echo "FAIL: app user not found" && exit 1) && \ + test -d /home/app/.cache/ms-playwright && echo "OK: Playwright browser cache exists" || \ + (echo "FAIL: Playwright browser cache missing at /home/app/.cache/ms-playwright" && exit 1) + secrets: inherit +``` + +- [ ] **Step 2: Commit** + +```bash +git add .github/workflows/build-ralphex-fe.yml +git commit -m "fix(ralphex-fe): update workflow for Debian-based image" +``` + +--- + +### Task 5: Update README.md + +**Files:** +- Rewrite: `ralphex-fe/README.md` + +Update to reflect: +- New Debian base (no longer Alpine/ralphex base) +- New tool list (Python 3, Playwright native, Go) +- ralphex wrapper usage with `RALPHEX_IMAGE` +- Source references for init scripts +- Removed: Alpine-specific Chromium/Playwright notes + +- [ ] **Step 1: Write updated README** + +The README should cover: +1. What the image is and what it's for (standalone image for ralphex docker-wrapper) +2. Features table with all tools +3. Usage with `RALPHEX_IMAGE` env var +4. Direct `docker run` usage +5. Building locally +6. Architecture/provenance section documenting where scripts come from +7. Runtime environment variables (`APP_UID`, `DOCKER_GID`, `TIME_ZONE`, `SKIP_HOME_CHOWN`, `INIT_QUIET`) +8. Image tags +9. Note on version tag format: `bun${VERSION}-hugo${VERSION}` (deviates from standalone convention of single primary version because this image bundles multiple tools with independent versions) + +- [ ] **Step 2: Commit** + +```bash +git add ralphex-fe/README.md +git commit -m "docs(ralphex-fe): update README for Debian-based image" +``` + +--- + +### Task 6: Local build test + +**Files:** None (verification only) + +- [ ] **Step 1: Build the image locally for current platform** + +```bash +docker build -t ralphex-fe:test ralphex-fe/ +``` + +- [ ] **Step 2: Verify all tools** + +```bash +docker run --rm --entrypoint sh ralphex-fe:test -c " + echo '=== Node ===' && node --version && + echo '=== Bun ===' && bun --version && + echo '=== Hugo ===' && hugo version && + echo '=== Go ===' && go version && + echo '=== Python ===' && python3 --version && + echo '=== Ralphex ===' && /srv/ralphex --version && + echo '=== Claude ===' && claude --version && + echo '=== Git ===' && git --version && + echo '=== Playwright ===' && npx playwright --version && + echo '=== App user ===' && id app +" +``` + +- [ ] **Step 3: Verify entrypoint works with APP_UID remapping** + +```bash +docker run --rm -e APP_UID=$(id -u) ralphex-fe:test whoami +# Expected: "app" (init.sh remaps UID then drops to app user via gosu) +``` + +- [ ] **Step 4: Verify workspace mount** + +```bash +docker run --rm -v $(pwd):/workspace -w /workspace ralphex-fe:test ls -la +# Expected: files from current directory, owned by app user's UID +``` diff --git a/ralphex-fe/Dockerfile b/ralphex-fe/Dockerfile index b501f95..e089f5f 100644 --- a/ralphex-fe/Dockerfile +++ b/ralphex-fe/Dockerfile @@ -1,70 +1,181 @@ ARG BUN_VERSION=1.3.9 +ARG DOCKER_VERSION=29.3.0 +ARG GO_VERSION=1.24.4 ARG HUGO_VERSION=0.156.0 -ARG RALPHEX_VERSION=0.20.0 +ARG PLAYWRIGHT_VERSION=1.58.2 -# Extend the ralphex base image which provides the ralphex binary at /srv/ralphex -# along with Claude Code, Codex, Node.js, git, ripgrep, and other development tools -FROM ghcr.io/umputun/ralphex:${RALPHEX_VERSION} +# ═════════════════════════════════════════════════════════════════════════════ +# Parallel download stages — BuildKit runs these concurrently +# ═════════════════════════════════════════════════════════════════════════════ -ARG BUN_VERSION -ARG HUGO_VERSION +# ── Go binary ──────────────────────────────────────────────────────────────── +# Pattern from: https://github.com/umputun/ralphex/blob/master/Dockerfile-go +FROM alpine:3.21 AS go-download +ARG GO_VERSION +RUN set -eux; \ + ARCH="$(uname -m | sed 's/x86_64/amd64/;s/aarch64/arm64/')"; \ + wget -qO- "https://go.dev/dl/go${GO_VERSION}.linux-${ARCH}.tar.gz" \ + | tar -xz -C /usr/local -# Platform architecture (automatically set by Docker based on build platform) +# ── Docker CLI static binary ──────────────────────────────────────────────── +# Matches ralphex base's docker-cli — only the CLI, no daemon/containerd/runc. +FROM alpine:3.21 AS docker-download +ARG DOCKER_VERSION +RUN set -eux; \ + ARCH="$(uname -m)"; \ + wget -qO- "https://download.docker.com/linux/static/stable/${ARCH}/docker-${DOCKER_VERSION}.tgz" \ + | tar -xz -C /tmp; \ + mv /tmp/docker/docker /usr/local/bin/docker + +# ── Hugo Extended with checksum verification ───────────────────────────────── +FROM alpine:3.21 AS hugo-download +ARG HUGO_VERSION ARG TARGETARCH +RUN set -eux; \ + wget -qO hugo.tar.gz \ + "https://github.com/gohugoio/hugo/releases/download/v${HUGO_VERSION}/hugo_extended_${HUGO_VERSION}_linux-${TARGETARCH}.tar.gz"; \ + wget -qO hugo_checksums.txt \ + "https://github.com/gohugoio/hugo/releases/download/v${HUGO_VERSION}/hugo_${HUGO_VERSION}_checksums.txt"; \ + EXPECTED=$(grep "hugo_extended_${HUGO_VERSION}_linux-${TARGETARCH}.tar.gz" hugo_checksums.txt | cut -d' ' -f1); \ + ACTUAL=$(sha256sum hugo.tar.gz | cut -d' ' -f1); \ + if [ "$EXPECTED" != "$ACTUAL" ]; then \ + echo "Hugo checksum mismatch: expected=$EXPECTED actual=$ACTUAL"; exit 1; \ + fi; \ + tar -xzf hugo.tar.gz -C /usr/local/bin/ hugo + +# ── Ralphex binary (latest from GitHub Releases) ──────────────────────────── +# Pattern from: claude-code/.devcontainer/Dockerfile +FROM alpine:3.21 AS ralphex-download +RUN apk add --no-cache curl jq +RUN set -eux; \ + ARCH="$(uname -m | sed 's/x86_64/amd64/;s/aarch64/arm64/')"; \ + VERSION=$(curl -fsSL https://api.github.com/repos/umputun/ralphex/releases/latest \ + | jq -r '.tag_name' | sed 's/^v//'); \ + curl -fsSL -o /tmp/ralphex.tar.gz \ + "https://github.com/umputun/ralphex/releases/download/v${VERSION}/ralphex_${VERSION}_linux_${ARCH}.tar.gz"; \ + tar -xzf /tmp/ralphex.tar.gz -C /usr/local/bin ralphex + +# ═════════════════════════════════════════════════════════════════════════════ +# Final image +# ═════════════════════════════════════════════════════════════════════════════ +FROM node:24-trixie-slim + +ARG BUN_VERSION +ARG PLAYWRIGHT_VERSION + +# ── System packages ────────────────────────────────────────────────────────── +# - ca-certificates: SSL/TLS for HTTPS connections +# - curl: downloading tools and installers +# - dumb-init: PID 1 init for proper signal handling (used by /init.sh entrypoint) +# - fzf: fuzzy finder (interactive selection in Claude Code and shell) +# - git: version control +# - gosu: drop privileges to app user (Debian equivalent of Alpine's su-exec) +# - jq: JSON processing +# - python3: scripting (Claude Code uses python for ad-hoc scripts) +# - ripgrep: fast code search +# - tzdata: timezone data for container timezone configuration +# - unzip: required by Bun install script on Linux +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 \ + dumb-init \ + fzf \ + git \ + gosu \ + jq \ + python3 \ + ripgrep \ + tzdata \ + unzip -# Install dependencies: -# - ca-certificates: Required for HTTPS connections -# - chromium: System Chromium browser for Playwright tests (Alpine/musl-native, avoids Playwright's -# bundled binary which requires glibc/Debian) -# - ttf-freefont: TrueType fonts required for Chromium text rendering in headless/screenshot mode -# - curl: Required by Bun install script -# - gcompat: glibc compatibility layer required for Hugo Extended binary (Alpine uses musl) -# - go: Required for Hugo Modules to download and manage dependencies (e.g., PaperMod theme) -# - unzip: Required by Bun install script on Linux -# - wget: Required to download Hugo Extended binary -RUN apk add --no-cache ca-certificates chromium curl gcompat go ttf-freefont unzip wget - -# Playwright environment variables: -# - PLAYWRIGHT_SKIP_BROWSER_DOWNLOAD: Prevents Playwright's postinstall from attempting to -# download its bundled Chromium binary (which requires glibc and is incompatible with Alpine/musl). -# - PLAYWRIGHT_CHROMIUM_EXECUTABLE_PATH: Custom env var that projects can reference in -# playwright.config.ts via executablePath. NOTE: Playwright does NOT read this automatically - -# you must wire it up manually (e.g. executablePath: process.env.PLAYWRIGHT_CHROMIUM_EXECUTABLE_PATH). -ENV PLAYWRIGHT_SKIP_BROWSER_DOWNLOAD=1 \ - PLAYWRIGHT_CHROMIUM_EXECUTABLE_PATH=/usr/bin/chromium-browser - -# Install Bun using official install script -# BUN_INSTALL sets the installation directory +# ── App user (matches umputun/baseimage convention) ────────────────────────── +# The ralphex docker-wrapper (ralphex-dk.sh) expects: +# - user "app" with home at /home/app +# - UID 1001 (remappable at runtime via APP_UID env var) +# - /srv owned by app +# - docker group GID 999 +# Source: https://github.com/umputun/baseimage/blob/master/base.alpine/Dockerfile +ENV APP_USER=app \ + APP_UID=1001 \ + DOCKER_GID=999 \ + TIME_ZONE=America/Chicago + +# Reserve GID 998 for ping to prevent conflicts (matches umputun/baseimage convention) +RUN groupadd -g 998 ping \ + && groupadd -g ${DOCKER_GID} docker \ + && useradd --no-log-init -m -s /bin/sh -u ${APP_UID} ${APP_USER} \ + && usermod -aG docker ${APP_USER} \ + && mkdir -p /srv /workspace \ + && chown -R ${APP_USER}:${APP_USER} /srv /workspace \ + && cp /usr/share/zoneinfo/${TIME_ZONE} /etc/localtime \ + && echo "${TIME_ZONE}" > /etc/timezone + +# ── Copy binaries from parallel download stages ───────────────────────────── +COPY --from=go-download /usr/local/go /usr/local/go +COPY --from=docker-download /usr/local/bin/docker /usr/local/bin/docker +COPY --from=hugo-download /usr/local/bin/hugo /usr/local/bin/hugo +COPY --from=ralphex-download /usr/local/bin/ralphex /srv/ralphex + +ENV GOROOT=/usr/local/go \ + GOPATH=/home/app/go +ENV PATH="${PATH}:${GOROOT}/bin:${GOPATH}/bin" + +# ── Entrypoint and init scripts ────────────────────────────────────────────── +# /init.sh — entrypoint handling APP_UID remapping and privilege drop +# Adapted from: https://github.com/umputun/baseimage/blob/master/base.alpine/files/init.sh +# /srv/init.sh — credential copier run before main command +# Source: https://github.com/umputun/ralphex/blob/master/scripts/internal/init-docker.sh +COPY files/init.sh /init.sh +COPY files/init-docker.sh /srv/init.sh +RUN chmod +x /init.sh /srv/init.sh + +WORKDIR /workspace + +# ── Playwright (Chromium only — headless testing for Claude Code via ralphex) ─ +# Only Chromium headless shell is needed; Firefox/WebKit skipped. +# 1. System deps (install-deps) — heavy, needs root +# 2. Browser binary (install --only-shell chromium) — Chromium headless shell only +RUN --mount=type=cache,target=/var/cache/apt,sharing=locked \ + --mount=type=cache,target=/var/lib/apt/lists,sharing=locked \ + npx -y playwright@${PLAYWRIGHT_VERSION} install-deps chromium +USER app +RUN npx -y playwright@${PLAYWRIGHT_VERSION} install --only-shell chromium +USER root + +# ── Claude Code CLI ────────────────────────────────────────────────────────── +# npm install (not native installer) to avoid rate-limiting in parallel Docker builds. +# See: claude-code/.devcontainer/Dockerfile for rationale. +RUN --mount=type=cache,target=/root/.npm \ + npm install -g @anthropic-ai/claude-code + +# ── Bun ────────────────────────────────────────────────────────────────────── ENV BUN_INSTALL=/usr/local/bun ENV PATH="$BUN_INSTALL/bin:$PATH" +# pipefail ensures curl failure is not masked by bash exit code +SHELL ["/bin/bash", "-o", "pipefail", "-c"] RUN curl -fsSL https://bun.sh/install | bash -s "bun-v${BUN_VERSION}" && \ bun --version +SHELL ["/bin/sh", "-c"] -# Install Hugo Extended by downloading pre-built binary with checksum verification -RUN set -eux; \ - wget -O hugo.tar.gz "https://github.com/gohugoio/hugo/releases/download/v${HUGO_VERSION}/hugo_extended_${HUGO_VERSION}_linux-${TARGETARCH}.tar.gz"; \ - wget -O hugo_checksums.txt "https://github.com/gohugoio/hugo/releases/download/v${HUGO_VERSION}/hugo_${HUGO_VERSION}_checksums.txt"; \ - EXPECTED_CHECKSUM=$(grep "hugo_extended_${HUGO_VERSION}_linux-${TARGETARCH}.tar.gz" hugo_checksums.txt | cut -d' ' -f1); \ - if [ -z "$EXPECTED_CHECKSUM" ]; then \ - echo "Error: Could not find checksum for hugo_extended_${HUGO_VERSION}_linux-${TARGETARCH}.tar.gz"; \ - exit 1; \ - fi; \ - ACTUAL_CHECKSUM=$(sha256sum hugo.tar.gz | cut -d' ' -f1); \ - if [ "$EXPECTED_CHECKSUM" != "$ACTUAL_CHECKSUM" ]; then \ - echo "Checksum verification failed for Hugo!"; \ - echo "Expected: $EXPECTED_CHECKSUM"; \ - echo "Actual: $ACTUAL_CHECKSUM"; \ - exit 1; \ - fi; \ - echo "Hugo checksum verified: $ACTUAL_CHECKSUM"; \ - tar -xzf hugo.tar.gz -C /usr/local/bin/ hugo; \ - rm hugo.tar.gz hugo_checksums.txt; \ - hugo version - -# OCI labels for container metadata and GitHub Package integration -LABEL org.opencontainers.image.source="https://github.com/gatezh/devcontainer-images" -LABEL org.opencontainers.image.description="Ralphex-fe: Frontend development image with Bun, Hugo Extended, Chromium, and Ralphex" -LABEL org.opencontainers.image.licenses="MIT" -LABEL org.opencontainers.image.title="ralphex-fe" -LABEL org.opencontainers.image.url="https://github.com/gatezh/devcontainer-images" +# ── Environment ────────────────────────────────────────────────────────────── +ENV RALPHEX_DOCKER=1 \ + USE_BUILTIN_RIPGREP=0 \ + PLAYWRIGHT_VERSION=${PLAYWRIGHT_VERSION} + +# Ralphex web dashboard port +EXPOSE 8080 + +# No final USER directive — /init.sh runs as root, handles APP_UID remapping, +# then drops to app user via gosu before executing CMD. +ENTRYPOINT ["/init.sh"] +CMD ["/srv/ralphex"] + +# ── OCI labels ─────────────────────────────────────────────────────────────── +LABEL org.opencontainers.image.source="https://github.com/gatezh/devcontainer-images" \ + org.opencontainers.image.description="Ralphex-fe: Frontend development image with Bun, Hugo Extended, Playwright, and Claude Code on Debian" \ + org.opencontainers.image.licenses="MIT" \ + org.opencontainers.image.title="ralphex-fe" \ + org.opencontainers.image.url="https://github.com/gatezh/devcontainer-images" diff --git a/ralphex-fe/README.md b/ralphex-fe/README.md index 9be6fb6..8451e18 100644 --- a/ralphex-fe/README.md +++ b/ralphex-fe/README.md @@ -1,113 +1,79 @@ # ralphex-fe -A standalone Docker image based on ralphex with Bun and Hugo Extended runtimes, designed for modern JavaScript/TypeScript development and static site generation. +Standalone Docker image for running [ralphex](https://github.com/umputun/ralphex) via its docker-wrapper script. Bundles the full frontend development toolchain needed for ralphex-powered projects. -This is a standalone Docker image, not a devcontainer configuration. It can be used directly with `docker run` or as a base for other images. +This is a standalone image, not a devcontainer. -## Features +## Tools -- **Ralphex Base** - Full-featured base image with common development tools -- **Node.js** - Included from ralphex base image (version provided by base) -- **Bun 1.3.9** - Fast JavaScript runtime, bundler, and package manager -- **Hugo Extended 0.155.3** - Full-featured static site generator with extended capabilities -- **Chromium** - System browser for headless end-to-end testing -- **Git** - Version control (included from base) -- **Zsh** - Modern shell (included from base) - -## Multiplatform Support - -This image is built for multiple architectures: -- `linux/amd64` (x86_64) -- `linux/arm64` (ARM64/Apple Silicon) +| Tool | Version | +|------|---------| +| Node.js | 24 (from base image) | +| Bun | 1.3.9 | +| Hugo Extended | 0.156.0 | +| Go | for Hugo Modules | +| Python 3 | system | +| Playwright + Chromium | native Debian | +| Claude Code CLI | latest | +| Ralphex | latest (GitHub Releases) | +| Git, ripgrep, jq, curl, wget | system | ## Usage -### Pull and Run - -```bash -# Pull the latest image -docker pull ghcr.io/gatezh/ralphex-fe:latest - -# Run interactively -docker run -it --rm ghcr.io/gatezh/ralphex-fe:latest - -# Run with a mounted project directory -docker run -it --rm -v $(pwd):/workspace -w /workspace ghcr.io/gatezh/ralphex-fe:latest -``` - -### Using Bun +### Via ralphex docker-wrapper ```bash -# Check Bun version -docker run --rm ghcr.io/gatezh/ralphex-fe:latest bun --version - -# Run a script -docker run --rm -v $(pwd):/workspace -w /workspace ghcr.io/gatezh/ralphex-fe:latest bun run index.ts - -# Install dependencies -docker run --rm -v $(pwd):/workspace -w /workspace ghcr.io/gatezh/ralphex-fe:latest bun install +export RALPHEX_IMAGE=ghcr.io/gatezh/ralphex-fe:latest +ralphex docs/plans/feature.md ``` -### Using Hugo +### Direct docker run ```bash -# Check Hugo version -docker run --rm ghcr.io/gatezh/ralphex-fe:latest hugo version - -# Start Hugo development server (with port mapping) -docker run --rm -v $(pwd):/workspace -w /workspace -p 1313:1313 ghcr.io/gatezh/ralphex-fe:latest hugo server --bind 0.0.0.0 - -# Build a Hugo site -docker run --rm -v $(pwd):/workspace -w /workspace ghcr.io/gatezh/ralphex-fe:latest hugo +docker run --rm \ + -e APP_UID=$(id -u) \ + -v ~/.claude:/mnt/claude:ro \ + -v $(pwd):/workspace \ + ghcr.io/gatezh/ralphex-fe:latest ``` ## Building Locally -### Simple Build - ```bash -docker build -t ralphex-fe ralphex-fe/ +docker build -t ralphex-fe:test ralphex-fe/ ``` -### Multiplatform Build with Docker Buildx - -```bash -docker buildx build \ - --platform linux/amd64,linux/arm64 \ - -t ghcr.io//ralphex-fe:0.11.0 \ - -t ghcr.io//ralphex-fe:latest \ - --push \ - ralphex-fe -``` +## Runtime Environment Variables -### Building with Custom Versions - -```bash -docker build \ - --build-arg BUN_VERSION=1.4.0 \ - --build-arg HUGO_VERSION=0.156.0 \ - -t ralphex-fe:custom \ - ralphex-fe/ -``` - -**Note:** The `--push` flag requires authentication to GitHub Container Registry: -```bash -echo $GITHUB_TOKEN | docker login ghcr.io -u --password-stdin -``` +| Variable | Default | Description | +|----------|---------|-------------| +| `APP_UID` | `1001` | Container user UID, remapped at startup to match host user | +| `DOCKER_GID` | `999` | Docker group GID for Docker socket access | +| `TIME_ZONE` | `America/Chicago` | Container timezone | +| `SKIP_HOME_CHOWN` | unset | Set to `1` to skip chown of `/home/app` at startup | +| `INIT_QUIET` | unset | Set to `1` to suppress `init.sh` log output | ## Image Tags -- `latest` - Most recent build -- `{ralphex-version}` - Version-specific tag (e.g., `0.11.0`) +- `latest` — always included +- `bun{VERSION}-hugo{VERSION}` — version-specific tag (e.g., `bun1.3.9-hugo0.156.0`) + +Note: this image deviates from the standalone convention of a single primary version tag because it bundles multiple independently-versioned tools. -## Version Information +## Architecture / Provenance -The image uses specific versions defined as build arguments in the Dockerfile: +| File / Pattern | Source | +|----------------|--------| +| `files/init.sh` | Adapted from [umputun/baseimage](https://github.com/umputun/baseimage/blob/master/base.alpine/files/init.sh) for Debian (gosu instead of su-exec, groupadd/groupdel instead of addgroup/delgroup) | +| `files/init-docker.sh` | From [umputun/ralphex](https://github.com/umputun/ralphex/blob/master/scripts/internal/init-docker.sh) (credential copying from mounted volumes) | +| Ralphex binary install | Pattern from `claude-code/.devcontainer/Dockerfile` | +| Playwright install | Pattern from `claude-code/.devcontainer/Dockerfile` | +| Hugo Extended install | Kept from previous `ralphex-fe/Dockerfile` | -- **Bun Version**: `1.3.9` (via `BUN_VERSION` build arg) -- **Hugo Version**: `0.155.3` (via `HUGO_VERSION` build arg) -- **Base Image**: `ghcr.io/umputun/ralphex:0.11.0` (via `RALPHEX_VERSION` build arg) +## Why Debian (not Alpine) -## License +This image migrated from Alpine to Debian because: -This image configuration is part of the devcontainer-images repository. +- Hugo Extended works natively — no `gcompat` shim needed +- Playwright's bundled Chromium works out of the box — no `PLAYWRIGHT_SKIP_BROWSER_DOWNLOAD` workaround required +- Larger package ecosystem via `apt` diff --git a/ralphex-fe/files/init-docker.sh b/ralphex-fe/files/init-docker.sh new file mode 100755 index 0000000..15d7eb9 --- /dev/null +++ b/ralphex-fe/files/init-docker.sh @@ -0,0 +1,35 @@ +#!/bin/sh +# Init script for ralphex docker container. +# The entrypoint (/init.sh) runs /srv/init.sh if it exists before the main command. +# +# Source: https://github.com/umputun/ralphex/blob/master/scripts/internal/init-docker.sh +# Copied as-is from umputun/ralphex. Check upstream for updates. + +# copy only essential claude files (not the entire 2GB directory) +if [ -d /mnt/claude ]; then + mkdir -p /home/app/.claude + # copy config files only (not cache, history, debug, todos, etc.) + for f in .credentials.json settings.json settings.local.json CLAUDE.md format.sh; do + [ -e "/mnt/claude/$f" ] && cp -L "/mnt/claude/$f" "/home/app/.claude/$f" 2>/dev/null || true + done + # copy essential directories (symlinked in dotfiles setups) + for d in commands skills hooks agents plugins; do + [ -d "/mnt/claude/$d" ] && cp -rL "/mnt/claude/$d" "/home/app/.claude/" 2>/dev/null || true + done + chown -R app:app /home/app/.claude +fi + +# copy credentials extracted from macOS keychain (mounted separately) +if [ -f /mnt/claude-credentials.json ]; then + mkdir -p /home/app/.claude + cp /mnt/claude-credentials.json /home/app/.claude/.credentials.json + chown -R app:app /home/app/.claude + chmod 600 /home/app/.claude/.credentials.json +fi + +# copy codex credentials if mounted +if [ -d /mnt/codex ]; then + mkdir -p /home/app/.codex + cp -rL /mnt/codex/* /home/app/.codex/ 2>/dev/null || true + chown -R app:app /home/app/.codex +fi diff --git a/ralphex-fe/files/init.sh b/ralphex-fe/files/init.sh new file mode 100755 index 0000000..f49b413 --- /dev/null +++ b/ralphex-fe/files/init.sh @@ -0,0 +1,67 @@ +#!/usr/bin/dumb-init /bin/sh + +# Entrypoint for ralphex-fe container. +# Adapted from umputun/baseimage (base.alpine/files/init.sh) for Debian. +# Changes: gosu instead of su-exec, groupadd/groupdel instead of addgroup/delgroup, +# dumb-init path adjusted for Debian. +# Source: https://github.com/umputun/baseimage/blob/master/base.alpine/files/init.sh + +uid=$(id -u) + +if [ "${uid}" -eq 0 ]; then + [ "${INIT_QUIET}" != "1" ] && echo "init container" + + # set container's time zone + if [ -f "/usr/share/zoneinfo/${TIME_ZONE}" ]; then + cp "/usr/share/zoneinfo/${TIME_ZONE}" /etc/localtime + echo "${TIME_ZONE}" >/etc/timezone + [ "${INIT_QUIET}" != "1" ] && echo "set timezone ${TIME_ZONE} ($(date))" + fi + + # set UID for user app + if [ "${APP_UID}" != "1001" ]; then + [ "${INIT_QUIET}" != "1" ] && echo "set custom APP_UID=${APP_UID}" + sed -i "s/:1001:1001:/:${APP_UID}:${APP_UID}:/g" /etc/passwd + sed -i "s/:1001:/:${APP_UID}:/g" /etc/group + else + [ "${INIT_QUIET}" != "1" ] && echo "custom APP_UID not defined, using default uid=1001" + fi + + # set GID for docker group + if [ "${DOCKER_GID}" != "999" ]; then + [ "${INIT_QUIET}" != "1" ] && echo "set custom DOCKER_GID=${DOCKER_GID}" + existing_group=$(getent group "${DOCKER_GID}" | cut -d: -f1) + if [ -n "${existing_group}" ] && [ "${existing_group}" != "docker" ]; then + [ "${INIT_QUIET}" != "1" ] && echo "GID ${DOCKER_GID} used by '${existing_group}', adding app to it" + usermod -aG "${existing_group}" app || { echo "error: failed to add app to group '${existing_group}'"; exit 1; } + else + groupdel docker 2>/dev/null || true + groupadd -g "${DOCKER_GID}" docker || { echo "error: failed to create docker group with GID=${DOCKER_GID}"; exit 1; } + usermod -aG docker app || { echo "error: failed to add app to docker group"; exit 1; } + fi + else + [ "${INIT_QUIET}" != "1" ] && echo "custom DOCKER_GID not defined, using default gid=999" + fi + + chown -R app:app /srv + if [ "${SKIP_HOME_CHOWN}" != "1" ]; then + chown -R app:app /home/app + fi +fi + +if [ -f "/srv/init.sh" ]; then + [ "${INIT_QUIET}" != "1" ] && echo "execute /srv/init.sh" + chmod +x /srv/init.sh + /srv/init.sh + if [ "$?" -ne "0" ]; then + echo "/srv/init.sh failed" + exit 1 + fi +fi + +[ "${INIT_QUIET}" != "1" ] && echo "execute $*" +if [ "${uid}" -eq 0 ]; then + exec gosu app "$@" +else + exec "$@" +fi