Add Render deployment (Docker cron job + one-off jobs)#1
Conversation
Deploy oba-validator to Render as a Docker cron job scheduled for Feb 29
(0 2 29 2 *) so it effectively never runs on its own; real validations are
launched on demand as Render one-off jobs.
Render splits a job's startCommand on whitespace with no shell and uses the
first token as the executable, so the JSON config is base64-encoded into one
token and decoded by entrypoint.sh (mirroring obacloud's existing one-off-job
pattern). No API key is baked in — keys are per-server and ride inside the
config.
- Dockerfile: multi-stage pure-Go build, alpine + ca-certificates
- entrypoint.sh: base64-decodes the config; raw JSON ("{"-prefixed) passed through
- render.yaml: Blueprint for the Docker cron job (no env vars by design)
- .dockerignore, Makefile docker-build target, README + design spec
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude <noreply@anthropic.com>
📝 WalkthroughWalkthroughThis PR adds containerized deployment support for oba-validator on Render. It introduces a multi-stage Docker build, an entrypoint wrapper that base64-decodes runtime-supplied JSON config, a Render cron job definition, a Makefile docker-build target, and comprehensive deployment documentation explaining how one-off jobs invoke the validator with config passed as an encoded command-line argument. ChangesRender Deployment Support
Estimated code review effort🎯 2 (Simple) | ⏱️ ~12 minutes 🚥 Pre-merge checks | ✅ 5✅ Passed checks (5 passed)
✏️ Tip: You can configure your own custom pre-merge checks in the settings. ✨ Finishing Touches📝 Generate docstrings
🧪 Generate unit tests (beta)
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. Comment |
There was a problem hiding this comment.
Actionable comments posted: 1
🧹 Nitpick comments (1)
docs/superpowers/specs/2026-05-25-render-deployment-design.md (1)
51-53: 💤 Low valueAdd language identifier to fenced code block.
The fenced code block at line 51 should specify a language identifier for better Markdown tooling compatibility and syntax highlighting.
📝 Proposed fix
-``` +```plaintext /app/entrypoint.sh <base64-of-compact-config-json></details> <details> <summary>🤖 Prompt for AI Agents</summary>Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.In
@docs/superpowers/specs/2026-05-25-render-deployment-design.mdaround lines
51 - 53, Add a language identifier to the fenced code block that contains
"/app/entrypoint.sh " so Markdown tooling can
provide proper highlighting; update the opening fence fromto something likeplaintext or ```bash to indicate the content type and keep the rest of the
block unchanged.</details> </blockquote></details> </blockquote></details> <details> <summary>🤖 Prompt for all review comments with AI agents</summary>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@Dockerfile:
- Around line 1-34: Add a non-root runtime user and switch to it in the
Dockerfile: create a user/group (e.g., obauser), chown /app and the binary
(/app/oba-validator and /app/entrypoint.sh) to that user, ensure entrypoint.sh
remains executable, and add USER obauser before ENTRYPOINT so the container does
not run as root; reference the Dockerfile stages that COPY --from=builder
/build/oba-validator and COPY entrypoint.sh /app/entrypoint.sh and ensure
permissions/ownership changes happen after those COPY steps.
Nitpick comments:
In@docs/superpowers/specs/2026-05-25-render-deployment-design.md:
- Around line 51-53: Add a language identifier to the fenced code block that
contains "/app/entrypoint.sh " so Markdown
tooling can provide proper highlighting; update the opening fence fromto something likeplaintext or ```bash to indicate the content type and keep the
rest of the block unchanged.</details> <details> <summary>🪄 Autofix (Beta)</summary> Fix all unresolved CodeRabbit comments on this PR: - [ ] <!-- {"checkboxId": "4b0d0e0a-96d7-4f10-b296-3a18ea78f0b9"} --> Push a commit to this branch (recommended) - [ ] <!-- {"checkboxId": "ff5b1114-7d8c-49e6-8ac1-43f82af23a33"} --> Create a new PR with the fixes </details> --- <details> <summary>ℹ️ Review info</summary> <details> <summary>⚙️ Run configuration</summary> **Configuration used**: Organization UI **Review profile**: CHILL **Plan**: Pro Plus **Run ID**: `2a7941b0-0524-42ae-b356-fb803373f093` </details> <details> <summary>📥 Commits</summary> Reviewing files that changed from the base of the PR and between adef7fef357c7701e9b2a590a7ce2a6ccc5b25e7 and 8669417b9a1c7460b889a7f9a02150828db6e749. </details> <details> <summary>📒 Files selected for processing (7)</summary> * `.dockerignore` * `Dockerfile` * `Makefile` * `README.md` * `docs/superpowers/specs/2026-05-25-render-deployment-design.md` * `entrypoint.sh` * `render.yaml` </details> </details> <!-- This is an auto-generated comment by CodeRabbit for review status -->
| # Stage 1: build the Go binary | ||
| FROM golang:1-alpine AS builder | ||
|
|
||
| WORKDIR /build | ||
|
|
||
| # Cache module downloads across builds | ||
| COPY go.mod go.sum ./ | ||
| RUN go mod download | ||
|
|
||
| # Build a static binary (no cgo) for the runtime image | ||
| COPY . . | ||
| RUN CGO_ENABLED=0 GOOS=linux go build -o oba-validator ./cmd/oba-validator | ||
|
|
||
| # Stage 2: minimal runtime | ||
| FROM alpine:3 | ||
|
|
||
| # HTTPS to the OBA API and the GTFS / GTFS-realtime feeds needs CA certificates. | ||
| RUN apk add --no-cache ca-certificates | ||
|
|
||
| WORKDIR /app | ||
| COPY --from=builder /build/oba-validator /app/oba-validator | ||
| COPY entrypoint.sh /app/entrypoint.sh | ||
| RUN chmod +x /app/entrypoint.sh | ||
|
|
||
| # entrypoint.sh base64-decodes its argument into the config JSON before invoking | ||
| # the validator. This is required because a Render one-off job's startCommand is | ||
| # split on whitespace and passed as argv (no shell), so the JSON — which has | ||
| # spaces and special characters — must be base64-encoded by the caller: | ||
| # `/app/entrypoint.sh <base64-config>`. (Render uses the startCommand's first | ||
| # token as the executable, hence naming entrypoint.sh explicitly.) See "Deploying | ||
| # to Render" in the README. No API key is baked in — keys are per-server and | ||
| # travel in the config. | ||
| ENTRYPOINT ["/app/entrypoint.sh"] | ||
| CMD [] |
There was a problem hiding this comment.
Add a non-root user for runtime security.
The container runs as root, which increases the attack surface if the validator processes untrusted input or has vulnerabilities. Since one-off jobs accept base64-encoded config from external callers, running as a non-root user follows the principle of least privilege.
🔒 Proposed fix to add a non-root user
# Stage 2: minimal runtime
FROM alpine:3
# HTTPS to the OBA API and the GTFS / GTFS-realtime feeds needs CA certificates.
RUN apk add --no-cache ca-certificates
+# Run as non-root user
+RUN addgroup -S appgroup && adduser -S appuser -G appgroup
+
WORKDIR /app
COPY --from=builder /build/oba-validator /app/oba-validator
COPY entrypoint.sh /app/entrypoint.sh
RUN chmod +x /app/entrypoint.sh
+USER appuser
+
# entrypoint.sh base64-decodes its argument into the config JSON before invoking
# the validator. This is required because a Render one-off job's startCommand is
# split on whitespace and passed as argv (no shell), so the JSON — which has
# spaces and special characters — must be base64-encoded by the caller:
# `/app/entrypoint.sh <base64-config>`. (Render uses the startCommand's first
# token as the executable, hence naming entrypoint.sh explicitly.) See "Deploying
# to Render" in the README. No API key is baked in — keys are per-server and
# travel in the config.
ENTRYPOINT ["/app/entrypoint.sh"]
CMD []📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| # Stage 1: build the Go binary | |
| FROM golang:1-alpine AS builder | |
| WORKDIR /build | |
| # Cache module downloads across builds | |
| COPY go.mod go.sum ./ | |
| RUN go mod download | |
| # Build a static binary (no cgo) for the runtime image | |
| COPY . . | |
| RUN CGO_ENABLED=0 GOOS=linux go build -o oba-validator ./cmd/oba-validator | |
| # Stage 2: minimal runtime | |
| FROM alpine:3 | |
| # HTTPS to the OBA API and the GTFS / GTFS-realtime feeds needs CA certificates. | |
| RUN apk add --no-cache ca-certificates | |
| WORKDIR /app | |
| COPY --from=builder /build/oba-validator /app/oba-validator | |
| COPY entrypoint.sh /app/entrypoint.sh | |
| RUN chmod +x /app/entrypoint.sh | |
| # entrypoint.sh base64-decodes its argument into the config JSON before invoking | |
| # the validator. This is required because a Render one-off job's startCommand is | |
| # split on whitespace and passed as argv (no shell), so the JSON — which has | |
| # spaces and special characters — must be base64-encoded by the caller: | |
| # `/app/entrypoint.sh <base64-config>`. (Render uses the startCommand's first | |
| # token as the executable, hence naming entrypoint.sh explicitly.) See "Deploying | |
| # to Render" in the README. No API key is baked in — keys are per-server and | |
| # travel in the config. | |
| ENTRYPOINT ["/app/entrypoint.sh"] | |
| CMD [] | |
| # Stage 1: build the Go binary | |
| FROM golang:1-alpine AS builder | |
| WORKDIR /build | |
| # Cache module downloads across builds | |
| COPY go.mod go.sum ./ | |
| RUN go mod download | |
| # Build a static binary (no cgo) for the runtime image | |
| COPY . . | |
| RUN CGO_ENABLED=0 GOOS=linux go build -o oba-validator ./cmd/oba-validator | |
| # Stage 2: minimal runtime | |
| FROM alpine:3 | |
| # HTTPS to the OBA API and the GTFS / GTFS-realtime feeds needs CA certificates. | |
| RUN apk add --no-cache ca-certificates | |
| # Run as non-root user | |
| RUN addgroup -S appgroup && adduser -S appuser -G appgroup | |
| WORKDIR /app | |
| COPY --from=builder /build/oba-validator /app/oba-validator | |
| COPY entrypoint.sh /app/entrypoint.sh | |
| RUN chmod +x /app/entrypoint.sh | |
| USER appuser | |
| # entrypoint.sh base64-decodes its argument into the config JSON before invoking | |
| # the validator. This is required because a Render one-off job's startCommand is | |
| # split on whitespace and passed as argv (no shell), so the JSON — which has | |
| # spaces and special characters — must be base64-encoded by the caller: | |
| # `/app/entrypoint.sh <base64-config>`. (Render uses the startCommand's first | |
| # token as the executable, hence naming entrypoint.sh explicitly.) See "Deploying | |
| # to Render" in the README. No API key is baked in — keys are per-server and | |
| # travel in the config. | |
| ENTRYPOINT ["/app/entrypoint.sh"] | |
| CMD [] |
🧰 Tools
🪛 Trivy (0.69.3)
[error] 1-1: Image user should not be 'root'
Specify at least 1 USER command in Dockerfile with non-root user as argument
Rule: DS-0002
(IaC/Dockerfile)
🤖 Prompt for 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.
In `@Dockerfile` around lines 1 - 34, Add a non-root runtime user and switch to it
in the Dockerfile: create a user/group (e.g., obauser), chown /app and the
binary (/app/oba-validator and /app/entrypoint.sh) to that user, ensure
entrypoint.sh remains executable, and add USER obauser before ENTRYPOINT so the
container does not run as root; reference the Dockerfile stages that COPY
--from=builder /build/oba-validator and COPY entrypoint.sh /app/entrypoint.sh
and ensure permissions/ownership changes happen after those COPY steps.
Two parallel jobs on push-to-main and pull_request:
- test: gofmt check, go vet, `go mod tidy` cleanliness, `go mod verify`,
`go test -race ./...` (the validator fans out concurrently, so the race
detector is worth running), and shellcheck on entrypoint.sh.
- docker: build the deployment image and smoke-test it offline — base64 of
"{}" exercises entrypoint.sh -> oba-validator -> config.Load and must exit 2.
The live integration test (OBA_VALIDATOR_LIVE) is intentionally not run in CI;
it depends on a real OBA server. Includes concurrency cancellation and per-job
timeouts.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude <noreply@anthropic.com>
actions/checkout@v4 and actions/setup-go@v5 run on Node.js 20, which GitHub forces off by June 2, 2026. The v6 majors run on Node 24. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com>
Summary
oba-validatorto Render as a Docker cron job scheduled for Feb 29 (0 2 29 2 *) so it effectively never runs on its own; real validations are launched on demand as Render one-off jobs.apiKeyrides inside the config supplied at invocation time.startCommandon whitespace with no shell (first token = the executable, overridingENTRYPOINT), so the config JSON is base64-encoded into one token and decoded byentrypoint.sh.Deployment artifacts:
Dockerfile(multi-stage pure-Go → alpine + ca-certificates),entrypoint.sh,render.yamlBlueprint,.dockerignore, adocker-buildMakefile target, a README "Deploying to Render" section, and a design spec.CI (
.github/workflows/ci.yml, two parallel jobs):gofmtcheck,go vet,go mod tidycleanliness,go mod verify,go test -race ./...(the validator fans out concurrently, so the race detector earns its place), andshellcheck entrypoint.sh.{}exercisesentrypoint.sh → oba-validator → config.Loadand must exit 2 (config error), so a broken Dockerfile/entrypoint fails the PR rather than the Render deploy.mainand PRs; usesgo-version-file: go.mod(auto-tracks the pinned Go version); concurrency cancellation + per-job timeouts. The live integration test (OBA_VALIDATOR_LIVE) is intentionally excluded — it depends on a real OBA server.Test plan
Deployment, exercised against the live KCM/Puget Sound config (public sample key
org.onebusaway.iphone):make docker-buildbuilds cleandocker run --rm oba-validator <base64-config>runs a full validation; exit code propagates throughexec(validation failure → 1)--entrypoint /app/entrypoint.sh→ same reportdocker run --rm oba-validator '<raw-json>'("{"-prefixed) passes through → full report... <base64> --jsonemits JSON{arg → loudbase64decode error, non-zero exitCI, verified locally and on a real Actions run (green):
go mod tidy(no diff),go mod verify,go test -race ./..., shellcheck, docker build + smoke test (exit 2)checkout/setup-goto v6 (Node 24) and re-verified — no warnings remainReview notes
ENTRYPOINTwas preserved (single-quoted JSON appended as args). Two review agents flagged it; obacloud's production integration (api_key_service_job.rb,create_job.rb) confirmed Render overridesENTRYPOINT, whitespace-splits with no shell, and the established fix is base64 + anentrypoint.shdecoder.entrypoint.shwith no args → exit 2 (a harmless "failed" run). Left as-is for simplicity; adockerCommandno-op could make it a clean exit 0 if that matters.🤖 Generated with Claude Code