Skip to content

Add Docker support and Render Blueprint#2

Merged
aaronbrethorst merged 3 commits into
mainfrom
docker
May 9, 2026
Merged

Add Docker support and Render Blueprint#2
aaronbrethorst merged 3 commits into
mainfrom
docker

Conversation

@aaronbrethorst
Copy link
Copy Markdown
Member

@aaronbrethorst aaronbrethorst commented May 9, 2026

Summary

  • Multi-stage Dockerfile (Go builder → small Alpine runtime, non-root UID 65532, ~33 MB) with BuildKit cache mounts, pure-Go build (modernc.org/sqlite, no cgo), and a HEALTHCHECK that honors HOOKS_LISTEN_ADDR.
  • render.yaml Blueprint provisions a Docker web service plus a 1 GiB persistent disk at /data. numInstances: 1 enforces the single-writer SQLite invariant.
  • New ./dockertest package, gated by the docker build tag so go test ./... stays fast — covers --help, non-root, both binaries, init scaffold, /healthz + /readyz, the Docker HEALTHCHECK directive, and restart-with-persisted-state.
  • README + docs/quickstart.md gain container and Render walkthroughs.

Test plan

  • make docker-test (7 tests pass; ~10s with cache, ~50s cold)
  • make test, make lint, go vet ./...
  • Built hooks:dev locally; ran end-to-end with a real signed Render webhook (HTTP 202), bad signature (HTTP 401), replay dedupe (HTTP 200)
  • Confirmed SQLite + hooks.yaml persist to host-mounted volume; hooksctl reachable via docker exec

Review notes

A few things were surfaced by review and intentionally deferred:

  • Render PORT contract. render.yaml hardcodes HOOKS_LISTEN_ADDR=:10000 to match Render's current Docker PORT default. The cleaner fix is to teach internal/config to honor $PORT directly — out of scope for this PR; happy to do as a follow-up.
  • Coverage gaps left for follow-up: SIGTERM-graceful-shutdown, hooksctl-against-running-server, and the host-side 0o755 permissions edge case (operators following the README literally without chmod-ing their data dir).
  • Digest-pinning. golang:1.26-alpine and alpine:3.20 are pinned by tag, not by sha256 digest. Worth doing if reproducibility becomes a concern.
  • tzdata not installed. No callers of time.LoadLocation in the tree; if a future feature needs IANA zones, add it back.

Summary by CodeRabbit

  • New Features

    • Added Docker containerization with multi-stage builds, health checks, and non-root runtime.
    • Added Render deployment configuration for single-instance hosted deployment.
    • Added Docker build and run Makefile targets.
  • Documentation

    • Updated README with Docker usage instructions.
    • Enhanced quickstart with container and Render deployment alternatives.
  • Tests

    • Added Docker integration test suite validating image build, binaries, health endpoints, and data persistence.

Review Change Stack

Multi-stage Dockerfile (Go builder → small Alpine runtime), non-root
UID 65532, /data volume for the SQLite DB and hooks.yaml. Pure-Go build
with BuildKit cache mounts; HEALTHCHECK honors HOOKS_LISTEN_ADDR so it
matches whatever port the binary is bound to. Image is ~33 MB and ships
both `hooks` and `hooksctl` so operators can manage tokens via
`docker exec`.

A `render.yaml` Blueprint provisions a Docker web service plus a 1 GiB
persistent disk at /data. `numInstances: 1` enforces the single-writer
SQLite invariant from CLAUDE.md.

Tests live in `./dockertest`, gated by the `docker` build tag so
`go test ./...` stays fast. Coverage: --help output, non-root UID, both
binaries shipped, init scaffold on a mounted volume, /healthz + /readyz
serve, Docker HEALTHCHECK directive reaches healthy, and restart-with-
persisted-state survives across container lifetimes.

README and docs/quickstart.md gain container + Render walkthroughs.
@coderabbitai
Copy link
Copy Markdown

coderabbitai Bot commented May 9, 2026

Warning

Rate limit exceeded

@aaronbrethorst has exceeded the limit for the number of commits that can be reviewed per hour. Please wait 39 minutes and 59 seconds before requesting another review.

You’ve run out of usage credits. Purchase more in the billing tab.

⌛ How to resolve this issue?

After the wait time has elapsed, a review can be triggered using the @coderabbitai review command as a PR comment. Alternatively, push new commits to this PR.

We recommend that you space out your commits to avoid hitting the rate limit.

🚦 How do rate limits work?

CodeRabbit enforces hourly rate limits for each developer per organization.

Our paid plans have higher rate limits than the trial, open-source and free plans. In all cases, we re-allow further reviews after a brief timeout.

Please see our FAQ for further information.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro Plus

Run ID: 70f8d256-59f1-47e4-bc0b-e65deb7071c7

📥 Commits

Reviewing files that changed from the base of the PR and between a296efd and 0b2588c.

📒 Files selected for processing (3)
  • Dockerfile
  • dockertest/docker_test.go
  • internal/pubsub/notifier.go
📝 Walkthrough

Walkthrough

This PR adds Docker containerization to the hooks relay, enabling local container development and production deployment on Render. It includes a two-stage Dockerfile, Makefile targets for building and running containers, integration tests validating image correctness, a Render Blueprint for one-click deployment, and updated documentation.

Changes

Docker Containerization of Hooks Relay

Layer / File(s) Summary
Container Image Definition
Dockerfile, .dockerignore
Dockerfile uses golang:1.26-alpine builder stage with CGO_ENABLED=0 and -trimpath to compile hooks and hooksctl binaries, then copies them into alpine:3.20 runtime as non-root user hooks (UID/GID 65532) with /data volume, HOOKS_DATABASE_URL pointed to /data/hooks.db, port 8080 exposed, and HEALTHCHECK polling /healthz. .dockerignore excludes build outputs, SQLite artifacts, local config, VCS/editor/OS noise, and documentation to optimize build context.
Local Build and Run Targets
Makefile
Makefile adds docker-build target to build the image, docker-run target to create ./hooks-data volume and run the container with port and environment forwarding, and docker-test target to run tests with the docker build tag; introduces DOCKER, DOCKER_IMAGE, and DOCKER_TAG variables.
Render Deployment Configuration
render.yaml
render.yaml Blueprint defines a single hooks web service using Docker runtime on Render's starter plan with autoDeploy enabled, single instance, persistent disk at /data for SQLite, health check at /readyz, and environment variables for database URL, listen address on :10000, and non-synced public URL and webhook secret.
Integration Test Suite
dockertest/doc.go, dockertest/docker_test.go
dockertest package with build-tagged tests that build the image per-process and validate: help output, non-root execution, both binary presence, hooks init scaffolding into mounted /data, /healthz and /readyz endpoints, Dockerfile HEALTHCHECK reaching healthy state, and container restart persistence using the same /data mount across two runs; includes helpers for waiting on /healthz, docker cleanup, and port resolution.
Documentation
README.md, docs/quickstart.md
README.md adds "Running it under Docker" section with local build/run commands and Render Blueprint note. docs/quickstart.md extends quickstart with "3a. Or run it as a container" and "3b. Or deploy to Render with the Blueprint" subsections covering Docker run/init commands, environment defaults, volume/UID behavior, Render provisioning, and bootstrap flow.

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~25 minutes

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 42.86% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title 'Add Docker support and Render Blueprint' accurately captures the main changes: Dockerfile addition, Render deployment configuration, and Docker-related tooling.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch docker

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

Copy link
Copy Markdown

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 1

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@dockertest/docker_test.go`:
- Around line 104-113: The test currently prints raw command output from
exec.Command(...).CombinedOutput() in t.Fatalf calls (variables out and err)
which may leak one-time admin tokens; change the failure handling in the
docker_test.go block that runs "docker ... init" so it does not include raw out
in logs: on error call t.Fatalf with a generic message including err but not
out, and when asserting strings.Contains fails compute a safe digest (e.g.,
sha256.Sum256(out)) and include only a short, non-secret 4-byte hex prefix of
that digest in the failure message instead of the full output; update the two
t.Fatalf sites referencing out and err accordingly and keep the strings.Contains
check logic the same.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro Plus

Run ID: 4223db7e-6c56-4e4e-81aa-a681a809ff1a

📥 Commits

Reviewing files that changed from the base of the PR and between 4b0b748 and a296efd.

📒 Files selected for processing (8)
  • .dockerignore
  • Dockerfile
  • Makefile
  • README.md
  • dockertest/doc.go
  • dockertest/docker_test.go
  • docs/quickstart.md
  • render.yaml

Comment thread dockertest/docker_test.go
…l race

Publish previously snapshotted the subscriber channels under the mutex,
released it, then performed non-blocking sends. Between the unlock and
the send, a concurrent Unsubscribe could close one of those channels —
sending on a closed channel panics, even with select/default. The race
detector flagged it on a busy CI runner running TestSubscribeReplayThenLive
under -race.

Hold the mutex for the duration of Publish. Each iteration is an O(1)
non-blocking send, so the lock is held only for the time it takes to walk
the subscriber map; the "publishers never block" semantics are preserved.

Verified with `go test -race -count=20 ./internal/pubsub/... ./internal/subscribe/...`.
- Dockerfile: replace `COPY . .` with explicit `COPY cmd/` + `COPY internal/`
  (SonarQube docker:S6470 hotspot: defence-in-depth against shipping
  secrets/.git/state, beyond what .dockerignore filters).
- dockertest/docker_test.go: redact `hooks init` output from t.Fatalf
  messages — the init command prints a one-time admin token and a
  bootstrap signup code, both of which would otherwise land in CI
  logs that are public on PRs (CodeRabbit feedback; matches the
  CLAUDE.md "no plaintext secrets in logs" policy).
- dockertest/docker_test.go: inline the `uid := strings.TrimSpace(...)`
  short-decl since `uid` was used only in the comparison (SonarQube
  godre:S8193 code smell).
@sonarqubecloud
Copy link
Copy Markdown

sonarqubecloud Bot commented May 9, 2026

@aaronbrethorst aaronbrethorst merged commit 08cac0a into main May 9, 2026
7 checks passed
@aaronbrethorst aaronbrethorst deleted the docker branch May 9, 2026 21:44
aaronbrethorst added a commit that referenced this pull request May 9, 2026
fix(config): honor $PORT directly (PR #2 follow-up)
aaronbrethorst added a commit that referenced this pull request May 9, 2026
test(docker): close SIGTERM, hooksctl-vs-server, 0o755 coverage gaps from PR #2
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.

1 participant