feat: add Docker image build and push to GHCR on release#656
Conversation
Add Dockerfile (multi-stage build with node:22-slim) and a new docker job in the release workflow that builds and pushes to ghcr.io when release-please creates a tag.
kevincodex1
left a comment
There was a problem hiding this comment.
definitely need this! Thank. you @Fexiven ! please have a look @gnanam1990 @Vasanthdev2004
|
@gnanam1990 @Vasanthdev2004 kindly have a look when you have time |
Vasanthdev2004
left a comment
There was a problem hiding this comment.
Review: Add Docker image build and push to GHCR on release
Reviewed on head 22d18ea. CI green ✅. 3 new files, +103/-0.
✅ Dockerfile
Multi-stage build is clean and follows best practices:
- Stage 1 (build):
node:22-slim+ Bun 1.3.11, installs all deps, runsbun run build, then prunes to production deps only. Layer caching optimized —package.json+bun.lockcopied before source. - Stage 2 (runtime):
node:22-slim, copies only runtime artifacts (dist/cli.mjs,bin/,node_modules/,package.json,README.md). Installsgitfor CLI tool operations. Entry point["node", "dist/cli.mjs"]is correct — single ESM bundle,package.jsonhas"type": "module".
All 5 pinned GHA SHAs verified against their version tags ✅:
actions/checkout@11bd719→ v4.2.2docker/setup-buildx-action@b5ca514→ v3.10.0docker/login-action@74a5d14→ v3.4.0docker/metadata-action@902fa8e→ v5.7.0docker/build-push-action@14487ce→ v6.16.0
✅ Release workflow (release.yml)
dockerjob hasneeds: release-please+if: release_created— only fires on actual releases, not every main push- Checkouts the release tag (
ref: ${{ needs.release-please.outputs.tag_name }}), not main HEAD — correct permissions: { contents: read, packages: write }— scoped to the docker job only.packages:writeis the minimum needed for GHCR push. No external secrets required (uses auto-provisionedGITHUB_TOKEN)- Semver tagging:
{{version}},{{major}}.{{minor}},latest— standard pattern - GHA layer caching (
cache-from: type=gha, cache-to: type=gha,mode=max) — fast rebuilds on subsequent releases
✅ .dockerignore
Excludes node_modules, dist, .git, .env*, coverage, docs, .github, *.md (except README.md). Keeps the build context lean.
🔶 Non-blocking suggestion: run as non-root
The Docker image runs as root by default. For a CLI tool that executes shell commands (Bash tool, Monitor tool), this amplifies the blast radius if a vulnerability is exploited inside the container. Consider adding:
RUN groupadd --gid 1000 appuser && useradd --uid 1000 --gid appuser --shell /bin/bash --create-home appuser
USER appuser
WORKDIR /home/appuser
ENV HOME=/home/appuserThis isn't a merge blocker — many CLI tool Docker images run as root — but it's worth a follow-up issue.
🔶 Non-blocking suggestion: add a HEALTHCHECK or at least test the image in CI
The docker job builds and pushes, but never actually runs the image to verify it starts. Consider adding a smoke step:
- name: Smoke test
run: docker run --rm ${{ steps.meta.outputs.tags }} --helpThis catches issues like missing shared libraries, broken entrypoints, or runtime import errors before the image reaches users.
Verdict: Approve-ready ✅
Clean Dockerfile, correct release workflow, all SHAs verified, permissions properly scoped. Two non-blocking suggestions (non-root user, smoke test) can be follow-ups. Kevin already approved. No blockers.
|
hello @Fexiven thank you for this PR just one more thing before we can merge this can you do the suggestions from @Vasanthdev2004 . 🔶 Non-blocking suggestion: run as non-root RUN groupadd --gid 1000 appuser && useradd --uid 1000 --gid appuser --shell /bin/bash --create-home appuser 🔶 Non-blocking suggestion: add a HEALTHCHECK or at least test the image in CI
|
Run the container as a non-root appuser to reduce blast radius. Add a smoke test step that runs --version before pushing to GHCR.
0490129
Thank you for the feedback, i implemented the suggestions |
Vasanthdev2004
left a comment
There was a problem hiding this comment.
Re-review: PR #656 — Docker image build + GHCR push (head 0490129)
CI green ✅. 3 files, +120/-0. Thanks for the updates, @Fexiven — both previous non-blocking suggestions are now implemented:
- ✅ Non-root user:
appuser(uid 1000) created and set asUSER, withWORKDIRandHOMEset to/home/appuser - ✅ Smoke test in CI:
docker run --rm openclaude:smoke --versionruns after building, before push
✅ Everything checks out
Dockerfile:
- Multi-stage build (node:22-slim) — clean and minimal
bun install --frozen-lockfilefor reproducible buildsrm -rf node_modules && bun install --frozen-lockfile --productionto prune devDependencies- Only runtime artifacts copied to final image (dist/cli.mjs, bin/, node_modules, package.json)
gitinstalled for CLI tool operations that depend on itapt-getlists cleaned up (rm -rf /var/lib/apt/lists/*)- Non-root user with proper HOME/WORKDIR
.dockerignore:
- Comprehensive — excludes node_modules, dist, .git, .env, coverage, docs, python, etc.
- Keeps README.md for license/attribution
release.yml docker job:
- Gated on
release-please.outputs.release_created == 'true'— only fires on actual releases ✅ - All 5 GitHub Actions use pinned commit SHAs — all verified against their tag refs ✅
permissions: contents: read, packages: write— least-privilege ✅- Checks out the release tag (not the branch) for reproducible images ✅
- Semver tags (version + major.minor) +
latest✅ - Build-and-load → smoke test → build-and-push flow (two builds but ensures push uses same context) ✅
- GHA cache for layer reuse ✅
Minor observations (not blockers):
-
The Dockerfile uses
bun@1.3.11specifically. This will go stale over time. Consider either pinning tobun@latest(less reproducible) or adding a comment noting where to update. Very minor. -
The "build and load locally" step builds the image, then the "build and push" step builds it again. This means two full builds per release. An alternative would be to build once with
load: true, push: truebut that's not how the Docker actions work (load and push are mutually exclusive). The current approach is correct and safe — the second build uses GHA cache so it's fast. -
ENTRYPOINT ["node", "/app/dist/cli.mjs"]— the absolute path/app/dist/cli.mjsworks but is hardcoded. If the build output path changes, this needs updating. Fine for now.
Verdict: Approve-ready ✅
Clean implementation. Both previous suggestions addressed. All pinned SHAs verified. Ready to merge.
|
looks great lets merge this |
* feat: add Docker image build and push to GHCR on release Add Dockerfile (multi-stage build with node:22-slim) and a new docker job in the release workflow that builds and pushes to ghcr.io when release-please creates a tag. * feat(docker): run as non-root user and add smoke test Run the container as a non-root appuser to reduce blast radius. Add a smoke test step that runs --version before pushing to GHCR.
Add Dockerfile (multi-stage build with node:22-slim) and a new docker job in the release workflow that builds and pushes to ghcr.io when release-please creates a tag.
Summary
Dockerfileusingnode:22-slim— builds with Bun in stage 1, copies only runtime artifacts (dist/cli.mjs,bin/,node_modules,package.json) into a slim final image with git installed.dockerignoreto keep the build context lean.github/workflows/release.ymlwith adockerjob that builds and pushes toghcr.io/gitlawb/openclaudewith semver tags (:latest,:<version>,:<major>.<minor>) when release-please creates a releaseImpact
docker pull ghcr.io/gitlawb/openclaudewithout needing Node.js or Bun installed locallyTesting
bun run buildbun run smokeNotes