Skip to content

feat: add Docker image build and push to GHCR on release#656

Merged
kevincodex1 merged 2 commits into
Gitlawb:mainfrom
Fexiven:main
Apr 14, 2026
Merged

feat: add Docker image build and push to GHCR on release#656
kevincodex1 merged 2 commits into
Gitlawb:mainfrom
Fexiven:main

Conversation

@Fexiven
Copy link
Copy Markdown
Contributor

@Fexiven Fexiven commented Apr 13, 2026

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

  • Added a multi-stage Dockerfile using node: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
  • Added .dockerignore to keep the build context lean
  • Extended .github/workflows/release.yml with a docker job that builds and pushes to ghcr.io/gitlawb/openclaude with semver tags (:latest, :<version>, :<major>.<minor>) when release-please creates a release

Impact

  • user-facing impact: Users can now pull and run OpenClaude directly via docker pull ghcr.io/gitlawb/openclaude without needing Node.js or Bun installed locally
  • developer/maintainer impact: Docker image is built and published automatically on release — no manual steps required. Uses GHA layer caching for fast rebuilds.

Testing

  • bun run build
  • bun run smoke
  • focused tests:

Notes

  • provider/model path tested:
  • screenshots attached (if UI changed):
  • follow-up work or known limitations:

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
kevincodex1 previously approved these changes Apr 13, 2026
Copy link
Copy Markdown
Contributor

@kevincodex1 kevincodex1 left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

definitely need this! Thank. you @Fexiven ! please have a look @gnanam1990 @Vasanthdev2004

@kevincodex1
Copy link
Copy Markdown
Contributor

@gnanam1990 @Vasanthdev2004 kindly have a look when you have time

@kevincodex1 kevincodex1 mentioned this pull request Apr 13, 2026
Vasanthdev2004
Vasanthdev2004 previously approved these changes Apr 13, 2026
Copy link
Copy Markdown
Collaborator

@Vasanthdev2004 Vasanthdev2004 left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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, runs bun run build, then prunes to production deps only. Layer caching optimized — package.json + bun.lock copied before source.
  • Stage 2 (runtime): node:22-slim, copies only runtime artifacts (dist/cli.mjs, bin/, node_modules/, package.json, README.md). Installs git for CLI tool operations. Entry point ["node", "dist/cli.mjs"] is correct — single ESM bundle, package.json has "type": "module".

All 5 pinned GHA SHAs verified against their version tags ✅:

  • actions/checkout@11bd719 → v4.2.2
  • docker/setup-buildx-action@b5ca514 → v3.10.0
  • docker/login-action@74a5d14 → v3.4.0
  • docker/metadata-action@902fa8e → v5.7.0
  • docker/build-push-action@14487ce → v6.16.0

✅ Release workflow (release.yml)

  • docker job has needs: 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:write is the minimum needed for GHCR push. No external secrets required (uses auto-provisioned GITHUB_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/appuser

This 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 }} --help

This 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.

@kevincodex1
Copy link
Copy Markdown
Contributor

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
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/appuser
This 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 }} --help
    This catches issues like missing shared libraries, broken entrypoints, or runtime import errors before the image reaches users.

Run the container as a non-root appuser to reduce blast radius.
Add a smoke test step that runs --version before pushing to GHCR.
@Fexiven Fexiven dismissed stale reviews from Vasanthdev2004 and kevincodex1 via 0490129 April 14, 2026 07:22
@Fexiven
Copy link
Copy Markdown
Contributor Author

Fexiven commented Apr 14, 2026

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 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/appuser This 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 }} --help
    This catches issues like missing shared libraries, broken entrypoints, or runtime import errors before the image reaches users.

Thank you for the feedback, i implemented the suggestions

Copy link
Copy Markdown
Collaborator

@Vasanthdev2004 Vasanthdev2004 left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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 as USER, with WORKDIR and HOME set to /home/appuser
  • Smoke test in CI: docker run --rm openclaude:smoke --version runs after building, before push

✅ Everything checks out

Dockerfile:

  • Multi-stage build (node:22-slim) — clean and minimal
  • bun install --frozen-lockfile for reproducible builds
  • rm -rf node_modules && bun install --frozen-lockfile --production to prune devDependencies
  • Only runtime artifacts copied to final image (dist/cli.mjs, bin/, node_modules, package.json)
  • git installed for CLI tool operations that depend on it
  • apt-get lists 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):

  1. The Dockerfile uses bun@1.3.11 specifically. This will go stale over time. Consider either pinning to bun@latest (less reproducible) or adding a comment noting where to update. Very minor.

  2. 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: true but 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.

  3. ENTRYPOINT ["node", "/app/dist/cli.mjs"] — the absolute path /app/dist/cli.mjs works 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.

@kevincodex1
Copy link
Copy Markdown
Contributor

looks great lets merge this

@kevincodex1 kevincodex1 merged commit 658d076 into Gitlawb:main Apr 14, 2026
1 check passed
@kevincodex1
Copy link
Copy Markdown
Contributor

docker failed to build. please see this issue @Fexiven

#681

C1ph3r404 pushed a commit to C1ph3r404/openclaude that referenced this pull request Apr 29, 2026
* 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.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants