feat: deploy anywhere — graceful io_uring fallback, PORT/HOST env, /_mer/health, drop-in PaaS configs#99
Conversation
…mer/health, drop-in PaaS configs Make merjs trivial to deploy on any Linux host regardless of io_uring support, and add ready-to-go configs for every major hosting target. Runtime: - src/runtime.zig: gracefully fall back from Evented (io_uring) to Threaded if std.Io.Evented.init fails. Common causes: old kernel, restricted seccomp profile (Docker default), gVisor, sandboxed container runtimes. Same binary now boots everywhere — io_uring is used opportunistically for max throughput where available. - src/main.zig: log the active backend at startup. Deployability (main.zig): - Honor PORT and HOST env vars (PaaS standard: Fly, Render, Railway, Heroku, Cloud Run all inject PORT). Flags still take precedence. - Default HOST to 0.0.0.0 in --no-dev so containers bind correctly out of the box; dev mode keeps 127.0.0.1. - Add MERJS_DEV=0 as an env-var equivalent to --no-dev. - Add --help / -h with documentation of all flags + env vars. Health endpoints (server.zig): - GET /_mer/health and /_mer/ready return prod-safe JSON. Available in both dev and --no-dev (unlike /_mer/debug). Used by Docker HEALTHCHECK, k8s probes, and PaaS health checks. Dockerfile: - Pin ZIG_VERSION=0.16.0 to match build.zig.zon (was 0.15.2). - Run as non-root user uid 10001 (required by OpenShift, Cloud Run, k8s). - tini as PID 1 — proper signal forwarding + zombie reaping. - HEALTHCHECK against /_mer/health. - ENV PORT=3000 HOST=0.0.0.0 baked in. - OCI labels for image metadata. install.sh: was broken — looked for merjs-VER-OS-ARCH.tar.gz but the release workflow ships raw mer-OS-ARCH binaries. Rewritten to match the actual release assets, with checksum verification and PATH-detection. Drop-in deploy configs: - docker-compose.yml — one-command run with healthcheck wired up. - fly.toml — Fly.io. - render.yaml — Render Blueprint. - railway.json — Railway. - .do/app.yaml — DigitalOcean App Platform. - Procfile — Heroku-style PaaS. CI: - .github/workflows/docker-publish.yml — multi-arch (amd64+arm64) Docker image published to GHCR on tag and main pushes. Docs: - README: replaced 'Deploy to Cloudflare Workers' section with 'Deploy' covering Docker, Fly, Render, Railway, DO, Heroku, and Workers. Documents the runtime selection log line. - AGENTS: documents new prod endpoints + deployment env vars. Co-Authored-By: Rach Pradhan <pradhan.rach4@gmail.com>
🤖 Devin AI EngineerI'll be helping with this pull request! Here's what you should know: ✅ I will automatically:
Note: I can only respond to comments from users who have write access to this repository. ⚙️ Control Options:
|
Zig 0.16.0's release stdlib has a known bug in std/Io/Uring.zig where Dir.OpenError and Dir.RealPathFileError are missing error.ReadOnlyFileSystem, but the Uring vtable forwards it via `else => |e| return e`. Calling `Evented.init().io()` forces analysis of every Uring vtable function and triggers compile errors on stock 0.16.0 in CI. Add `stdlib_evented_works = false` const guard. The whole fallback architecture is preserved — flipping it back to true re-enables the Evented (io_uring) path with no other code changes once the toolchain pin moves past 0.16.0. Docs updated to reflect current state. Co-Authored-By: Rach Pradhan <pradhan.rach4@gmail.com>
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: 7c2482b85f
ℹ️ About Codex in GitHub
Codex has been enabled to automatically review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
When you sign up for Codex through ChatGPT, Codex can also answer questions or update the PR, like "@codex address that feedback".
| config.port = std.fmt.parseInt(u16, v, 10) catch |err| { | ||
| log.err("invalid PORT={s}: {s}", .{ v, @errorName(err) }); | ||
| return err; |
There was a problem hiding this comment.
Honor --port override when PORT is malformed
The new env parsing returns immediately on an invalid PORT value before CLI arguments are processed, so PORT=abc merjs --port 3000 now exits with an error instead of honoring the documented flag precedence. This makes startup brittle in environments where PORT may be preset incorrectly and prevents users from recovering via --port.
Useful? React with 👍 / 👎.
build.zig.zon's `.version` is now the only place the framework version
lives. Wired into a `build_options` module that feeds:
- src/mer.zig `pub const version` (used by /_mer/health, /_mer/ready,
the dev error overlay, and Sentry telemetry tags)
- cli.zig `pub const version` (mer --version)
- the macOS desktop bundle's Info.plist CFBundleVersion (built via
b.fmt so the literal disappears)
Both mer.zig and cli.zig get a unit test asserting their `version`
constant equals build_options.version. If a future change reverts
either constant to a hardcoded literal that drifts from the package
version, the test fails. mer.zig also sanity-checks the shape
("X.Y.Z", length ≥ 5) so a typo in build.zig.zon surfaces in tests
rather than at runtime.
Verified:
- zig build test: 27/29 pass (2 pre-existing skips), including the
two new drift tests.
- /_mer/health and /_mer/ready now report {"version":"0.2.53"}.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Rach's Agentic Contribution Template
Maintainer process template by @rachpradhan.
Linked Issue
Closes #
Summary
Goal: same merjs binary boots and serves on every Linux host (any kernel, any seccomp profile, any PaaS), with one-command deploys to every major target.
What changed:
src/runtime.zignow triesstd.Io.Evented(io_uring) first on Linux and automatically falls back tostd.Io.Threadedifio_uring_setupfails. Common causes: old kernel, restricted seccomp (Docker default), gVisor, sandboxed containers. Before this change the server panicked at boot in any environment without io_uring.PORT/HOSTenv-var support insrc/main.zig— every PaaS (Fly, Render, Railway, Heroku, Cloud Run) injectsPORT, but merjs only honored the--portflag. Now: env vars are read, flags still take precedence.--no-devdefaultsHOSTto0.0.0.0(containers bind correctly out of the box); dev mode keeps127.0.0.1./_mer/health+/_mer/ready— production-safe JSON endpoints, available in both dev and--no-dev(/_mer/debugis dev-only). Used by DockerHEALTHCHECK, k8s liveness/readiness probes, and PaaS health checks.ZIG_VERSIONwas pinned to0.15.2despitebuild.zig.zonrequiring0.16.0(broken). Repinned. Also: non-root user (uid 10001 — required by OpenShift / Cloud Run / k8s),tinias PID 1 for signal handling,HEALTHCHECKagainst/_mer/health, OCI labels, multi-arch ready.install.shwas broken — it tried to downloadmerjs-${VER}-${OS}-${ARCH}.tar.gzbut the release workflow ships rawmer-${OS}-${ARCH}binaries (different naming). Rewrote to match the actual release assets, with sha256 checksum verification + PATH detection.docker-compose.yml,fly.toml,render.yaml,railway.json,.do/app.yaml,Procfile..github/workflows/docker-publish.ymlbuilds & pushes a multi-arch (amd64+arm64) image toghcr.io/justrach/merjson tag andmainpushes.What is intentionally not in scope:
cli.zig/mer inittemplate changes — preserved for a follow-up.runtime.ioplumbing — the existing call sites (static.zig,fetch.zig,prerender.zig,telemetry.zig) already route throughruntime.io, so they pick up io_uring automatically when available. The fallback change makes the opportunistic use of io_uring real (binary now actually ships everywhere instead of crashing).install.sherrors out cleanly on unsupported OSes.MERJS_RUNTIME=threaded); deferred because reading env in 0.16 before runtime init requires either libc-linking codegen or threading theInit.Minimalstruct down. Happy to add as a build-time flag if you want.Scope
src/runtime.zig,src/main.zig,src/server.zig,Dockerfile,install.sh, README, AGENTS, plus new top-level deploy configs and one CI workflow.build.zig.zonuntouched)install.sh— that alone is +138/−160). The actual runtime/server code change is small:runtime.zig+60/−26,main.zig+56/−5,server.zig+13/0. Most of the rest is config files for new deployment targets and the README rewrite. Happy to split if reviewers prefer (e.g. liftinstall.shand the deploy configs into a follow-up PR), but each piece relies on the others to actually be useful end-to-end (/_mer/healthonly matters once the Dockerfile uses it,PORTonly matters once a PaaS config wires it up, etc.), so I shipped them as one coherent "deploy anywhere" theme.Rebase Status
maincompleted before requesting review (branched offfe5b35cwhich is currentorigin/main)Tests Run
zig build codegen zig build zig build test --summary allRed-To-Green Evidence
Failing Before
Passing After
Nearby Non-Regression Checks
Benchmarks
Not applicable — no benchmark code touched, no perf claims altered. The opportunistic io_uring path is unchanged on hosts where io_uring works; on hosts where it doesn't, we go from "panic at boot" to "Threaded fallback" (which is strictly an improvement over not booting at all).
Checklist
install.shrewrite + new deploy config files; runtime/server code change is ~75 net lines). Happy to split if reviewers prefer..zig-cache,zig-out, dylibs, or other build artifacts are committedLink to Devin session: https://app.devin.ai/sessions/0b66b6a4b7714415995d0cfdcef09af7
Requested by: @justrach