1"use strict";(self.webpackChunkgatsby_starter_default=self.webpackChunkgatsby_starter_default||[]).push([[2676],{19668:function(e,n,a){a.r(n),a.d(n,{Head:function(){return g},default:function(){return h}});var t=a(28453),s=a(96540);function r(e){const n=Object.assign({h1:"h1",a:"a",span:"span",blockquote:"blockquote",p:"p",strong:"strong",h2:"h2",ul:"ul",li:"li",table:"table",thead:"thead",tr:"tr",th:"th",tbody:"tbody",td:"td",h3:"h3",ol:"ol",div:"div",svg:"svg",path:"path"},(0,t.R)(),e.components);return s.createElement(s.Fragment,null,s.createElement(n.h1,{id:"node-renderer-container-deployment",style:{position:"relative"}},s.createElement(n.a,{href:"#node-renderer-container-deployment","aria-label":"node renderer container deployment permalink",className:"anchor before"},s.createElement(n.span,{dangerouslySetInnerHTML:{__html:'<svg aria-hidden="true" height="20" version="1.1" viewBox="0 0 16 16" width="20"><path fill-rule="evenodd" d="M4 9h1v1H4c-1.5 0-3-1.69-3-3.5S2.55 3 4 3h4c1.45 0 3 1.69 3 3.5 0 1.41-.91 2.72-2 3.25V8.59c.58-.45 1-1.27 1-2.09C10 5.22 8.98 4 8 4H4c-.98 0-2 1.22-2 2.5S3 9 4 9zm9-3h-1v1h1c1 0 2 1.22 2 2.5S13.98 12 13 12H9c-.98 0-2-1.22-2-2.5 0-.83.42-1.64 1-2.09V6.25c-1.09.53-2 1.84-2 3.25C6 11.31 7.55 13 9 13h4c1.45 0 3-1.69 3-3.5S14.5 6 13 6z"></path></svg>'}})),"Node Renderer: Container Deployment"),"\n",s.createElement(n.blockquote,null,"\n",s.createElement(n.p,null,s.createElement(n.strong,null,"Pro Feature")," â Available with ",s.createElement(n.a,{href:"https://pro.reactonrails.com"},"React on Rails Pro"),".\nFree in development, test, CI, and staging, and in production for small\norganizations, charities, schools, and hospitals; larger organizations\nsubscribe at ",s.createElement(n.a,{href:"https://pro.reactonrails.com/"},"https://pro.reactonrails.com/")," ($1,800 per year per organization).\nNo license key is needed to run Pro."),"\n"),"\n",s.createElement(n.p,null,"This guide covers deploying the Node Renderer in containerized environments (Docker, Kubernetes, ControlPlane, etc.), including architecture options, performance tuning, memory management, error tracking, and troubleshooting."),"\n",s.createElement(n.h2,{id:"prerequisites",style:{position:"relative"}},s.createElement(n.a,{href:"#prerequisites","aria-label":"prerequisites permalink",className:"anchor before"},s.createElement(n.span,{dangerouslySetInnerHTML:{__html:'<svg aria-hidden="true" height="20" version="1.1" viewBox="0 0 16 16" width="20"><path fill-rule="evenodd" d="M4 9h1v1H4c-1.5 0-3-1.69-3-3.5S2.55 3 4 3h4c1.45 0 3 1.69 3 3.5 0 1.41-.91 2.72-2 3.25V8.59c.58-.45 1-1.27 1-2.09C10 5.22 8.98 4 8 4H4c-.98 0-2 1.22-2 2.5S3 9 4 9zm9-3h-1v1h1c1 0 2 1.22 2 2.5S13.98 12 13 12H9c-.98 0-2-1.22-2-2.5 0-.83.42-1.64 1-2.09V6.25c-1.09.53-2 1.84-2 3.25C6 11.31 7.55 13 9 13h4c1.45 0 3-1.69 3-3.5S14.5 6 13 6z"></path></svg>'}})),"Prerequisites"),"\n",s.createElement(n.ul,null,"\n",s.createElement(n.li,null,s.createElement(n.strong,null,"React on Rails Pro")," v16.4.0 or later"),"\n",s.createElement(n.li,null,s.createElement(n.strong,null,"Node.js")," 22+ (LTS recommended)"),"\n",s.createElement(n.li,null,s.createElement(n.strong,null,"Ruby")," 3.1+ with Bundler"),"\n",s.createElement(n.li,null,"The ",s.createElement(n.span,{dangerouslySetInnerHTML:{__html:'<code class="language-text">react-on-rails-pro-node-renderer</code>'}})," npm package installed in your project"),"\n"),"\n",s.createElement(n.h2,{id:"http-transport-h2c-or-http11",style:{position:"relative"}},s.createElement(n.a,{href:"#http-transport-h2c-or-http11","aria-label":"http transport h2c or http11 permalink",className:"anchor before"},s.createElement(n.span,{dangerouslySetInnerHTML:{__html:'<svg aria-hidden="true" height="20" version="1.1" viewBox="0 0 16 16" width="20"><path fill-rule="evenodd" d="M4 9h1v1H4c-1.5 0-3-1.69-3-3.5S2.55 3 4 3h4c1.45 0 3 1.69 3 3.5 0 1.41-.91 2.72-2 3.25V8.59c.58-.45 1-1.27 1-2.09C10 5.22 8.98 4 8 4H4c-.98 0-2 1.22-2 2.5S3 9 4 9zm9-3h-1v1h1c1 0 2 1.22 2 2.5S13.98 12 13 12H9c-.98 0-2-1.22-2-2.5 0-.83.42-1.64 1-2.09V6.25c-1.09.53-2 1.84-2 3.25C6 11.31 7.55 13 9 13h4c1.45 0 3-1.69 3-3.5S14.5 6 13 6z"></path></svg>'}}
1)),"HTTP Transport: h2c or HTTP/1.1"),"\n",s.createElement(n.p,null,"The Node Renderer and Rails client use cleartext HTTP/2 (h2c) by default. Keep that default when async props must cross\nan intermediary that may buffer an HTTP/1.1 request."),"\n",s.createElement(n.p,null,"Deployments that require HTTP/1.1 can select it explicitly by pairing Node's\n",s.createElement(n.span,{dangerouslySetInnerHTML:{__html:'<code class="language-text">fastifyServerOptions: { http2: false }</code>'}})," with Rails'\n",s.createElement(n.span,{dangerouslySetInnerHTML:{__html:'<code class="language-text">config.renderer_http_force_http2 = false</code>'}}),". Regular rendering and response streaming remain available. Direct HTTP/1.1\nconnections can stream async props in both directions, but request-buffering or half-duplex intermediaries remain\nunsupported for async props."),"\n",s.createElement(n.p,null,"See ",s.createElement(n.a,{href:"./health-checks.md#choosing-h2c-or-http11"},"Node Renderer Health and Readiness Endpoints")," for the complete paired\nconfiguration, password and private-network requirements, ALB target-group settings, connection-pool sizing, probe\noptions, and safe rollout order."),"\n",s.createElement(n.h2,{id:"architecture-options",style:{position:"relative"}},s.createElement(n.a,{href:"#architecture-options","aria-label":"architecture options permalink",className:"anchor before"},s.createElement(n.span,{dangerouslySetInnerHTML:{__html:'<svg aria-hidden="true" height="20" version="1.1" viewBox="0 0 16 16" width="20"><path fill-rule="evenodd" d="M4 9h1v1H4c-1.5 0-3-1.69-3-3.5S2.55 3 4 3h4c1.45 0 3 1.69 3 3.5 0 1.41-.91 2.72-2 3.25V8.59c.58-.45 1-1.27 1-2.09C10 5.22 8.98 4 8 4H4c-.98 0-2 1.22-2 2.5S3 9 4 9zm9-3h-1v1h1c1 0 2 1.22 2 2.5S13.98 12 13 12H9c-.98 0-2-1.22-2-2.5 0-.83.42-1.64 1-2.09V6.25c-1.09.53-2 1.84-2 3.25C6 11.31 7.55 13 9 13h4c1.45 0 3-1.69 3-3.5S14.5 6 13 6z"></path></svg>'}})),"Architecture Options"),"\n",s.createElement(n.p,null,"When running Rails and the Node Renderer in containers, you have three options, listed from simplest to most complex:"),"\n",s.createElement(n.table,null,s.createElement(n.thead,null,s.createElement(n.tr,null,s.createElement(n.th),s.createElement(n.th,null,"Single Container"),s.createElement(n.th,null,"Sidecar Containers"),s.createElement(n.th,null,"Separate Workloads"))),s.createElement(n.tbody,null,s.createElement(n.tr,null,s.createElement(n.td,null,s.createElement(n.strong,null,"Complexity")),s.createElement(n.td,null,"Lowest"),s.createElement(n.td,null,"Medium"),s.createElement(n.td,null,"Highest")),s.createElement(n.tr,null,s.createElement(n.td,null,s.createElement(n.strong,null,"Scaling")),s.createElement(n.td,null,"Together"),s.createElement(n.td,null,"Together"),s.createElement(n.td,null,"Independent")),s.createElement(n.tr,null,s.createElement(n.td,null,s.createElement(n.strong,null,"Version alignment")),s.createElement(n.td,null,"Guaranteed"),s.createElement(n.td,null,"Guaranteed"),s.createElement(n.td,null,"Risk of drift")),s.createElement(n.tr,null,s.createElement(n.td,null,s.createElement(n.strong,null,"Networking")),s.createElement(n.td,null,s.createElement(n.span,{dangerouslySetInnerHTML:{__html:'<code class="language-text">localhost</code>'}})),s.createElement(n.td,null,s.createElement(n.span,{dangerouslySetInnerHTML:{__html:'<code class="language-text">localhost</code>'}})),s.createElement(n.td,null,"Service DNS")),s.createElement(n.tr,null,s.createElement(n.td,null,s.createElement(n.strong,null,"Per-process visibility")),s.createElement(n.td,null,"No"),s.createElement(n.td,null,"Yes"),s.createElement(n.td,null,"Yes")),s.createElement(n.tr,null,s.createElement(n.td,null,s.createElement(n.strong,null,"Generations per renderer during rollout")),s.createElement(n.td,null,"1 when lifecycle is atomic"),s.createElement(n.td,null,"1 when pod lifecycle is atomic"),s.createElement(n.td,null,"2+ while app revisions overlap")),s.createElement(n.tr,null,s.createElement(n.td,null,s.createElement(n.strong,null,"RSC VM contexts per worker")),s.createElement(n.td,null,"2 minimum; 4 default"),s.createElement(n.td,null,"2 minimum; 4 default"),s.createElement(n.td,null,"4 for old + new; increase for wider overlap")),s.createElement(n.tr,null,s.createElement(n.td,null,s.createElement(n.strong,null,"When to use")),s.createElement(n.td,null,"Default starting point"),s.createElement(n.td,null,"Need to diagnose OOM source"),s.createElement(n.td,null,"Need independent scaling at high replica counts")))),"\n",s.createElement("p",{align:"center"},s.createElement("img",{src:"images/deployment-topologies.svg",alt:"Three container topologies for running Rails with the Pro Node Renderer. Single container: both processes run in one container sharing OS resources and talking over localhost â lowest complexity, scaled together, versions always aligned. Sidecar containers: Rails and the Node Renderer run as two containers in the same pod with their own resource limits but still talking over localhost â scaled together, versions aligned, with per-process visibility. Separate workloads: Rails and the renderer run as independent workloads communicating over service DNS â scaled independently but with a risk of version drift on rolling deploys.",width:"840"})),"\n",s.createElement(n.h3,{id:"option-1-single-container-default",style:{position:"relative"}}
1,s.createElement(n.a,{href:"#option-1-single-container-default","aria-label":"option 1 single container default permalink",className:"anchor before"},s.createElement(n.span,{dangerouslySetInnerHTML:{__html:'<svg aria-hidden="true" height="20" version="1.1" viewBox="0 0 16 16" width="20"><path fill-rule="evenodd" d="M4 9h1v1H4c-1.5 0-3-1.69-3-3.5S2.55 3 4 3h4c1.45 0 3 1.69 3 3.5 0 1.41-.91 2.72-2 3.25V8.59c.58-.45 1-1.27 1-2.09C10 5.22 8.98 4 8 4H4c-.98 0-2 1.22-2 2.5S3 9 4 9zm9-3h-1v1h1c1 0 2 1.22 2 2.5S13.98 12 13 12H9c-.98 0-2-1.22-2-2.5 0-.83.42-1.64 1-2.09V6.25c-1.09.53-2 1.84-2 3.25C6 11.31 7.55 13 9 13h4c1.45 0 3-1.69 3-3.5S14.5 6 13 6z"></path></svg>'}})),"Option 1: Single Container (Default)"),"\n",s.createElement(n.p,null,"Rails and the Node Renderer run together in a ",s.createElement(n.strong,null,"single container"),". This is the simplest setup and the recommended starting point (the leftmost topology in the diagram above)."),"\n",s.createElement(n.p,null,"Both processes share the container's CPU and memory limits (cgroup resources); they do not communicate via shared-memory IPC."),"\n",s.createElement(n.p,null,s.createElement(n.strong,null,"Advantages:")),"\n",s.createElement(n.ul,null,"\n",s.createElement(n.li,null,s.createElement(n.strong,null,"Simplest setup")," â One container, one image, one deploy."),"\n",s.createElement(n.li,null,s.createElement(n.strong,null,"No networking config")," â Rails connects to the renderer via ",s.createElement(n.span,{dangerouslySetInnerHTML:{__html:'<code class="language-text">localhost</code>'}})," out of the box."),"\n",s.createElement(n.li,null,s.createElement(n.strong,null,"Guaranteed version alignment")," â Both processes always run from the same image."),"\n",s.createElement(n.li,null,s.createElement(n.strong,null,"Lower overhead")," â Shared OS layer saves ~200â300 MB vs. separate containers."),"\n"),"\n",s.createElement(n.p,null,s.createElement(n.strong,null,"When to move on:")," If you're experiencing OOM restarts and need to determine whether Rails or the Node Renderer is the culprit, move to sidecar containers for visibility."),"\n",s.createElement(n.p,null,s.createElement(n.strong,null,"Configuration:")),"\n",s.createElement(n.span,{dangerouslySetInnerHTML:{__html:'<div class="gatsby-highlight" data-language="ruby"><pre class="language-ruby"><code class="language-ruby"><span class="token comment"># config/initializers/react_on_rails_pro.rb</span>\nReactOnRailsPro<span class="token punctuation">.</span>configure <span class="token keyword">do</span> <span class="token operator">|</span>config<span class="token operator">|</span>\n config<span class="token punctuation">.</span>server_renderer <span class="token operator">=</span> <span class="token string-literal"><span class="token string">"NodeRenderer"</span></span>\n config<span class="token punctuation">.</span>renderer_url <span class="token operator">=</span> <span class="token string-literal"><span class="token string">"http://localhost:3800"</span></span>\n<span class="token keyword">end</span></code></pre></div>'}}),"\n",s.createElement(n.h3,{id:"option-2-sidecar-containers",style:{position:"relative"}},s.createElement(n.a,{href:"#option-2-sidecar-containers","aria-label":"option 2 sidecar containers permalink",className:"anchor before"},s.createElement(n.span,{dangerouslySetInnerHTML:{__html:'<svg aria-hidden="true" height="20" version="1.1" viewBox="0 0 16 16" width="20"><path fill-rule="evenodd" d="M4 9h1v1H4c-1.5 0-3-1.69-3-3.5S2.55 3 4 3h4c1.45 0 3 1.69 3 3.5 0 1.41-.91 2.72-2 3.25V8.59c.58-.45 1-1.27 1-2.09C10 5.22 8.98 4 8 4H4c-.98 0-2 1.22-2 2.5S3 9 4 9zm9-3h-1v1h1c1 0 2 1.22 2 2.5S13.98 12 13 12H9c-.98 0-2-1.22-2-2.5 0-.83.42-1.64 1-2.09V6.25c-1.09.53-2 1.84-2 3.25C6 11.31 7.55 13 9 13h4c1.45 0 3-1.69 3-3.5S14.5 6 13 6z"></path></svg>'}})),"Option 2: Sidecar Containers"),"\n",s.createElement(n.p,null,"Rails and the Node Renderer run as separate containers within the ",s.createElement(n.strong,null,"same pod/workload"),", sharing the same lifecycle (the middle topology in the diagram above). Use this when you need to isolate and diagnose memory/CPU usage per process."),"\n",s.createElement(n.p,null,s.createElement(n.strong,null,"Advantages:")),"\n",s.createElement(n.ul,null,"\n",s.createElement(n.li,null,s.createElement(n.strong,null,"Per-process visibility")," â See exactly which process is consuming memory and causing OOM kills."),"\n",s.createElement(n.li,null,s.createElement(n.strong,null,"Independent resource limits")," â Set separate CPU/memory limits for Rails and the Node Renderer."),"\n",s.createElement(n.li,null,s.createElement(n.strong,null,"Guaranteed ver
1sion alignment")," â Both containers use the same image on deploy."),"\n",s.createElement(n.li,null,s.createElement(n.strong,null,"Simpler networking")," â Rails still connects via ",s.createElement(n.span,{dangerouslySetInnerHTML:{__html:'<code class="language-text">localhost</code>'}}),"."),"\n"),"\n",s.createElement(n.p,null,s.createElement(n.strong,null,"Tradeoffs:")),"\n",s.createElement(n.ul,null,"\n",s.createElement(n.li,null,"Slightly more complex deployment config than a single container."),"\n",s.createElement(n.li,null,"Autoscaling logic may need adjustment (see ",s.createElement(n.a,{href:"#autoscaling-considerations"},"Autoscaling Considerations"),")."),"\n"),"\n",s.createElement(n.p,null,s.createElement(n.strong,null,"Configuration:")),"\n",s.createElement(n.span,{dangerouslySetInnerHTML:{__html:'<div class="gatsby-highlight" data-language="ruby"><pre class="language-ruby"><code class="language-ruby"><span class="token comment"># config/initializers/react_on_rails_pro.rb</span>\nReactOnRailsPro<span class="token punctuation">.</span>configure <span class="token keyword">do</span> <span class="token operator">|</span>config<span class="token operator">|</span>\n config<span class="token punctuation">.</span>server_renderer <span class="token operator">=</span> <span class="token string-literal"><span class="token string">"NodeRenderer"</span></span>\n config<span class="token punctuation">.</span>renderer_url <span class="token operator">=</span> <span class="token string-literal"><span class="token string">"http://localhost:3800"</span></span>\n<span class="token keyword">end</span></code></pre></div>'}}),"\n",s.createElement(n.h3,{id:"option-3-separate-workloads",style:{position:"relative"}},s.createElement(n.a,{href:"#option-3-separate-workloads","aria-label":"option 3 separate workloads permalink",className:"anchor before"},s.createElement(n.span,{dangerouslySetInnerHTML:{__html:'<svg aria-hidden="true" height="20" version="1.1" viewBox="0 0 16 16" width="20"><path fill-rule="evenodd" d="M4 9h1v1H4c-1.5 0-3-1.69-3-3.5S2.55 3 4 3h4c1.45 0 3 1.69 3 3.5 0 1.41-.91 2.72-2 3.25V8.59c.58-.45 1-1.27 1-2.09C10 5.22 8.98 4 8 4H4c-.98 0-2 1.22-2 2.5S3 9 4 9zm9-3h-1v1h1c1 0 2 1.22 2 2.5S13.98 12 13 12H9c-.98 0-2-1.22-2-2.5 0-.83.42-1.64 1-2.09V6.25c-1.09.53-2 1.84-2 3.25C6 11.31 7.55 13 9 13h4c1.45 0 3-1.69 3-3.5S14.5 6 13 6z"></path></svg>'}})),"Option 3: Separate Workloads"),"\n",s.createElement(n.p,null,"Rails and the Node Renderer run as independent workloads with their own scaling. This is the most complex option and is rarely needed."),"\n",s.createElement(n.p,null,s.createElement(n.strong,null,"Advantages:")),"\n",s.createElement(n.ul,null,"\n",s.createElement(n.li,null,s.createElement(n.strong,null,"Independent scaling")," â Scale the renderer independently of Rails."),"\n",s.createElement(n.li,null,s.createElement(n.strong,null,"Isolated resource limits")," â OOM in the renderer doesn't affect Rails and vice versa."),"\n"),"\n",s.createElement(n.p,null,s.createElement(n.strong,null,"Disadvantages:")),"\n",s.createElement(n.ul,null,"\n",s.createElement(n.li,null,s.createElement(n.strong,null,"Version drift risk")," â During rolling deploys, Rails and the Node Renderer roll independently and briefly run different bundle versions, so the renderer can receive SSR requests for a bundle it hasn't cached. Mitigate this with a ",s.createElement(n.a,{href:"/linked-assets/8dc4fb6f64b752a9f7b0bc4e58254f59/rolling-deploy-adapters.md"},"rolling-deploy adapter")," (Pro): it pre-seeds the new renderer with the draining bundle and makes the renderer-before-Rails ordering explicit. This risk (and that mitigation) is ",s.createElement(n.strong,null,"specific to this separate-workloads option")," â Options 1â2 share one lifecycle and cannot drift."),"\n",s.createElement(n.li,null,s.createElement(n.strong,null,"Race conditions")," â Pods restart independently, which can cause transient connection errors."),"\n",s.createElement(n.li,null,s.createElement(n.strong,null,"Network dependency")," â Renderer must be reachable via internal service DNS."),"\n",s.createElement(n.li,null,s.createElement(n.strong,null,"Overkill for most setups")," â If you're running 2â4 replicas, independent scaling adds complexity without real benefit."),"\n"),"\n",s.createElement(n.p,null,s.createElement(n.strong,null,"Configuration:")),"\n",s.createElement(n.span,{dangerouslySetInnerHTML:{__html:'<div class="gatsby-highlight" data-language="ruby"><pre class="language-ruby"><code class="language-ruby"><span class="token comment">
1# config/initializers/react_on_rails_pro.rb</span>\nReactOnRailsPro<span class="token punctuation">.</span>configure <span class="token keyword">do</span> <span class="token operator">|</span>config<span class="token operator">|</span>\n config<span class="token punctuation">.</span>server_renderer <span class="token operator">=</span> <span class="token string-literal"><span class="token string">"NodeRenderer"</span></span>\n config<span class="token punctuation">.</span>renderer_url <span class="token operator">=</span> <span class="token constant">ENV</span><span class="token punctuation">.</span>fetch<span class="token punctuation">(</span><span class="token string-literal"><span class="token string">"RENDERER_URL"</span></span><span class="token punctuation">,</span> <span class="token string-literal"><span class="token string">"http://node-renderer-service:3800"</span></span><span class="token punctuation">)</span>\n<span class="token keyword">end</span></code></pre></div>'}}),"\n",s.createElement(n.blockquote,null,"\n",s.createElement(n.p,null,s.createElement(n.strong,null,"Recommendation:")," Start with a single container. Move to sidecar containers if you need per-process memory/CPU visibility (e.g., to diagnose OOM restarts). Separate workloads are rarely justified unless you have a specific need for independent scaling at high replica counts."),"\n"),"\n",s.createElement(n.h3,{id:"rollout-vm-capacity-and-memory-tradeoff",style:{position:"relative"}},s.createElement(n.a,{href:"#rollout-vm-capacity-and-memory-tradeoff","aria-label":"rollout vm capacity and memory tradeoff permalink",className:"anchor before"},s.createElement(n.span,{dangerouslySetInnerHTML:{__html:'<svg aria-hidden="true" height="20" version="1.1" viewBox="0 0 16 16" width="20"><path fill-rule="evenodd" d="M4 9h1v1H4c-1.5 0-3-1.69-3-3.5S2.55 3 4 3h4c1.45 0 3 1.69 3 3.5 0 1.41-.91 2.72-2 3.25V8.59c.58-.45 1-1.27 1-2.09C10 5.22 8.98 4 8 4H4c-.98 0-2 1.22-2 2.5S3 9 4 9zm9-3h-1v1h1c1 0 2 1.22 2 2.5S13.98 12 13 12H9c-.98 0-2-1.22-2-2.5 0-.83.42-1.64 1-2.09V6.25c-1.09.53-2 1.84-2 3.25C6 11.31 7.55 13 9 13h4c1.45 0 3-1.69 3-3.5S14.5 6 13 6z"></path></svg>'}})),"Rollout VM capacity and memory tradeoff"),"\n",s.createElement(n.p,null,"The Node Renderer retains compiled VM contexts per worker. Size its hard cap with:"),"\n",s.createElement(n.span,{dangerouslySetInnerHTML:{__html:'<div class="gatsby-highlight" data-language="text"><pre class="language-text"><code class="language-text">maxVMPoolSize >= simultaneous bundle generations reaching one renderer à contexts per generation</code></pre></div>'}}),"\n",s.createElement(n.p,null,"Contexts per generation are ",s.createElement(n.span,{dangerouslySetInnerHTML:{__html:'<code class="language-text">1</code>'}})," for SSR-only and ",s.createElement(n.span,{dangerouslySetInnerHTML:{__html:'<code class="language-text">2</code>'}})," when RSC is enabled. The package default is ",s.createElement(n.span,{dangerouslySetInnerHTML:{__html:'<code class="language-text">4</code>'}}),", which covers the common separate-workload overlap of old and new RSC-enabled Rails revisions (",s.createElement(n.span,{dangerouslySetInnerHTML:{__html:'<code class="language-text">2 à 2</code>'}}),"). Pooled VM memory grows approximately with ",s.createElement(n.span,{dangerouslySetInnerHTML:{__html:'<code class="language-text">workersCount à pooled contexts per worker</code>'}}),", plus each worker's base V8 heap and source-map overhead. In-flight requests can temporarily retain evicted execution contexts and source maps outside the pool, so measure concurrent workload headroom with your own bundles before reducing container limits."),"\n",s.createElement(n.p,null,"Topology changes the first factor:"),"\n",s.createElement(n.ul,null,"\n",s.createElement(n.li,null,s.createElement(n.strong,null,"Single container:")," An atomic app revision normally sends Rails requests only to the renderer in the same container, so one generation reaches each renderer. The default leaves rollout headroom; ",s.createElement(n.span,{dangerouslySetInnerHTML:{__html:'<code class="language-text">2</code>'}})," is the RSC minimum only if that lifecycle guarantee is durable."),"\n",s.createElement(n.li,null,s.createElement(n.strong,null,"Sidecar containers:")," ",s.createElement(n.span,{dangerouslySetInnerHTML:{__html:'<code class="language-text">localhost</code>'}})," proves only a shared pod network namespace. Treat the topology as one generation per renderer only when both containers are in the same pod template and roll as one unit with aligned images and startup gates. It does not prove that Rails and Node inherited the same environment variables."),"\n",s.createElement(n.li,null,s.createElement(n.strong,null,"Separate workloads:")," A shared renderer service receives requests from draining and current Rails revisions. Use at least ",s.createElement(n.span,{dangerouslySetInnerHTML:{__html:'<code class="language-text">4</code>'}})," for RSC, deploy and warm the renderer first, and increase the cap if more than two Rails revisions can overlap."),"\n"),"\n",s.createElement(n.p,null,"An advanced deployment can use generation-affinity routing so each renderer receives only one Rails revision, but this package does not create, enforce, or diagnose that routing guarantee. Without such a platform-level guarantee, do not lower the generation factor. The revision-scoped pre-seed declaration supplies trusted current identity; incoming nonmatching requests are draining generations and cannot demote it."),"\n",s.createElement(n.p,null,s.createElement(n.span,{dangerouslySetInnerHTML:{__html:'<code class="language-text">react_on_rails:doctor</code>'}})," uses the conservative old/new formula and reports whether capacity evidence is ",s.createElement(n.span,{dangerouslySetInnerHTML:{__html:'<code class="language-text">observed</code>'}})," or ",s.createElement(n.span,{dangerouslySetInnerHTML:{__html:'<code class="language-text">unverified</code>'}}),". It never treats a loopback ",s.createElement(n.span,{dangerouslySetInnerHTML:{__html:'<code class="language-text">renderer_url</code>'}})," or a Rails-process environment variable as proof of live sidecar configuration. For a separate renderer workload, confirm ",s.createElement(n.span,{dangerouslySetInnerHTML:{__html:'<code class="language-text">MAX_VM_POOL_SIZE</code>'}})," or ",s.createElement(n.span,{dangerouslySetInnerHTML:{__html:'<code class="language-text">maxVMPoolSize</code>'}})," in that workload directly."),"\n",s.createElement(n.p,null,"Pre-seeding populates the on-disk cache and emits an immutable declaration for the new revision. Set ",s.createElement(n.span,{dangerouslySetInnerHTML:{__html:'<code class="language-text">RENDERER_CURRENT_GENERATION_MANIFEST</code>'}})," to that exact file so every worker compiles and pins the declared server plus optional RSC contexts before listening. Inactive nonmatching bundle sets drain after ",s.createElement(n.span,{dangerouslySetInnerHTML:{__html:'<code class="language-text">vmPoolRolloutDrainTimeout</code>'}})," (60 seconds by default); see ",s.createElement(n.a,{href:"./js-configuration.md#sizing-and-draining-the-vm-pool"}
1,"Node Renderer JavaScript Configuration"),"."),"\n",s.createElement(n.p,null,"The exact pooled-context upper bound across the live fleet is:"),"\n",s.createElement(n.span,{dangerouslySetInnerHTML:{__html:'<div class="gatsby-highlight" data-language="text"><pre class="language-text"><code class="language-text">maximum pooled contexts = overlapping renderer replicas à effective workers per replica à maxVMPoolSize</code></pre></div>'}}),"\n",s.createElement(n.p,null,"Count old replicas plus the new rollout surge. ",s.createElement(n.span,{dangerouslySetInnerHTML:{__html:'<code class="language-text">workersCount: 0</code>'}})," is one effective worker. For each replica, budget its memory request and limit from the worker/pooled-context term plus measured master-process, base-heap, bundle-buffer, and safety overhead for source maps and evicted execution contexts held by concurrent in-flight requests. Multiply the per-replica request by the maximum overlapping replica count to plan the cluster reservation. A higher VM cap can stop rebuild churn while still causing OOMs if rollout surge or in-flight concurrency was omitted from capacity planning."),"\n",s.createElement(n.p,null,"The revision-scoped declaration is the supported eager VM-prewarm seam. The built-in ",s.createElement(n.span,{dangerouslySetInnerHTML:{__html:'<code class="language-text">/ready</code>'}})," check then proves that the answering worker's complete declared current set is compiled. If the hard cap cannot hold that set, startup fails explicitly; it never starts an unbounded pool or reports a false warm pass. Without the declaration, readiness and request-time compilation retain their legacy compatibility semantics."),"\n",s.createElement(n.h3,{id:"recommended-rollout-sequence-by-topology",style:{position:"relative"}},s.createElement(n.a,{href:"#recommended-rollout-sequence-by-topology","aria-label":"recommended rollout sequence by topology permalink",className:"anchor before"},s.createElement(n.span,{dangerouslySetInnerHTML:{__html:'<svg aria-hidden="true" height="20" version="1.1" viewBox="0 0 16 16" width="20"><path fill-rule="evenodd" d="M4 9h1v1H4c-1.5 0-3-1.69-3-3.5S2.55 3 4 3h4c1.45 0 3 1.69 3 3.5 0 1.41-.91 2.72-2 3.25V8.59c.58-.45 1-1.27 1-2.09C10 5.22 8.98 4 8 4H4c-.98 0-2 1.22-2 2.5S3 9 4 9zm9-3h-1v1h1c1 0 2 1.22 2 2.5S13.98 12 13 12H9c-.98 0-2-1.22-2-2.5 0-.83.42-1.64 1-2.09V6.25c-1.09.53-2 1.84-2 3.25C6 11.31 7.55 13 9 13h4c1.45 0 3-1.69 3-3.5S14.5 6 13 6z"></path></svg>'}})),"Recommended rollout sequence by topology"),"\n",s.createElement(n.p,null,s.createElement(n.strong,null,"Single container")),"\n",s.createElement(n.ol,null,"\n",s.createElement(n.li,null,"Build one aligned Ruby+Node image and keep the build-time seed as a fallback."),"\n",s.createElement(n.li,null,"Before accepting traffic, run the release-time seed against the draining deployment."),"\n",s.createElement(n.li,null,"Start NodeRenderer, start Rails, wait for renderer liveness, and run an internal authenticated Rails smoke render if the process supervisor supports a pre-readiness gate."),"\n",s.createElement(n.li,null,"Mark the combined container ready, then drain the old combined revision."),"\n"),"\n",s.createElement(n.p,null,s.createElement(n.strong,null,"Sidecar containers in one pod")),"\n",s.createElement(n.ol,null,"\n",s.createElement(n.li,null,"Use a Ruby-capable init step to complete the release-time seed into the writable cache volume shared with the Node sidecar."),"\n",s.createElement(n.li,null,"Start the aligned Rails and Node sidecars from the same pod revision. Keep Rails public readiness false until Node liveness passes."),"\n",s.createElement(n.li,null,"Run internal authenticated Rails smoke requests for SSR and RSC when supported, then mark the pod ready."),"\n",s.createElement(n.li,null,"Roll and drain the pod as one lifecycle. A loopback URL alone is not evidence that the images, environment, cache volume, or lifecycle are aligned."),"\n"),"\n",s.createElement(n.p,null,s.createElement(n.strong,null,"Separate workloads")),"\n",s.createElement(n.ol,null,"\n",s.createElement(n.li,null,"Complete the release-time seed for the new renderer revision and make the cache available to that workload."),"\n",s.createElement(n.li,null,"Deploy the renderer first on private networking with ",s.createElement(n.span,{dangerouslySetInnerHTML:{__html:'<code class="language-text">RENDERER_PASSWORD</code>'}})," authentication. Wait until the seeded revision is live and ready."),"\n",s.createElement(n.li,null,"Deploy a new Rails candidate configured for that renderer. Keep it out of public traffic while authenticated SSR/RSC smoke requests run when the platform supports this gate."),"\n",s.createElement(n.li,null,"Make new Rails live, drain old Rails, keep old/new VM capacity for the full drain window, and only then remove obsolete renderer capacity."),"\n"),"\n",s.createElement(n.p,null,"Never expose a separately deployed renderer directly to the public network. The Node ",s.createElement(n.span,{dangerouslySetInnerHTML:{__html:'<code class="language-text">
1vm</code>'}})," module is not a security boundary; use private service discovery, firewall policy, and renderer password authentication."),"\n",s.createElement(n.h2,{id:"control-plane-deployment-shapes",style:{position:"relative"}},s.createElement(n.a,{href:"#control-plane-deployment-shapes","aria-label":"control plane deployment shapes permalink",className:"anchor before"},s.createElement(n.span,{dangerouslySetInnerHTML:{__html:'<svg aria-hidden="true" height="20" version="1.1" viewBox="0 0 16 16" width="20"><path fill-rule="evenodd" d="M4 9h1v1H4c-1.5 0-3-1.69-3-3.5S2.55 3 4 3h4c1.45 0 3 1.69 3 3.5 0 1.41-.91 2.72-2 3.25V8.59c.58-.45 1-1.27 1-2.09C10 5.22 8.98 4 8 4H4c-.98 0-2 1.22-2 2.5S3 9 4 9zm9-3h-1v1h1c1 0 2 1.22 2 2.5S13.98 12 13 12H9c-.98 0-2-1.22-2-2.5 0-.83.42-1.64 1-2.09V6.25c-1.09.53-2 1.84-2 3.25C6 11.31 7.55 13 9 13h4c1.45 0 3-1.69 3-3.5S14.5 6 13 6z"></path></svg>'}})),"Control Plane Deployment Shapes"),"\n",s.createElement(n.p,null,"For Control Plane deployments, choose the probe target based on where the node renderer runs. Control Plane configures\nprobes per container. The examples below keep the default h2c transport and therefore use ",s.createElement(n.span,{dangerouslySetInnerHTML:{__html:'<code class="language-text">tcpSocket</code>'}})," or h2c-aware\n",s.createElement(n.span,{dangerouslySetInnerHTML:{__html:'<code class="language-text">exec</code>'}})," probes. An HTTP/1.1 ",s.createElement(n.span,{dangerouslySetInnerHTML:{__html:'<code class="language-text">httpGet</code>'}})," probe is also available when both sides use the paired\n",s.createElement(n.a,{href:"#http-transport-h2c-or-http11"},"HTTP/1.1 configuration"),"."),"\n",s.createElement(n.p,null,s.createElement(n.a,{href:"https://github.com/shakacode/control-plane-flow"},"Control Plane Flow"),"'s default ",s.createElement(n.span,{dangerouslySetInnerHTML:{__html:'<code class="language-text">rails</code>'}})," template models Rails as a\nsingle-container standard workload. If you follow that template and run the renderer inside the Rails container,\nconfigure the Rails workload's probes rather than looking for a separate node-renderer container. If you split the\nrenderer into its own container or workload, add renderer-specific probes there."),"\n",s.createElement(n.h3,{id:"same-rails-container-rails-and-renderer-co-located",style:{position:"relative"}},s.createElement(n.a,{href:"#same-rails-container-rails-and-renderer-co-located","aria-label":"same rails container rails and renderer co located permalink",className:"anchor before"},s.createElement(n.span,{dangerouslySetInnerHTML:{__html:'<svg aria-hidden="true" height="20" version="1.1" viewBox="0 0 16 16" width="20"><path fill-rule="evenodd" d="M4 9h1v1H4c-1.5 0-3-1.69-3-3.5S2.55 3 4 3h4c1.45 0 3 1.69 3 3.5 0 1.41-.91 2.72-2 3.25V8.59c.58-.45 1-1.27 1-2.09C10 5.22 8.98 4 8 4H4c-.98 0-2 1.22-2 2.5S3 9 4 9zm9-3h-1v1h1c1 0 2 1.22 2 2.5S13.98 12 13 12H9c-.98 0-2-1.22-2-2.5 0-.83.42-1.64 1-2.09V6.25c-1.09.53-2 1.84-2 3.25C6 11.31 7.55 13 9 13h4c1.45 0 3-1.69 3-3.5S14.5 6 13 6z"></path></svg>'}})),"Same Rails Container (Rails and Renderer Co-Located)"),"\n",s.createElement(n.p,null,"Set the Rails ",s.createElement(n.span,{dangerouslySetInnerHTML:{__html:'<code class="language-text">renderer_url</code>'}})," to ",s.createElement(n.span,{dangerouslySetInnerHTML:{__html:'<code class="language-text">http://localhost:3800</code>'}}),". The renderer can keep the default ",s.createElement(n.span,{dangerouslySetInnerHTML:{__html:'<code class="language-text">localhost</code>'}})," host binding.\nProbe the ",s.createElement(n.span,{dangerouslySetInnerHTML:{__html:'<code class="language-text">rails</code>'}})," container's Rails health endpoint, such as ",s.createElement(n.span,{dangerouslySetInnerHTML:{__html:'<code class="language-text">/up</code>'}})," on port ",s.createElement(n.span,{dangerouslySetInnerHTML:{__html:'<code class="language-text">3000</code>'}})," in Rails 7.1+ or a custom endpoint in\nearlier Rails versions."),"\n",s.createElement(n.p,null,"This also applies when a process supervisor starts Rails and the renderer together in the same container: probe the\nRails container and let the Rails health endpoint cover the co-located renderer when needed."),"\n",s.createElement(n.p,null,"When Rails and the renderer share one container, use one combined Rails health endpoint if you need to check both\nprocesses. For example, make the Rails readiness endpoint perform a short TCP connection check to ",s.createElement(n.span,{dangerouslySetInnerHTML:{__html:'<code class="language-text">localhost:3800</code>'}})," and\nreturn ",s.createElement(n.span,{dangerouslySetInnerHTML:{__html:'<code class="language-text">503</code>'}})," if the renderer is unreachable."),"\n",s.createElement(n.p,null,"Because this guide covers React on Rails Pro's Node Renderer, the Rails endpoint below reads the same\n",s.createElement(n.span,{dangerouslySetInnerHTML:{__html:'<code class="language-text">ReactOnRailsPro.configuration.renderer_url</code>'}})," value used for SSR requests rather than requiring a second port environment\nvariable."),"\n",s.createElement(n.p,null,s.createElement(n.span,{dangerouslySetInnerHTML:{__html:'<code class="language-text">config/routes.rb</code>'}}),":"),"\n",s.createElement(n.span,{dangerouslySetInnerHTML:{__html:'<div class="gatsby-highlight" data-language="ruby"><pre class="language-ruby"><code class="language-ruby"><span class="token comment"># Override Rails 7.1+\'s built-in /up route to add the renderer TCP check.</span>\n<span class="token comment"># If you already have custom /up logic, use a distinct path such as /healthz</span>\n<span class="token comment"># to avoid silently replacing existing health behavior.</span>\nget <span class="token string-literal"><span class="token string">"up"</span></span><span class="token punctuation">,</span> <span class="token symbol">to</span><span class="token operator">:</span> <span class="token string-literal"><span class="token string">"health#show"</span></span></code></pre></div>'}}
1),"\n",s.createElement(n.p,null,"Loading the ",s.createElement(n.span,{dangerouslySetInnerHTML:{__html:'<code class="language-text">Socket</code>'}})," stdlib from an initializer keeps the controller body lean and outside the autoload boundary, which\nmatches Rails conventions for stdlib dependencies. If your environment already auto-requires stdlib (for example via\nBundler) the require below is harmless; placing it in an initializer is still preferred."),"\n",s.createElement(n.p,null,s.createElement(n.span,{dangerouslySetInnerHTML:{__html:'<code class="language-text">config/initializers/socket_require.rb</code>'}}),":"),"\n",s.createElement(n.span,{dangerouslySetInnerHTML:{__html:'<div class="gatsby-highlight" data-language="ruby"><pre class="language-ruby"><code class="language-ruby"><span class="token comment"># Load the Ruby stdlib Socket class once at boot for the Health controller\'s TCP check.</span>\n<span class="token keyword">require</span> <span class="token string-literal"><span class="token string">"socket"</span></span></code></pre></div>'}}),"\n",s.createElement(n.p,null,s.createElement(n.span,{dangerouslySetInnerHTML:{__html:'<code class="language-text">app/controllers/health_controller.rb</code>'}}),":"),"\n",s.createElement(n.span,{dangerouslySetInnerHTML:{__html:'<div class="gatsby-highlight" data-language="ruby"><pre class="language-ruby"><code class="language-ruby"><span class="token comment"># Inherits from ActionController::Base (not ApplicationController) to avoid</span>\n<span class="token comment"># app-level authentication callbacks on unauthenticated probe requests.</span>\n<span class="token comment"># `Socket` is loaded once at boot via config/initializers/socket_require.rb.</span>\n<span class="token keyword">class</span> <span class="token class-name">HealthController</span> <span class="token operator"><</span> ActionController<span class="token double-colon punctuation">::</span>Base\n <span class="token keyword">def</span> <span class="token method-definition"><span class="token function">show</span></span>\n <span class="token comment"># A successful TCP connect means the h2c listener is bound, not that cluster workers</span>\n <span class="token comment"># are ready. Pair with the startup probe to shield liveness during boot.</span>\n <span class="token comment"># In this same-container topology, Rails and the renderer share a network namespace,</span>\n <span class="token comment"># so always probe localhost even if other deployment shapes use a service host.</span>\n <span class="token comment"># connect_timeout is supported by the Ruby versions in this guide\'s prerequisites.</span>\n renderer_url <span class="token operator">=</span> ReactOnRailsPro<span class="token punctuation">.</span>configuration<span class="token punctuation">.</span>renderer_url\n <span class="token keyword">raise</span> ArgumentError<span class="token punctuation">,</span> <span class="token string-literal"><span class="token string">"renderer_url not configured"</span></span> <span class="token keyword">if</span> renderer_url<span class="token punctuation">.</span><span class="token keyword">nil</span><span class="token operator">?</span> <span class="token operator">||</span> renderer_url<span class="token punctuation">.</span>empty<span class="token operator">?</span>\n\n <span class="token comment"># Extract the explicit port from the URL authority, skipping any userinfo</span>\n <span class="token comment"># (`user@`, `user:pass@`) and supporting bracketed IPv6 hosts. URI#port would</span>\n <span class="token comment"># return 80/443 for URLs without an explicit port, so we read the string</span>\n <span class="token comment"># directly and fall back to 3800 when no authority port is present.</span>\n renderer_port <span class="token operator">=</span> renderer_url<span class="token punctuation">[</span><span class="token regex-literal"><span class="token regex">%r{://(?:[^/?#@]*@)?(?:\\[[^\\]]+\\]|[^/?#:]+):(\\d+)}</span></span><span class="token punctuation">,</span> <span class="token number">1</span><span class="token punctuation">]</span><span class="token operator">&.</span>to_i <span class="token operator">||</span> <span class="token number">3800</span>\n <span class="token comment"># Block form ensures the socket closes after a successful connect; the explicit</span>\n <span class="token comment"># |_socket| parameter signals that the empty body is intentional, not a typo.</span>\n Socket<span class="token punctuation">.</span>tcp<span class="token punctuation">(</span><span class="token string-literal"><span class="token string">"localhost"</span></span><span class="token punctuation">,</span> renderer_port<span class="token punctuation">,</span> <span class="token symbol">connect_timeout</span><span class="token operator">:</span> <span class="token number">1</span><span class="token punctuation">)</span> <span class="token punctuation">{</span> <span class="token operator">|</span>_socket<span class="token operator">|</span> <span class="token punctuation">}</span>\n head <span class="token symbol">:ok</span>\n <span class="token keyword">rescue</span> SocketError<span class="token punctuation">,</span> SystemCallError\n <span class="token comment"># Only renderer reachability failures map to 503. ArgumentError raised above</span>\n <span class="token comment"># intentionally escapes as 500 so a misconfigured `renderer_url` stays visible</span>\n <span class="token comment"># in logs and alerting.</span>\n head <span class="token symbol">:service_unavailable</span>\n <span class="token keyword">end</span>\n<span class="token keyword">end</span></code></pre></div>'}}),"\n",s.createElement(n.blockquote,null,"\n",s.createElement(n.p,null,s.createElement(n.strong,null,"Topology-specific:")," This same-container example always probes ",s.createElement(n.span,{dangerouslySetInnerHTML:{__html:'<code class="language-text">localhost</code>'}})," and only borrows the port from\n",s.createElement(n.span,{dangerouslySetInnerHTML:{__html:'<code class="language-text">renderer_url</code>'}}),". Do not reuse it as-is for sidecar or separate-workload topologies where the renderer runs behind a\ndifferent host."),"\n",s.createElement(n.p,null,"A missing ",s.createElement(n.span,{dangerouslySetInnerHTML:{__html:'<code class="language-text">renderer_url</code>'}})," raises ",s.createElement(n.span,{dangerouslySetInnerHTML:{__html:'<code class="language-text">ArgumentError</code>'}})," and surfaces as a 500 so the misconfiguration stays visible in logs\nand alerting. Only renderer reachability failures are converted to ",s.createElement(n.span,{dangerouslySetInnerHTML:{__html:'<code class="language-text">503</code>'}}),"."),"\n"),"\n",s.createElement(n.h3,{id:"separate-container-in-the-same-workload",style:{position:"relative"}}
1,s.createElement(n.a,{href:"#separate-container-in-the-same-workload","aria-label":"separate container in the same workload permalink",className:"anchor before"},s.createElement(n.span,{dangerouslySetInnerHTML:{__html:'<svg aria-hidden="true" height="20" version="1.1" viewBox="0 0 16 16" width="20"><path fill-rule="evenodd" d="M4 9h1v1H4c-1.5 0-3-1.69-3-3.5S2.55 3 4 3h4c1.45 0 3 1.69 3 3.5 0 1.41-.91 2.72-2 3.25V8.59c.58-.45 1-1.27 1-2.09C10 5.22 8.98 4 8 4H4c-.98 0-2 1.22-2 2.5S3 9 4 9zm9-3h-1v1h1c1 0 2 1.22 2 2.5S13.98 12 13 12H9c-.98 0-2-1.22-2-2.5 0-.83.42-1.64 1-2.09V6.25c-1.09.53-2 1.84-2 3.25C6 11.31 7.55 13 9 13h4c1.45 0 3-1.69 3-3.5S14.5 6 13 6z"></path></svg>'}})),"Separate Container In The Same Workload"),"\n",s.createElement(n.p,null,"Keep the Rails ",s.createElement(n.span,{dangerouslySetInnerHTML:{__html:'<code class="language-text">renderer_url</code>'}})," as ",s.createElement(n.span,{dangerouslySetInnerHTML:{__html:'<code class="language-text">http://localhost:3800</code>'}}),". Use ",s.createElement(n.span,{dangerouslySetInnerHTML:{__html:'<code class="language-text">0.0.0.0</code>'}})," for the renderer ",s.createElement(n.span,{dangerouslySetInnerHTML:{__html:'<code class="language-text">host</code>'}})," when you rely on\n",s.createElement(n.span,{dangerouslySetInnerHTML:{__html:'<code class="language-text">tcpSocket</code>'}})," probes; ",s.createElement(n.span,{dangerouslySetInnerHTML:{__html:'<code class="language-text">localhost</code>'}})," is fine for ",s.createElement(n.span,{dangerouslySetInnerHTML:{__html:'<code class="language-text">exec</code>'}}),"-only probes."),"\n",s.createElement(n.p,null,"Add h2c-aware ",s.createElement(n.span,{dangerouslySetInnerHTML:{__html:'<code class="language-text">exec</code>'}})," probes against ",s.createElement(n.span,{dangerouslySetInnerHTML:{__html:'<code class="language-text">localhost:3800</code>'}})," or ",s.createElement(n.span,{dangerouslySetInnerHTML:{__html:'<code class="language-text">tcpSocket</code>'}})," probes on the renderer port. For ",s.createElement(n.span,{dangerouslySetInnerHTML:{__html:'<code class="language-text">tcpSocket</code>'}}),", bind\nthe renderer to ",s.createElement(n.span,{dangerouslySetInnerHTML:{__html:'<code class="language-text">0.0.0.0</code>'}})," because Kubernetes and platform TCP probes are initiated by the kubelet and connect to the pod\nor workload IP, not container-local loopback. Railsârenderer HTTP traffic over ",s.createElement(n.span,{dangerouslySetInnerHTML:{__html:'<code class="language-text">localhost</code>'}})," still works inside the shared\npod network namespace (see ",s.createElement(n.a,{href:"#host-binding-for-container-environments"},"Host Binding for Container Environments"),"); only\nexternally-initiated probes need the ",s.createElement(n.span,{dangerouslySetInnerHTML:{__html:'<code class="language-text">0.0.0.0</code>'}})," bind. ",s.createElement(n.span,{dangerouslySetInnerHTML:{__html:'<code class="language-text">exec</code>'}})," probes run a command inside the container, so ",s.createElement(n.span,{dangerouslySetInnerHTML:{__html:'<code class="language-text">localhost</code>'}}),"\nworks there."),"\n",s.createElement(n.blockquote,null,"\n",s.createElement(n.p,null,s.createElement(n.strong,null,"Probe YAML:")," For Control Plane readiness and liveness fields, reuse the individual ",s.createElement(n.span,{dangerouslySetInnerHTML:{__html:'<code class="language-text">exec</code>'}})," or ",s.createElement(n.span,{dangerouslySetInnerHTML:{__html:'<code class="language-text">tcpSocket</code>'}})," probe blocks\nfrom ",s.createElement(n.a,{href:"#kubernetes-sidecar-manifest"},"Kubernetes Sidecar Manifest"),". Attach them to the node-renderer container in this\nworkload instead of to a separate Kubernetes pod spec."),"\n"),"\n",s.createElement(n.h3,{id:"separate-node-renderer-workload",style:{position:"relative"}},s.createElement(n.a,{href:"#separate-node-renderer-workload","aria-label":"separate node renderer workload permalink",className:"anchor before"},s.createElement(n.span,{dangerouslySetInnerHTML:{__html:'<svg aria-hidden="true" height="20" version="1.1" viewBox="0 0 16 16" width="20"><path fill-rule="evenodd" d="M4 9h1v1H4c-1.5 0-3-1.69-3-3.5S2.55 3 4 3h4c1.45 0 3 1.69 3 3.5 0 1.41-.91 2.72-2 3.25V8.59c.58-.45 1-1.27 1-2.09C10 5.22 8.98 4 8 4H4c-.98 0-2 1.22-2 2.5S3 9 4 9zm9-3h-1v1h1c1 0 2 1.22 2 2.5S13.98 12 13 12H9c-.98 0-2-1.22-2-2.5 0-.83.42-1.64 1-2.09V6.25c-1.09.53-2 1.84-2 3.25C6 11.31 7.55 13 9 13h4c1.45 0 3-1.69 3-3.5S14.5 6 13 6z"></path></svg>'}})),"Separate Node-Renderer Workload"),"\n",s.createElement(n.p,null,"Set the Rails ",s.createElement(n.span,{dangerouslySetInnerHTML:{__html:'<code class="language-text">renderer_url</code>'}})," to ",s.createElement(n.span,{dangerouslySetInnerHTML:{__html:'<code class="language-text">http://<WORKLOAD_NAME>.<GVC_NAME>.cpln.local:3800</code>'}}),", use ",s.createElement(n.span,{dangerouslySetInnerHTML:{__html:'<code class="language-text">0.0.0.0</code>'}})," for the renderer\n",s.createElement(n.span,{dangerouslySetInnerHTML:{__html:'<code class="language-text">
1host</code>'}}),", and add ",s.createElement(n.span,{dangerouslySetInnerHTML:{__html:'<code class="language-text">tcpSocket</code>'}})," or h2c-aware ",s.createElement(n.span,{dangerouslySetInnerHTML:{__html:'<code class="language-text">exec</code>'}})," probes to the node-renderer workload container. Expose the renderer port\ninternally, not publicly, unless required."),"\n",s.createElement(n.p,null,"Use the same Control Plane probe fields as the same-workload case, but attach them to the separate node-renderer workload\ncontainer."),"\n",s.createElement(n.p,null,"Replace ",s.createElement(n.span,{dangerouslySetInnerHTML:{__html:'<code class="language-text"><WORKLOAD_NAME></code>'}})," with the renderer workload name and ",s.createElement(n.span,{dangerouslySetInnerHTML:{__html:'<code class="language-text"><GVC_NAME></code>'}})," with your Control Plane Global Virtual Cloud\nname. Use your actual renderer port if it is not ",s.createElement(n.span,{dangerouslySetInnerHTML:{__html:'<code class="language-text">3800</code>'}}),"; see Control Plane's\n",s.createElement(n.a,{href:"https://docs.controlplane.com/guides/service-to-service"},"service-to-service endpoint format"),"."),"\n",s.createElement(n.h2,{id:"dockerfile-example",style:{position:"relative"}},s.createElement(n.a,{href:"#dockerfile-example","aria-label":"dockerfile example permalink",className:"anchor before"},s.createElement(n.span,{dangerouslySetInnerHTML:{__html:'<svg aria-hidden="true" height="20" version="1.1" viewBox="0 0 16 16" width="20"><path fill-rule="evenodd" d="M4 9h1v1H4c-1.5 0-3-1.69-3-3.5S2.55 3 4 3h4c1.45 0 3 1.69 3 3.5 0 1.41-.91 2.72-2 3.25V8.59c.58-.45 1-1.27 1-2.09C10 5.22 8.98 4 8 4H4c-.98 0-2 1.22-2 2.5S3 9 4 9zm9-3h-1v1h1c1 0 2 1.22 2 2.5S13.98 12 13 12H9c-.98 0-2-1.22-2-2.5 0-.83.42-1.64 1-2.09V6.25c-1.09.53-2 1.84-2 3.25C6 11.31 7.55 13 9 13h4c1.45 0 3-1.69 3-3.5S14.5 6 13 6z"></path></svg>'}})),"Dockerfile Example"),"\n",s.createElement(n.blockquote,null,"\n",s.createElement(n.p,null,s.createElement(n.strong,null,"Why the renderer entry point lives in a dedicated ",s.createElement(n.span,{dangerouslySetInnerHTML:{__html:'<code class="language-text">renderer/</code>'}})," directory:")," Production Docker builds commonly strip JavaScript sources after the client bundles are built, since the Rails app no longer needs them at runtime. Keeping the renderer entry point in its own top-level directory (separate from ",s.createElement(n.span,{dangerouslySetInnerHTML:{__html:'<code class="language-text">client/</code>'}}),") makes it trivial to exclude from that cleanup â the Node Renderer process still needs its entry file and dependencies at runtime."),"\n"),"\n",s.createElement(n.p,null,"A minimal Dockerfile that bundles Rails and the Node Renderer in a single image:"),"\n",s.createElement(n.span,{dangerouslySetInnerHTML:{__html:'<div class="gatsby-highlight" data-language="dockerfile"><pre class="language-dockerfile"><code class="language-dockerfile"><span class="token instruction"><span class="token keyword">FROM</span> node:22-slim <span class="token keyword">AS</span> node</span>\n<span class="token instruction"><span class="token keyword">FROM</span> ruby:3.3</span>\n\n<span class="token comment"># Copy Node.js from the official image (avoids curl-pipe-bash)</span>\n<span class="token instruction"><span class="token keyword">COPY</span> <span class="token options"><span class="token property">--from</span><span class="token punctuation">=</span><span class="token string">node</span></span> /usr/local/bin/node /usr/local/bin/node</span>\n<span class="token instruction"><span class="token keyword">COPY</span> <span class="token options"><span class="token property">--from</span><span class="token punctuation">=</span><span class="token string">node</span></span> /usr/local/lib/node_modules /usr/local/lib/node_modules</span>\n<span class="token instruction"><span class="token keyword">RUN</span> ln -s /usr/local/lib/node_modules/npm/bin/npm-cli.js /usr/local/bin/npm</span>\n\n<span class="token comment"># jemalloc for Rails memory (adjust path for arm64: aarch64-linux-gnu)</span>\n<span class="token comment"># and curl for h2c health checks (`curl --http2-prior-knowledge`)</span>\n<span class="token instruction"><span class="token keyword">RUN</span> apt-get update && apt-get install -y libjemalloc2 curl && rm -rf /var/lib/apt/lists/*</span>\n<span class="token instruction"><span class="token keyword">ENV</span> LD_PRELOAD=/usr/lib/x86_64-linux-gnu/libjemalloc.so.2</span>\n<span class="token instruction"><span class="token keyword">ENV</span> MALLOC_CONF=<span class="token string">"dirty_decay_ms:1000,muzzy_decay_ms:1000"</span></span>\n\n<span class="token instruction"><span class="token keyword">WORKDIR</span> /app</span>\n\n<span class="token comment"># Install Ruby and Node dependencies</span>\n<span class="token instruction"><span class="token keyword">COPY</span> Gemfile Gemfile.lock ./</span>\n<span class="token instruction"><span class="token keyword">RUN</span> bundle install</span>\n\n<span class="token instruction"><span class="token keyword">COPY</span> package.json package-lock.json ./</span>\n<span class="token instruction"><span class="token keyword">RUN</span> npm ci</span>\n\n<span class="token instruction"><span class="token keyword">COPY</span> . .</span>\n\n<span class="token comment"># Precompile assets</span>\n<span class="token instruction"><span class="token keyword">RUN</span> bundle exec rake assets:precompile</span>\n\n<span class="token comment"># Expose Rails and Node Renderer ports</span>\n<span class="token instruction"><span class="token keyword">EXPOSE</span> 3000 3800</span>\n\n<span class="token comment"># Start both processes with a process manager (overmind, foreman, etc.)</span>\n<span class="token comment"># See the Procfile example below</span>\n<span class="token instruction"><span class="token keyword">CMD</span> [<span class="token string">"overmind"</span>, <span class="token string">"start"</span>, <span class="token string">"-f"</span>, <span class="token string">"Procfile"</span>]</span></code></pre></div>'}}),"\n",s.createElement(n.p,null,"For the single-container pattern, use a process manager like ",s.createElement(n.a,{href:"https://github.com/DarthSim/overmind"},"overmind")," or ",s.createElement(n.a,{href:"https://github.com/ddollar/foreman"},"foreman")," with a ",s.createElement(n.span,{dangerouslySetInnerHTML:{__html:'<code class="language-text">Procfile</code>'}}),":"),"\n",s.createElement(n.span,{dangerouslySetInnerHTML:{__html:'<div class="gatsby-highlight" data-language="text"><pre class="language-text"><code class="language-text">
1# Procfile\nrails: bundle exec rails server -b 0.0.0.0 -p 3000\nrenderer: node renderer/node-renderer.js</code></pre></div>'}}),"\n",s.createElement(n.blockquote,null,"\n",s.createElement(n.p,null,s.createElement(n.strong,null,"Tip:")," For sidecar containers, use the same image but override the ",s.createElement(n.span,{dangerouslySetInnerHTML:{__html:'<code class="language-text">CMD</code>'}})," â one container runs ",s.createElement(n.span,{dangerouslySetInnerHTML:{__html:'<code class="language-text">bundle exec rails server</code>'}}),", the other runs ",s.createElement(n.span,{dangerouslySetInnerHTML:{__html:'<code class="language-text">node renderer/node-renderer.js</code>'}})," (or your Node Renderer entry point)."),"\n"),"\n",s.createElement(n.h2,{id:"docker-compose-example",style:{position:"relative"}},s.createElement(n.a,{href:"#docker-compose-example","aria-label":"docker compose example permalink",className:"anchor before"},s.createElement(n.span,{dangerouslySetInnerHTML:{__html:'<svg aria-hidden="true" height="20" version="1.1" viewBox="0 0 16 16" width="20"><path fill-rule="evenodd" d="M4 9h1v1H4c-1.5 0-3-1.69-3-3.5S2.55 3 4 3h4c1.45 0 3 1.69 3 3.5 0 1.41-.91 2.72-2 3.25V8.59c.58-.45 1-1.27 1-2.09C10 5.22 8.98 4 8 4H4c-.98 0-2 1.22-2 2.5S3 9 4 9zm9-3h-1v1h1c1 0 2 1.22 2 2.5S13.98 12 13 12H9c-.98 0-2-1.22-2-2.5 0-.83.42-1.64 1-2.09V6.25c-1.09.53-2 1.84-2 3.25C6 11.31 7.55 13 9 13h4c1.45 0 3-1.69 3-3.5S14.5 6 13 6z"></path></svg>'}})),"Docker Compose Example"),"\n",s.createElement(n.p,null,"For local development with sidecar-like containers:"),"\n",s.createElement(n.span,{dangerouslySetInnerHTML:{__html:'<div class="gatsby-highlight" data-language="yaml"><pre class="language-yaml"><code class="language-yaml"><span class="token key atrule">services</span><span class="token punctuation">:</span>\n <span class="token key atrule">rails</span><span class="token punctuation">:</span>\n <span class="token key atrule">build</span><span class="token punctuation">:</span> .\n <span class="token key atrule">command</span><span class="token punctuation">:</span> bundle exec rails server <span class="token punctuation">-</span>b 0.0.0.0 <span class="token punctuation">-</span>p 3000\n <span class="token key atrule">ports</span><span class="token punctuation">:</span>\n <span class="token punctuation">-</span> <span class="token string">\'3000:3000\'</span>\n <span class="token key atrule">environment</span><span class="token punctuation">:</span>\n <span class="token key atrule">RENDERER_URL</span><span class="token punctuation">:</span> <span class="token string">\'http://renderer:3800\'</span>\n <span class="token key atrule">depends_on</span><span class="token punctuation">:</span>\n <span class="token key atrule">renderer</span><span class="token punctuation">:</span>\n <span class="token key atrule">condition</span><span class="token punctuation">:</span> service_healthy\n\n <span class="token key atrule">renderer</span><span class="token punctuation">:</span>\n <span class="token key atrule">build</span><span class="token punctuation">:</span> .\n <span class="token key atrule">command</span><span class="token punctuation">:</span> node renderer/node<span class="token punctuation">-</span>renderer.js\n <span class="token key atrule">ports</span><span class="token punctuation">:</span>\n <span class="token punctuation">-</span> <span class="token string">\'3800:3800\'</span>\n <span class="token key atrule">environment</span><span class="token punctuation">:</span>\n <span class="token key atrule">RENDERER_HOST</span><span class="token punctuation">:</span> <span class="token string">\'0.0.0.0\'</span>\n <span class="token key atrule">NODE_OPTIONS</span><span class="token punctuation">:</span> <span class="token string">\'--max-old-space-size=512\'</span>\n <span class="token key atrule">healthcheck</span><span class="token punctuation">:</span>\n <span class="token comment"># --max-time 2 leaves a 1 s buffer below the 3 s healthcheck timeout so curl exits</span>
1\n <span class="token comment"># cleanly with a non-zero code rather than being killed mid-request.</span>\n <span class="token key atrule">test</span><span class="token punctuation">:</span> <span class="token punctuation">[</span><span class="token string">\'CMD\'</span><span class="token punctuation">,</span> <span class="token string">\'curl\'</span><span class="token punctuation">,</span> <span class="token string">\'-sf\'</span><span class="token punctuation">,</span> <span class="token string">\'--max-time\'</span><span class="token punctuation">,</span> <span class="token string">\'2\'</span><span class="token punctuation">,</span> <span class="token string">\'--http2-prior-knowledge\'</span><span class="token punctuation">,</span> <span class="token string">\'http://localhost:3800/info\'</span><span class="token punctuation">]</span>\n <span class="token key atrule">interval</span><span class="token punctuation">:</span> 5s\n <span class="token key atrule">timeout</span><span class="token punctuation">:</span> 3s\n <span class="token key atrule">retries</span><span class="token punctuation">:</span> <span class="token number">5</span>\n <span class="token key atrule">start_period</span><span class="token punctuation">:</span> 10s</code></pre></div>'}}),"\n",s.createElement(n.blockquote,null,"\n",s.createElement(n.p,null,s.createElement(n.strong,null,"Note:")," In Docker Compose, the containers do not share a network namespace (unlike Kubernetes sidecars), so the renderer must bind to ",s.createElement(n.span,{dangerouslySetInnerHTML:{__html:'<code class="language-text">0.0.0.0</code>'}})," and Rails must connect via the service name (",s.createElement(n.span,{dangerouslySetInnerHTML:{__html:'<code class="language-text">renderer</code>'}}),").\nThe Compose example uses ",s.createElement(n.span,{dangerouslySetInnerHTML:{__html:'<code class="language-text">--max-time 2</code>'}})," with ",s.createElement(n.span,{dangerouslySetInnerHTML:{__html:'<code class="language-text">timeout: 3s</code>'}})," for fast local feedback; the Kubernetes examples use\n",s.createElement(n.span,{dangerouslySetInnerHTML:{__html:'<code class="language-text">--max-time 3</code>'}})," with ",s.createElement(n.span,{dangerouslySetInnerHTML:{__html:'<code class="language-text">timeoutSeconds: 5</code>'}})," to allow more scheduler and node-load jitter."),"\n"),"\n",s.createElement(n.h2,{id:"host-binding-for-container-environments",style:{position:"relative"}},s.createElement(n.a,{href:"#host-binding-for-container-environments","aria-label":"host binding for container environments permalink",className:"anchor before"},s.createElement(n.span,{dangerouslySetInnerHTML:{__html:'<svg aria-hidden="true" height="20" version="1.1" viewBox="0 0 16 16" width="20"><path fill-rule="evenodd" d="M4 9h1v1H4c-1.5 0-3-1.69-3-3.5S2.55 3 4 3h4c1.45 0 3 1.69 3 3.5 0 1.41-.91 2.72-2 3.25V8.59c.58-.45 1-1.27 1-2.09C10 5.22 8.98 4 8 4H4c-.98 0-2 1.22-2 2.5S3 9 4 9zm9-3h-1v1h1c1 0 2 1.22 2 2.5S13.98 12 13 12H9c-.98 0-2-1.22-2-2.5 0-.83.42-1.64 1-2.09V6.25c-1.09.53-2 1.84-2 3.25C6 11.31 7.55 13 9 13h4c1.45 0 3-1.69 3-3.5S14.5 6 13 6z"></path></svg>'}})),"Host Binding for Container Environments"),"\n",s.createElement(n.p,null,"By default, the Node Renderer binds to ",s.createElement(n.span,{dangerouslySetInnerHTML:{__html:'<code class="language-text">localhost</code>'}}),". For ",s.createElement(n.strong,null,"sidecar containers")," in the same Kubernetes pod, that works because the containers share a network namespace. For ",s.createElement(n.strong,null,"separate workloads")," or Docker Compose setups without shared networking, bind to ",s.createElement(n.span,{dangerouslySetInnerHTML:{__html:'<code class="language-text">0.0.0.0</code>'}}),":"),"\n",s.createElement(n.span,{dangerouslySetInnerHTML:{__html:'<div class="gatsby-highlight" data-language="javascript"><pre class="language-javascript"><code class="language-javascript"><span class="token comment">// renderer/node-renderer.js</span>\n<span class="token keyword">import</span> <span class="token punctuation">{</span> reactOnRailsProNodeRenderer <span class="token punctuation">}</span> <span class="token keyword">from</span> <span class="token string">\'react-on-rails-pro-node-renderer\'</span><span class="token punctuation">;</span>\n\n<span class="token keyword">const</span> config <span class="token operator">=</span> <span class="token punctuation">{</span>\n <span class="token literal-property property">host</span><span class="token operator">:</span> <span class="token string">
1\'0.0.0.0\'</span><span class="token punctuation">,</span>\n <span class="token comment">// ... other config</span>\n<span class="token punctuation">}</span><span class="token punctuation">;</span>\n\n<span class="token function">reactOnRailsProNodeRenderer</span><span class="token punctuation">(</span>config<span class="token punctuation">)</span><span class="token punctuation">;</span></code></pre></div>'}}),"\n",s.createElement(n.p,null,"Or via environment variable:"),"\n",s.createElement(n.span,{dangerouslySetInnerHTML:{__html:'<div class="gatsby-highlight" data-language="bash"><pre class="language-bash"><code class="language-bash"><span class="token assign-left variable">RENDERER_HOST</span><span class="token operator">=</span><span class="token number">0.0</span>.0.0</code></pre></div>'}}),"\n",s.createElement(n.blockquote,null,"\n",s.createElement(n.p,null,s.createElement(n.strong,null,"Security note:")," The renderer executes JavaScript via ",s.createElement(n.span,{dangerouslySetInnerHTML:{__html:'<code class="language-text">vm.runInContext()</code>'}}),", making it a remote code execution service. Binding to ",s.createElement(n.span,{dangerouslySetInnerHTML:{__html:'<code class="language-text">0.0.0.0</code>'}})," exposes this to the network. Use it only when the renderer must be reachable across a network namespace (separate workloads, Docker Compose). Always set ",s.createElement(n.span,{dangerouslySetInnerHTML:{__html:'<code class="language-text">RENDERER_PASSWORD</code>'}})," and place the renderer behind private networking when bound to ",s.createElement(n.span,{dangerouslySetInnerHTML:{__html:'<code class="language-text">0.0.0.0</code>'}}),". See ",s.createElement(n.a,{href:"./basics.md#network-security"},"Network Security")," for the full threat model."),"\n"),"\n",s.createElement(n.h2,{id:"optimizing-performance-per-cost",style:{position:"relative"}},s.createElement(n.a,{href:"#optimizing-performance-per-cost","aria-label":"optimizing performance per cost permalink",className:"anchor before"},s.createElement(n.span,{dangerouslySetInnerHTML:{__html:'<svg aria-hidden="true" height="20" version="1.1" viewBox="0 0 16 16" width="20"><path fill-rule="evenodd" d="M4 9h1v1H4c-1.5 0-3-1.69-3-3.5S2.55 3 4 3h4c1.45 0 3 1.69 3 3.5 0 1.41-.91 2.72-2 3.25V8.59c.58-.45 1-1.27 1-2.09C10 5.22 8.98 4 8 4H4c-.98 0-2 1.22-2 2.5S3 9 4 9zm9-3h-1v1h1c1 0 2 1.22 2 2.5S13.98 12 13 12H9c-.98 0-2-1.22-2-2.5 0-.83.42-1.64 1-2.09V6.25c-1.09.53-2 1.84-2 3.25C6 11.31 7.55 13 9 13h4c1.45 0 3-1.69 3-3.5S14.5 6 13 6z"></path></svg>'}})),"Optimizing Performance per Cost"),"\n",s.createElement(n.h3,{id:"memory-understanding-the-growth-pattern",style:{position:"relative"}},s.createElement(n.a,{href:"#memory-understanding-the-growth-pattern","aria-label":"memory understanding the growth pattern permalink",className:"anchor before"},s.createElement(n.span,{dangerouslySetInnerHTML:{__html:'<svg aria-hidden="true" height="20" version="1.1" viewBox="0 0 16 16" width="20"><path fill-rule="evenodd" d="M4 9h1v1H4c-1.5 0-3-1.69-3-3.5S2.55 3 4 3h4c1.45 0 3 1.69 3 3.5 0 1.41-.91 2.72-2 3.25V8.59c.58-.45 1-1.27 1-2.09C10 5.22 8.98 4 8 4H4c-.98 0-2 1.22-2 2.5S3 9 4 9zm9-3h-1v1h1c1 0 2 1.22 2 2.5S13.98 12 13 12H9c-.98 0-2-1.22-2-2.5 0-.83.42-1.64 1-2.09V6.25c-1.09.53-2 1.84-2 3.25C6 11.31 7.55 13 9 13h4c1.45 0 3-1.69 3-3.5S14.5 6 13 6z"></path></svg>'}})),"Memory: Understanding the Growth Pattern"),"\n",s.createElement(n.p,null,"The Node Renderer's memory grows over time as it handles SSR requests. This is ",s.createElement(n.strong,null,"expected behavior")," â V8's garbage collector doesn't always return memory to the OS, even after objects are freed. In containerized environments with cgroup memory limits, this can trigger OOM kills."),"\n",s.createElement(n.p,null,"Typical memory profile:"),"\n",s.createElement(n.ul,null,"\n",s.createElement(n.li,null,s.createElement(n.strong,null,"Startup:")," ~500â600 MB for the renderer process with workers"),"\n",s.createElement(n.li,null,s.createElement(n.strong,null,"After hours of traffic:")," 2â3 GB+ depending on component complexity and traffic volume"),"\n",s.createElement(n.li,null,s.createElement(n.strong,null,"Rails container:")," Usually stabilizes at 2â4 GB depending on ",s.createElement(n.span,{dangerouslySetInnerHTML:{__html:'<code class="language-text">WEB_CONCURRENCY</code>'}})," and ",s.createElement(n.span,{dangerouslySetInnerHTML:{__html:'<code class="language-text">RAILS_MAX_THREADS</code>'}})),"\n"),"\n",s.createElement(n.h3,{id:"constraining-node-memory",style:{position:"relative"}}
1,s.createElement(n.a,{href:"#constraining-node-memory","aria-label":"constraining node memory permalink",className:"anchor before"},s.createElement(n.span,{dangerouslySetInnerHTML:{__html:'<svg aria-hidden="true" height="20" version="1.1" viewBox="0 0 16 16" width="20"><path fill-rule="evenodd" d="M4 9h1v1H4c-1.5 0-3-1.69-3-3.5S2.55 3 4 3h4c1.45 0 3 1.69 3 3.5 0 1.41-.91 2.72-2 3.25V8.59c.58-.45 1-1.27 1-2.09C10 5.22 8.98 4 8 4H4c-.98 0-2 1.22-2 2.5S3 9 4 9zm9-3h-1v1h1c1 0 2 1.22 2 2.5S13.98 12 13 12H9c-.98 0-2-1.22-2-2.5 0-.83.42-1.64 1-2.09V6.25c-1.09.53-2 1.84-2 3.25C6 11.31 7.55 13 9 13h4c1.45 0 3-1.69 3-3.5S14.5 6 13 6z"></path></svg>'}})),"Constraining Node Memory"),"\n",s.createElement(n.p,null,"Use ",s.createElement(n.span,{dangerouslySetInnerHTML:{__html:'<code class="language-text">NODE_OPTIONS</code>'}})," to cap V8's old-generation heap per worker:"),"\n",s.createElement(n.span,{dangerouslySetInnerHTML:{__html:'<div class="gatsby-highlight" data-language="bash"><pre class="language-bash"><code class="language-bash"><span class="token assign-left variable">NODE_OPTIONS</span><span class="token operator">=</span><span class="token string">"--max-old-space-size=512"</span></code></pre></div>'}}),"\n",s.createElement(n.p,null,"This tells V8 to trigger garbage collection more aggressively and limits each worker's heap. Adjust the value based on your component complexity:"),"\n",s.createElement(n.table,null,s.createElement(n.thead,null,s.createElement(n.tr,null,s.createElement(n.th,null,"Component Complexity"),s.createElement(n.th,null,"Recommended ",s.createElement(n.span,{dangerouslySetInnerHTML:{__html:'<code class="language-text">max-old-space-size</code>'}})))),s.createElement(n.tbody,null,s.createElement(n.tr,null,s.createElement(n.td,null,"Simple components"),s.createElement(n.td,null,"256â512 MB")),s.createElement(n.tr,null,s.createElement(n.td,null,"Medium (Redux, large props)"),s.createElement(n.td,null,"512â768 MB")),s.createElement(n.tr,null,s.createElement(n.td,null,"Complex (large data, many components)"),s.createElement(n.td,null,"768â1024 MB")))),"\n",s.createElement(n.blockquote,null,"\n",s.createElement(n.p,null,s.createElement(n.strong,null,"Note:")," This setting applies per worker. Total renderer memory â ",s.createElement(n.span,{dangerouslySetInnerHTML:{__html:'<code class="language-text">max-old-space-size à workersCount + overhead</code>'}}),". If V8 hits the limit, the worker process exits and is automatically restarted by the cluster manager."),"\n"),"\n",s.createElement(n.h3,{id:"worker-count-tuning",style:{position:"relative"}},s.createElement(n.a,{href:"#worker-count-tuning","aria-label":"worker count tuning permalink",className:"anchor before"},s.createElement(n.span,{dangerouslySetInnerHTML:{__html:'<svg aria-hidden="true" height="20" version="1.1" viewBox="0 0 16 16" width="20"><path fill-rule="evenodd" d="M4 9h1v1H4c-1.5 0-3-1.69-3-3.5S2.55 3 4 3h4c1.45 0 3 1.69 3 3.5 0 1.41-.91 2.72-2 3.25V8.59c.58-.45 1-1.27 1-2.09C10 5.22 8.98 4 8 4H4c-.98 0-2 1.22-2 2.5S3 9 4 9zm9-3h-1v1h1c1 0 2 1.22 2 2.5S13.98 12 13 12H9c-.98 0-2-1.22-2-2.5 0-.83.42-1.64 1-2.09V6.25c-1.09.53-2 1.84-2 3.25C6 11.31 7.55 13 9 13h4c1.45 0 3-1.69 3-3.5S14.5 6 13 6z"></path></svg>'}})),"Worker Count Tuning"),"\n",s.createElement(n.p,null,"The default ",s.createElement(n.span,{dangerouslySetInnerHTML:{__html:'<code class="language-text">workersCount</code>'}})," is CPU count minus 1, which may over-allocate in containers. Set it explicitly:"),"\n",s.createElement(n.span,{dangerouslySetInnerHTML:{__html:'<div class="gatsby-highlight" data-language="javascript"><pre class="language-javascript"><code class="language-javascript"><span class="token keyword">const</span> config <span class="token operator">=</span> <span class="token punctuation">{</span>\n <span class="token literal-property property">workersCount</span><span class="token operator">:</span> <span class="token function">parseInt</span><span class="token punctuation">(</span>process<span class="token punctuation">.</span>env<span class="token punctuation">.</span><span class="token constant">RENDERER_WORKERS_COUNT</span><span class="token punctuation">,</span> <span class="token number">10</span><span class="token punctuation">)</span><span class="token punctuation">,</span> <span class="token comment">// Required â derive from the sizing formula below</span>\n<span class="token punctuation">}</span><span class="token punctuation">;</span></code></pre></div>'}}),"\n",s.createElement(n.p,null,s.createElement(n.strong,null,"Sizing guideline:")," Match worker count to expected concurrent SSR requests."),"\n",s.createElement(n.p,null,"A rough formula (assuming each SSR render takes roughly half a full Rails request cycle, so one renderer worker can serve ~2 concurrent threads â adjust the divisor based on your measured render times):"),"\n",s.createElement(n.span,{dangerouslySetInnerHTML:{__html:'<div class="gatsby-highlight" data-language="text"><pre class="language-text"><code class="language-text">renderer_workers ⥠(WEB_CONCURRENCY à RAILS_MAX_THREADS à ssr_request_ratio) / 2</code></pre></div>'}}),"\n",s.createElement(n.p,null,"Where ",s.createElement(n.span,{dangerouslySetInnerHTML:{__html:'<code class="language-text">ssr_request_ratio</code>'}})," is the fraction of requests that need server rendering (often 30â60% for hybrid apps)."),"\n",s.createElement(n.p,null,"Example: With ",s.createElement(n.span,{dangerouslySetInnerHTML:{__html:'<code class="language-text">WEB_CONCURRENCY=4</code>'}})," and ",s.createElement(n.span,{dangerouslySetInnerHTML:{__html:'<code class="language-text">RAILS_MAX_THREADS=8</code>'}})," (32 total Rails threads), and ~50% of requests needing SSR:"),"\n",s.createElement(n.span,{dangerouslySetInnerHTML:{__html:'<div class="gatsby-highlight" data-language="text"><pre class="language-text"><code class="language-text">renderer_workers ⥠(4 à 8 à 0.5) / 2 = 8 workers</code></pre></div>'}}),"\n",s.createElement(n.blockquote,null,"\n",s.createElement(n.p,null,s.createElement(n.strong,null,"Tip:")," Monitor CPU utilization per worker. If average CPU per worker is above 80%, you need more workers. If below 20%, you can reduce."),"\n"),"\n",s.createElement(n.h3,{id:"rolling-worker-restarts",style:{position:"relative"}}
1,s.createElement(n.a,{href:"#rolling-worker-restarts","aria-label":"rolling worker restarts permalink",className:"anchor before"},s.createElement(n.span,{dangerouslySetInnerHTML:{__html:'<svg aria-hidden="true" height="20" version="1.1" viewBox="0 0 16 16" width="20"><path fill-rule="evenodd" d="M4 9h1v1H4c-1.5 0-3-1.69-3-3.5S2.55 3 4 3h4c1.45 0 3 1.69 3 3.5 0 1.41-.91 2.72-2 3.25V8.59c.58-.45 1-1.27 1-2.09C10 5.22 8.98 4 8 4H4c-.98 0-2 1.22-2 2.5S3 9 4 9zm9-3h-1v1h1c1 0 2 1.22 2 2.5S13.98 12 13 12H9c-.98 0-2-1.22-2-2.5 0-.83.42-1.64 1-2.09V6.25c-1.09.53-2 1.84-2 3.25C6 11.31 7.55 13 9 13h4c1.45 0 3-1.69 3-3.5S14.5 6 13 6z"></path></svg>'}})),"Rolling Worker Restarts"),"\n",s.createElement(n.p,null,"To mitigate memory growth, enable periodic worker restarts:"),"\n",s.createElement(n.span,{dangerouslySetInnerHTML:{__html:'<div class="gatsby-highlight" data-language="javascript"><pre class="language-javascript"><code class="language-javascript"><span class="token keyword">const</span> config <span class="token operator">=</span> <span class="token punctuation">{</span>\n <span class="token literal-property property">allWorkersRestartInterval</span><span class="token operator">:</span> <span class="token number">360</span><span class="token punctuation">,</span> <span class="token comment">// Restart all workers every 6 hours</span>\n <span class="token literal-property property">delayBetweenIndividualWorkerRestarts</span><span class="token operator">:</span> <span class="token number">2</span><span class="token punctuation">,</span> <span class="token comment">// 2 minutes between each worker restart</span>\n <span class="token literal-property property">gracefulWorkerRestartTimeout</span><span class="token operator">:</span> <span class="token number">30</span><span class="token punctuation">,</span> <span class="token comment">// Kill stuck workers after 30 seconds</span>\n<span class="token punctuation">}</span><span class="token punctuation">;</span></code></pre></div>'}}),"\n",s.createElement(n.p,null,"This drains requests from each worker before restarting, so there's no downtime."),"\n",s.createElement(n.h3,{id:"container-resource-allocation",style:{position:"relative"}},s.createElement(n.a,{href:"#container-resource-allocation","aria-label":"container resource allocation permalink",className:"anchor before"},s.createElement(n.span,{dangerouslySetInnerHTML:{__html:'<svg aria-hidden="true" height="20" version="1.1" viewBox="0 0 16 16" width="20"><path fill-rule="evenodd" d="M4 9h1v1H4c-1.5 0-3-1.69-3-3.5S2.55 3 4 3h4c1.45 0 3 1.69 3 3.5 0 1.41-.91 2.72-2 3.25V8.59c.58-.45 1-1.27 1-2.09C10 5.22 8.98 4 8 4H4c-.98 0-2 1.22-2 2.5S3 9 4 9zm9-3h-1v1h1c1 0 2 1.22 2 2.5S13.98 12 13 12H9c-.98 0-2-1.22-2-2.5 0-.83.42-1.64 1-2.09V6.25c-1.09.53-2 1.84-2 3.25C6 11.31 7.55 13 9 13h4c1.45 0 3-1.69 3-3.5S14.5 6 13 6z"></path></svg>'}})),"Container Resource Allocation"),"\n",s.createElement(n.p,null,"Recommended starting points for sidecar configuration:"),"\n",s.createElement(n.table,null,s.createElement(n.thead,null,s.createElement(n.tr,null,s.createElement(n.th,null,"Container"),s.createElement(n.th,null,"CPU Request"),s.createElement(n.th,null,"CPU Limit"),s.createElement(n.th,null,"Memory Request"),s.createElement(n.th,null,"Memory Limit"))),s.createElement(n.tbody,null,s.createElement(n.tr,null,s.createElement(n.td,null,"Rails"),s.createElement(n.td,null,"1â2 cores"),s.createElement(n.td,null,"2â4 cores"),s.createElement(n.td,null,"4 GB"),s.createElement(n.td,null,"4 GB")),s.createElement(n.tr,null,s.createElement(n.td,null,"Node Renderer"),s.createElement(n.td,null,"1â2 cores"),s.createElement(n.td,null,"2â4 cores"),s.createElement(n.td,null,"4 GB"),s.createElement(n.td,null,"4 GB")))),"\n",s.createElement(n.blockquote,null,"\n",s.createElement(n.p,null,s.createElement(n.strong,null,"Important:")," Set memory ",s.createElement(n.strong,null,"requests")," equal to ",s.createElement(n.strong,null,"limits")," for the renderer container so its memory budget is explicit. Kubernetes QoS is determined at the pod level, so you only get ",s.createElement(n.span,{dangerouslySetInnerHTML:{__html:'<code class="language-text">Guaranteed</code>'}})," QoS when ",s.createElement(n.strong,null,"every")," container in the pod has matching requests and limits. If using ",s.createElement(n.span,{dangerouslySetInnerHTML:{__html:'<code class="language-text">--max-old-space-size</code>'}}),", set the container memory limit to ",s.createElement(n.span,{dangerouslySetInnerHTML:{__html:'<code class="language-text">max-old-space-size à workersCount à 1.5</code>'}})," to account for overhead."),"\n"),"\n",s.createElement(n.h3,{id:"jemalloc-for-rails-memory",style:{position:"relative"}}
1,s.createElement(n.a,{href:"#jemalloc-for-rails-memory","aria-label":"jemalloc for rails memory permalink",className:"anchor before"},s.createElement(n.span,{dangerouslySetInnerHTML:{__html:'<svg aria-hidden="true" height="20" version="1.1" viewBox="0 0 16 16" width="20"><path fill-rule="evenodd" d="M4 9h1v1H4c-1.5 0-3-1.69-3-3.5S2.55 3 4 3h4c1.45 0 3 1.69 3 3.5 0 1.41-.91 2.72-2 3.25V8.59c.58-.45 1-1.27 1-2.09C10 5.22 8.98 4 8 4H4c-.98 0-2 1.22-2 2.5S3 9 4 9zm9-3h-1v1h1c1 0 2 1.22 2 2.5S13.98 12 13 12H9c-.98 0-2-1.22-2-2.5 0-.83.42-1.64 1-2.09V6.25c-1.09.53-2 1.84-2 3.25C6 11.31 7.55 13 9 13h4c1.45 0 3-1.69 3-3.5S14.5 6 13 6z"></path></svg>'}})),"jemalloc for Rails Memory"),"\n",s.createElement(n.p,null,"Consider using ",s.createElement(n.a,{href:"https://github.com/jemalloc/jemalloc"},"jemalloc")," as Ruby's memory allocator to reduce Rails memory fragmentation:"),"\n",s.createElement(n.span,{dangerouslySetInnerHTML:{__html:'<div class="gatsby-highlight" data-language="dockerfile"><pre class="language-dockerfile"><code class="language-dockerfile"><span class="token instruction"><span class="token keyword">RUN</span> apt-get install -y libjemalloc2</span>\n<span class="token comment">## Adjust the preload path for your image architecture:</span>\n<span class="token comment">## amd64: /usr/lib/x86_64-linux-gnu/libjemalloc.so.2</span>\n<span class="token comment">## arm64: /usr/lib/aarch64-linux-gnu/libjemalloc.so.2</span>\n<span class="token instruction"><span class="token keyword">ENV</span> LD_PRELOAD=/usr/lib/x86_64-linux-gnu/libjemalloc.so.2</span>\n<span class="token instruction"><span class="token keyword">ENV</span> MALLOC_CONF=<span class="token string">"dirty_decay_ms:1000,muzzy_decay_ms:1000"</span></span></code></pre></div>'}}),"\n",s.createElement(n.p,null,"This won't help the Node Renderer (which uses V8's allocator), but can significantly reduce Rails container memory."),"\n",s.createElement(n.h2,{id:"graceful-shutdown",style:{position:"relative"}},s.createElement(n.a,{href:"#graceful-shutdown","aria-label":"graceful shutdown permalink",className:"anchor before"},s.createElement(n.span,{dangerouslySetInnerHTML:{__html:'<svg aria-hidden="true" height="20" version="1.1" viewBox="0 0 16 16" width="20"><path fill-rule="evenodd" d="M4 9h1v1H4c-1.5 0-3-1.69-3-3.5S2.55 3 4 3h4c1.45 0 3 1.69 3 3.5 0 1.41-.91 2.72-2 3.25V8.59c.58-.45 1-1.27 1-2.09C10 5.22 8.98 4 8 4H4c-.98 0-2 1.22-2 2.5S3 9 4 9zm9-3h-1v1h1c1 0 2 1.22 2 2.5S13.98 12 13 12H9c-.98 0-2-1.22-2-2.5 0-.83.42-1.64 1-2.09V6.25c-1.09.53-2 1.84-2 3.25C6 11.31 7.55 13 9 13h4c1.45 0 3-1.69 3-3.5S14.5 6 13 6z"></path></svg>'}})),"Graceful Shutdown"),"\n",s.createElement(n.p,null,"During rolling deploys and scale-down events, containers receive ",s.createElement(n.span,{dangerouslySetInnerHTML:{__html:'<code class="language-text">SIGTERM</code>'}})," before being killed. Both Rails and the Node Renderer need to handle this gracefully to avoid dropping in-flight requests."),"\n",s.createElement(n.h3,{id:"node-renderer",style:{position:"relative"}},s.createElement(n.a,{href:"#node-renderer","aria-label":"node renderer permalink",className:"anchor before"},s.createElement(n.span,{dangerouslySetInnerHTML:{__html:'<svg aria-hidden="true" height="20" version="1.1" viewBox="0 0 16 16" width="20"><path fill-rule="evenodd" d="M4 9h1v1H4c-1.5 0-3-1.69-3-3.5S2.55 3 4 3h4c1.45 0 3 1.69 3 3.5 0 1.41-.91 2.72-2 3.25V8.59c.58-.45 1-1.27 1-2.09C10 5.22 8.98 4 8 4H4c-.98 0-2 1.22-2 2.5S3 9 4 9zm9-3h-1v1h1c1 0 2 1.22 2 2.5S13.98 12 13 12H9c-.98 0-2-1.22-2-2.5 0-.83.42-1.64 1-2.09V6.25c-1.09.53-2 1.84-2 3.25C6 11.31 7.55 13 9 13h4c1.45 0 3-1.69 3-3.5S14.5 6 13 6z"></path></svg>'}})),"Node Renderer"),"\n",s.createElement(n.p,null,"The Node Renderer's cluster manager handles ",s.createElement(n.span,{dangerouslySetInnerHTML:{__html:'<code class="language-text">SIGTERM</code>'}})," automatically â it stops accepting new connections and waits for in-flight requests to complete before exiting. No additional configuration is needed."),"\n",s.createElement(n.p,null,"Set ",s.createElement(n.span,{dangerouslySetInnerHTML:{__html:'<code class="language-text">terminationGracePeriodSeconds</code>'}})," in your pod spec to give workers enough time to drain:"),"\n",s.createElement(n.span,{dangerouslySetInnerHTML:{__html:'<div class="gatsby-highlight" data-language="yaml"><pre class="language-yaml"><code class="language-yaml"><span class="token key atrule">spec</span><span class="token punctuation">:</span>\n <span class="token key atrule">terminationGracePeriodSeconds</span><span class="token punctuation">:</span> <span class="token number">60</span> <span class="token comment"># default is 30; increase if SSR requests are slow</span></code></pre></div>'}}
1),"\n",s.createElement(n.h3,{id:"rails",style:{position:"relative"}},s.createElement(n.a,{href:"#rails","aria-label":"rails permalink",className:"anchor before"},s.createElement(n.span,{dangerouslySetInnerHTML:{__html:'<svg aria-hidden="true" height="20" version="1.1" viewBox="0 0 16 16" width="20"><path fill-rule="evenodd" d="M4 9h1v1H4c-1.5 0-3-1.69-3-3.5S2.55 3 4 3h4c1.45 0 3 1.69 3 3.5 0 1.41-.91 2.72-2 3.25V8.59c.58-.45 1-1.27 1-2.09C10 5.22 8.98 4 8 4H4c-.98 0-2 1.22-2 2.5S3 9 4 9zm9-3h-1v1h1c1 0 2 1.22 2 2.5S13.98 12 13 12H9c-.98 0-2-1.22-2-2.5 0-.83.42-1.64 1-2.09V6.25c-1.09.53-2 1.84-2 3.25C6 11.31 7.55 13 9 13h4c1.45 0 3-1.69 3-3.5S14.5 6 13 6z"></path></svg>'}})),"Rails"),"\n",s.createElement(n.p,null,"Rails handles ",s.createElement(n.span,{dangerouslySetInnerHTML:{__html:'<code class="language-text">SIGTERM</code>'}})," via Puma's built-in graceful shutdown. Ensure ",s.createElement(n.span,{dangerouslySetInnerHTML:{__html:'<code class="language-text">terminationGracePeriodSeconds</code>'}})," exceeds your longest expected request duration."),"\n",s.createElement(n.blockquote,null,"\n",s.createElement(n.p,null,s.createElement(n.strong,null,"Tip:")," If either process doesn't shut down within ",s.createElement(n.span,{dangerouslySetInnerHTML:{__html:'<code class="language-text">terminationGracePeriodSeconds</code>'}}),", Kubernetes sends ",s.createElement(n.span,{dangerouslySetInnerHTML:{__html:'<code class="language-text">SIGKILL</code>'}}),". Monitor for exit code 137 in your logs, which may indicate the grace period is too short rather than an OOM kill."),"\n"),"\n",s.createElement(n.h2,{id:"autoscaling-considerations",style:{position:"relative"}},s.createElement(n.a,{href:"#autoscaling-considerations","aria-label":"autoscaling considerations permalink",className:"anchor before"},s.createElement(n.span,{dangerouslySetInnerHTML:{__html:'<svg aria-hidden="true" height="20" version="1.1" viewBox="0 0 16 16" width="20"><path fill-rule="evenodd" d="M4 9h1v1H4c-1.5 0-3-1.69-3-3.5S2.55 3 4 3h4c1.45 0 3 1.69 3 3.5 0 1.41-.91 2.72-2 3.25V8.59c.58-.45 1-1.27 1-2.09C10 5.22 8.98 4 8 4H4c-.98 0-2 1.22-2 2.5S3 9 4 9zm9-3h-1v1h1c1 0 2 1.22 2 2.5S13.98 12 13 12H9c-.98 0-2-1.22-2-2.5 0-.83.42-1.64 1-2.09V6.25c-1.09.53-2 1.84-2 3.25C6 11.31 7.55 13 9 13h4c1.45 0 3-1.69 3-3.5S14.5 6 13 6z"></path></svg>'}})),"Autoscaling Considerations"),"\n",s.createElement(n.p,null,"When using sidecar containers, autoscaling becomes more nuanced because CPU and memory metrics are aggregated across both containers."),"\n",s.createElement(n.h3,{id:"cpu-based-autoscaling",style:{position:"relative"}},s.createElement(n.a,{href:"#cpu-based-autoscaling","aria-label":"cpu based autoscaling permalink",className:"anchor before"},s.createElement(n.span,{dangerouslySetInnerHTML:{__html:'<svg aria-hidden="true" height="20" version="1.1" viewBox="0 0 16 16" width="20"><path fill-rule="evenodd" d="M4 9h1v1H4c-1.5 0-3-1.69-3-3.5S2.55 3 4 3h4c1.45 0 3 1.69 3 3.5 0 1.41-.91 2.72-2 3.25V8.59c.58-.45 1-1.27 1-2.09C10 5.22 8.98 4 8 4H4c-.98 0-2 1.22-2 2.5S3 9 4 9zm9-3h-1v1h1c1 0 2 1.22 2 2.5S13.98 12 13 12H9c-.98 0-2-1.22-2-2.5 0-.83.42-1.64 1-2.09V6.25c-1.09.53-2 1.84-2 3.25C6 11.31 7.55 13 9 13h4c1.45 0 3-1.69 3-3.5S14.5 6 13 6z"></path></svg>'}})),"CPU-Based Autoscaling"),"\n",s.createElement(n.p,null,"If you previously scaled on CPU at 75%, you may need to adjust:"),"\n",s.createElement(n.ul,null,"\n",s.createElement(n.li,null,s.createElement(n.strong,null,"Rails-heavy traffic")," (mostly API/JSON responses): Rails CPU spikes, but the renderer is idle. The pod-level CPU average may underreport Rails load."),"\n",s.createElement(n.li,null,s.createElement(n.strong,null,"SSR-heavy traffic"),": Both containers are active. Pod-level CPU is more representative."),"\n"),"\n",s.createElement(n.p,null,s.createElement(n.strong,null,"Recommendation:")," If your orchestrator supports per-container metrics (e.g., Kubernetes custom metrics), scale based on the Rails container's CPU. Otherwise, lower the threshold (e.g., to 60%) to account for the averaging effect."),"\n",s.createElement(n.h3,{id:"scaling-with-separate-workloads",style:{position:"relative"}}
1,s.createElement(n.a,{href:"#scaling-with-separate-workloads","aria-label":"scaling with separate workloads permalink",className:"anchor before"},s.createElement(n.span,{dangerouslySetInnerHTML:{__html:'<svg aria-hidden="true" height="20" version="1.1" viewBox="0 0 16 16" width="20"><path fill-rule="evenodd" d="M4 9h1v1H4c-1.5 0-3-1.69-3-3.5S2.55 3 4 3h4c1.45 0 3 1.69 3 3.5 0 1.41-.91 2.72-2 3.25V8.59c.58-.45 1-1.27 1-2.09C10 5.22 8.98 4 8 4H4c-.98 0-2 1.22-2 2.5S3 9 4 9zm9-3h-1v1h1c1 0 2 1.22 2 2.5S13.98 12 13 12H9c-.98 0-2-1.22-2-2.5 0-.83.42-1.64 1-2.09V6.25c-1.09.53-2 1.84-2 3.25C6 11.31 7.55 13 9 13h4c1.45 0 3-1.69 3-3.5S14.5 6 13 6z"></path></svg>'}})),"Scaling with Separate Workloads"),"\n",s.createElement(n.p,null,"With separate workloads, scale each independently:"),"\n",s.createElement(n.ul,null,"\n",s.createElement(n.li,null,s.createElement(n.strong,null,"Rails"),": Scale on CPU utilization or request queue depth."),"\n",s.createElement(n.li,null,s.createElement(n.strong,null,"Node Renderer"),": Scale on CPU utilization per worker."),"\n"),"\n",s.createElement(n.h2,{id:"tracking-and-responding-to-errors",style:{position:"relative"}},s.createElement(n.a,{href:"#tracking-and-responding-to-errors","aria-label":"tracking and responding to errors permalink",className:"anchor before"},s.createElement(n.span,{dangerouslySetInnerHTML:{__html:'<svg aria-hidden="true" height="20" version="1.1" viewBox="0 0 16 16" width="20"><path fill-rule="evenodd" d="M4 9h1v1H4c-1.5 0-3-1.69-3-3.5S2.55 3 4 3h4c1.45 0 3 1.69 3 3.5 0 1.41-.91 2.72-2 3.25V8.59c.58-.45 1-1.27 1-2.09C10 5.22 8.98 4 8 4H4c-.98 0-2 1.22-2 2.5S3 9 4 9zm9-3h-1v1h1c1 0 2 1.22 2 2.5S13.98 12 13 12H9c-.98 0-2-1.22-2-2.5 0-.83.42-1.64 1-2.09V6.25c-1.09.53-2 1.84-2 3.25C6 11.31 7.55 13 9 13h4c1.45 0 3-1.69 3-3.5S14.5 6 13 6z"></path></svg>'}})),"Tracking and Responding to Errors"),"\n",s.createElement(n.h3,{id:"startup-errors-err_stream_premature_close",style:{position:"relative"}},s.createElement(n.a,{href:"#startup-errors-err_stream_premature_close","aria-label":"startup errors err_stream_premature_close permalink",className:"anchor before"},s.createElement(n.span,{dangerouslySetInnerHTML:{__html:'<svg aria-hidden="true" height="20" version="1.1" viewBox="0 0 16 16" width="20"><path fill-rule="evenodd" d="M4 9h1v1H4c-1.5 0-3-1.69-3-3.5S2.55 3 4 3h4c1.45 0 3 1.69 3 3.5 0 1.41-.91 2.72-2 3.25V8.59c.58-.45 1-1.27 1-2.09C10 5.22 8.98 4 8 4H4c-.98 0-2 1.22-2 2.5S3 9 4 9zm9-3h-1v1h1c1 0 2 1.22 2 2.5S13.98 12 13 12H9c-.98 0-2-1.22-2-2.5 0-.83.42-1.64 1-2.09V6.25c-1.09.53-2 1.84-2 3.25C6 11.31 7.55 13 9 13h4c1.45 0 3-1.69 3-3.5S14.5 6 13 6z"></path></svg>'}})),"Startup Errors: ",s.createElement(n.span,{dangerouslySetInnerHTML:{__html:'<code class="language-text">ERR_STREAM_PREMATURE_CLOSE</code>'}})),"\n",s.createElement(n.p,null,"During container startup, you may see ",s.createElement(n.span,{dangerouslySetInnerHTML:{__html:'<code class="language-text">ERR_STREAM_PREMATURE_CLOSE</code>'}})," errors from Fastify. This occurs when Rails sends render requests before all Node Renderer workers are ready."),"\n",s.createElement(n.p,null,s.createElement(n.strong,null,"Mitigation:")),"\n",s.createElement(n.blockquote,null,"\n",s.createElement(n.p,null,"Probe semantics, timing values, and curl command flags are documented canonically in\n",s.createElement(n.a,{href:"./js-configuration.md#configuring-startup-readiness-and-liveness-probes"},"JS Configuration: Configuring Startup, Readiness, and Liveness Probes"),".\nThe full copy-paste YAML lives in ",s.createElement(n.a,{href:"#kubernetes-sidecar-manifest"},"Kubernetes Sidecar Manifest")," below. Update the JS\nConfiguration section first when tuning thresholds, then reflect the change in the manifest."),"\n"),"\n",s.createElement(n.ol,null,"\n",s.createElement(n.li,null,"\n",s.createElement(n.p,null,s.createElement(n.strong,null,"Health check endpoint")," - The Node Renderer exposes a built-in ",s.createElement(n.span,{dangerouslySetInnerHTML:{__html:'<code class="language-text">/info</code>'}})," endpoint that returns the node version and\nrenderer version. The default cleartext HTTP/2 listener requires a ",s.createElement(n.span,{dangerouslySetInnerHTML:{__html:'<code class="language-text">tcpSocket</code>'}})," probe or an ",s.createElement(n.span,{dangerouslySetInnerHTML:{__html:'<code class="language-text">exec</code>'}})," probe with an\nh2c-aware client such as ",s.createElement(n.span,{dangerouslySetInnerHTML:{__html:'<code class="language-text">curl --http2-prior-knowledge</code>'}}),". If the renderer and Rails client both use HTTP/1.1, a\nKubernetes ",s.createElement(n.span,{dangerouslySetInnerHTML:{__html:'<code class="language-text">httpGet</code>'}}
1)," probe can reach the renderer directly. For a custom ",s.createElement(n.span,{dangerouslySetInnerHTML:{__html:'<code class="language-text">/health</code>'}})," route with more granular checks, see\n",s.createElement(n.a,{href:"./js-configuration.md#adding-a-health-check-endpoint"},"JS Configuration: Adding a Health Check Endpoint"),"."),"\n"),"\n",s.createElement(n.li,null,"\n",s.createElement(n.p,null,s.createElement(n.strong,null,"Startup probe")," â A startup probe is the primary mitigation for this error. It defers readiness and liveness until\nthe renderer has bound its port, so Rails only sends render requests after workers are ready:"),"\n",s.createElement(n.span,{dangerouslySetInnerHTML:{__html:'<div class="gatsby-highlight" data-language="yaml"><pre class="language-yaml"><code class="language-yaml"><span class="token key atrule">startupProbe</span><span class="token punctuation">:</span>\n <span class="token key atrule">tcpSocket</span><span class="token punctuation">:</span>\n <span class="token key atrule">port</span><span class="token punctuation">:</span> <span class="token number">3800</span>\n <span class="token key atrule">initialDelaySeconds</span><span class="token punctuation">:</span> <span class="token number">10</span>\n <span class="token key atrule">periodSeconds</span><span class="token punctuation">:</span> <span class="token number">5</span>\n <span class="token key atrule">failureThreshold</span><span class="token punctuation">:</span> <span class="token number">6</span>\n <span class="token key atrule">timeoutSeconds</span><span class="token punctuation">:</span> <span class="token number">1</span></code></pre></div>'}}),"\n"),"\n",s.createElement(n.li,null,"\n",s.createElement(n.p,null,s.createElement(n.strong,null,"Readiness and liveness probes")," â Configure these alongside the startup probe so Rails only routes requests to a\nhealthy renderer and stuck containers get restarted. See\n",s.createElement(n.a,{href:"./js-configuration.md#configuring-startup-readiness-and-liveness-probes"},"Configuring Startup, Readiness, and Liveness Probes"),"\nfor probe-style choices, timing values, and curl command guidance, and\n",s.createElement(n.a,{href:"#kubernetes-sidecar-manifest"},"Kubernetes Sidecar Manifest")," for the full copy-paste YAML."),"\n"),"\n"),"\n",s.createElement(n.blockquote,null,"\n",s.createElement(n.p,null,s.createElement(n.strong,null,"Security:")," See the canonical ",s.createElement(n.a,{href:"./js-configuration.md#built-in-endpoints"},s.createElement(n.span,{dangerouslySetInnerHTML:{__html:'<code class="language-text">/info</code>'}})," security note")," for the\nunauthenticated-access caveat when ",s.createElement(n.span,{dangerouslySetInnerHTML:{__html:'<code class="language-text">password</code>'}})," is configured."),"\n"),"\n",s.createElement(n.h3,{id:"oom-tracking",style:{position:"relative"}},s.createElement(n.a,{href:"#oom-tracking","aria-label":"oom tracking permalink",className:"anchor before"},s.createElement(n.span,{dangerouslySetInnerHTML:{__html:'<svg aria-hidden="true" height="20" version="1.1" viewBox="0 0 16 16" width="20"><path fill-rule="evenodd" d="M4 9h1v1H4c-1.5 0-3-1.69-3-3.5S2.55 3 4 3h4c1.45 0 3 1.69 3 3.5 0 1.41-.91 2.72-2 3.25V8.59c.58-.45 1-1.27 1-2.09C10 5.22 8.98 4 8 4H4c-.98 0-2 1.22-2 2.5S3 9 4 9zm9-3h-1v1h1c1 0 2 1.22 2 2.5S13.98 12 13 12H9c-.98 0-2-1.22-2-2.5 0-.83.42-1.64 1-2.09V6.25c-1.09.53-2 1.84-2 3.25C6 11.31 7.55 13 9 13h4c1.45 0 3-1.69 3-3.5S14.5 6 13 6z"></path></svg>'}})),"OOM Tracking"),"\n",s.createElement(n.p,null,"Distinguish between Rails and Node Renderer OOM kills by checking container-level exit codes:"),"\n",s.createElement(n.ul,null,"\n",s.createElement(n.li,null,s.createElement(n.strong,null,"Exit code 137"),": Process received SIGKILL â commonly OOM from cgroup limit, but can also be forced termination (grace-period expiry, liveness probe failure). Check ",s.createElement(n.span,{dangerouslySetInnerHTML:{__html:'<code class="language-text">kubectl describe pod</code>'}})," for ",s.createElement(n.span,{dangerouslySetInnerHTML:{__html:'<code class="language-text">Reason: OOMKilled</code>'}})," to confirm OOM."),"\n",s.createElement(n.li,null,s.createElement(n.strong,null,"Exit code 1"),": Application crash (check logs for stack trace)."),"\n"),"\n",s.createElement(n.p,null,"With sidecar containers, your orchestrator should report which container was OOM-killed. Use this to tune resource limits for the specific container rather than
1increasing the entire pod."),"\n",s.createElement(n.h3,{id:"error-reporting",style:{position:"relative"}},s.createElement(n.a,{href:"#error-reporting","aria-label":"error reporting permalink",className:"anchor before"},s.createElement(n.span,{dangerouslySetInnerHTML:{__html:'<svg aria-hidden="true" height="20" version="1.1" viewBox="0 0 16 16" width="20"><path fill-rule="evenodd" d="M4 9h1v1H4c-1.5 0-3-1.69-3-3.5S2.55 3 4 3h4c1.45 0 3 1.69 3 3.5 0 1.41-.91 2.72-2 3.25V8.59c.58-.45 1-1.27 1-2.09C10 5.22 8.98 4 8 4H4c-.98 0-2 1.22-2 2.5S3 9 4 9zm9-3h-1v1h1c1 0 2 1.22 2 2.5S13.98 12 13 12H9c-.98 0-2-1.22-2-2.5 0-.83.42-1.64 1-2.09V6.25c-1.09.53-2 1.84-2 3.25C6 11.31 7.55 13 9 13h4c1.45 0 3-1.69 3-3.5S14.5 6 13 6z"></path></svg>'}})),"Error Reporting"),"\n",s.createElement(n.p,null,"See ",s.createElement(n.a,{href:"/linked-assets/3cc15217b5218a5e8de246cae7cb94b0/error-reporting-and-tracing.md"},"Error Reporting and Tracing")," for setting up Sentry, Honeybadger, or other error tracking with the Node Renderer."),"\n",s.createElement(n.h3,{id:"logging",style:{position:"relative"}},s.createElement(n.a,{href:"#logging","aria-label":"logging permalink",className:"anchor before"},s.createElement(n.span,{dangerouslySetInnerHTML:{__html:'<svg aria-hidden="true" height="20" version="1.1" viewBox="0 0 16 16" width="20"><path fill-rule="evenodd" d="M4 9h1v1H4c-1.5 0-3-1.69-3-3.5S2.55 3 4 3h4c1.45 0 3 1.69 3 3.5 0 1.41-.91 2.72-2 3.25V8.59c.58-.45 1-1.27 1-2.09C10 5.22 8.98 4 8 4H4c-.98 0-2 1.22-2 2.5S3 9 4 9zm9-3h-1v1h1c1 0 2 1.22 2 2.5S13.98 12 13 12H9c-.98 0-2-1.22-2-2.5 0-.83.42-1.64 1-2.09V6.25c-1.09.53-2 1.84-2 3.25C6 11.31 7.55 13 9 13h4c1.45 0 3-1.69 3-3.5S14.5 6 13 6z"></path></svg>'}})),"Logging"),"\n",s.createElement(n.p,null,"Set log levels to capture useful information without noise:"),"\n",s.createElement(n.span,{dangerouslySetInnerHTML:{__html:'<div class="gatsby-highlight" data-language="javascript"><pre class="language-javascript"><code class="language-javascript"><span class="token keyword">const</span> config <span class="token operator">=</span> <span class="token punctuation">{</span>\n <span class="token literal-property property">logLevel</span><span class="token operator">:</span> <span class="token string">\'info\'</span><span class="token punctuation">,</span> <span class="token comment">// General renderer logs</span>\n <span class="token literal-property property">logHttpLevel</span><span class="token operator">:</span> <span class="token string">\'error\'</span><span class="token punctuation">,</span> <span class="token comment">// Only log HTTP errors (not every request)</span>\n<span class="token punctuation">}</span><span class="token punctuation">;</span></code></pre></div>'}}),"\n",s.createElement(n.p,null,"In production, ",s.createElement(n.span,{dangerouslySetInnerHTML:{__html:"<code class=\"language-text\">logLevel: 'warn'</code>"}})," is sufficient unless actively debugging."),"\n",s.createElement(n.h2,{id:"kubernetes-sidecar-manifest",style:{position:"relative"}},s.createElement(n.a,{href:"#kubernetes-sidecar-manifest","aria-label":"kubernetes sidecar manifest permalink",className:"anchor before"},s.createElement(n.span,{dangerouslySetInnerHTML:{__html:'<svg aria-hidden="true" height="20" version="1.1" viewBox="0 0 16 16" width="20"><path fill-rule="evenodd" d="M4 9h1v1H4c-1.5 0-3-1.69-3-3.5S2.55 3 4 3h4c1.45 0 3 1.69 3 3.5 0 1.41-.91 2.72-2 3.25V8.59c.58-.45 1-1.27 1-2.09C10 5.22 8.98 4 8 4H4c-.98 0-2 1.22-2 2.5S3 9 4 9zm9-3h-1v1h1c1 0 2 1.22 2 2.5S13.98 12 13 12H9c-.98 0-2-1.22-2-2.5 0-.83.42-1.64 1-2.09V6.25c-1.09.53-2 1.84-2 3.25C6 11.31 7.55 13 9 13h4c1.45 0 3-1.69 3-3.5S14.5 6 13 6z"></path></svg>'}})),"Kubernetes Sidecar Manifest"),"\n",s.createElement(n.p,null,"A complete pod spec for the sidecar pattern. This is the canonical copy-paste YAML for renderer probes â other sections\nreference it instead of repeating the full block."),"\n",s.createElement(n.div,{className:"markdown-alert markdown-alert-note",dir:"auto"},"\n",s.createElement(n.p,{className:"markdown-alert-title",dir:"auto"},s.createElement(n.svg,{className:"octicon",viewBox:"0 0 16 16",width:"16",height:"16","aria-hidden":"true"},s.createElement(n.path,{d:"M0 8a8 8 0 1 1 16 0A8 8 0 0 1 0 8Zm8-6.5a6.5 6.5 0 1 0 0 13 6.5 6.5 0 0 0 0-13ZM6.5 7.75A.75.75 0 0 1 7.25 7h1a.75.75 0 0 1 .75.75v2.75h.25a.75.75 0 0 1 0 1.5h-2a.75.75 0 0 1 0-1.5h.25v-2h-.25a.75.75 0 0 1-.75-.75ZM8 6a1 1 0 1 1 0-2 1 1 0 0 1 0 2Z"})),"NOTE"),"\n",s.createElement(n.p,null,"The manifest uses an h2c-aware ",s.createElement(n.span,{dangerouslySetInnerHTML:{__html:'<code class="language-text">exec</code>'}})," probe for readiness and a ",s.createElement(n.span,{dangerouslySetInnerHTML:{__html:'<code class="language-text">tcpSocket</code>'}})," probe for liveness. Keep that split unless\nyou intentionally need stricter liveness detection and have verified curl HTTP/2 support in the image. Probe timing\nvalues and curl command guidance are canonical in\n",s.createElement(n.a,{href:"./js-configuration.md#configuring-startup-readiness-and-liveness-probes"},"JS Configuration: Configuring Startup, Readiness, and Liveness Probes"),";\nupdate that section first when tuning thresholds and reflect the change here."),"\n"),"\n",s.createElement(n.span,{dangerouslySetInnerHTML:{__html:'<div class="gatsby-highlight" data-language="yaml"><pre class="language-yaml"><code class="language-yaml"><span class="token key atrule">apiVersion</span><span class="token punctuation">:</span> apps/v1\n<span class="token key atrule">kind</span><span class="token punctuation">:</span> Deployment\n<span class="token key atrule">metadata</span><span class="token punctuation">:</span>\n <span class="token key atrule">name</span><span class="token punctuation">:</span> rails<span class="token punctuation">-</span>app\n<span class="token key atrule">spec</span><span class="token punctuation">:</span>\n <span class="token key atrule">replicas</span><span class="token punctuation">:</span> <span class="token number">2</span>\n <span class="token key atrule">selector</span><span class="token punctuation">:</span>\n <span class="token key atrule">matchLabels</span><span class="token punctuation">:</span>
1\n <span class="token key atrule">app</span><span class="token punctuation">:</span> rails<span class="token punctuation">-</span>app\n <span class="token key atrule">template</span><span class="token punctuation">:</span>\n <span class="token key atrule">metadata</span><span class="token punctuation">:</span>\n <span class="token key atrule">labels</span><span class="token punctuation">:</span>\n <span class="token key atrule">app</span><span class="token punctuation">:</span> rails<span class="token punctuation">-</span>app\n <span class="token key atrule">spec</span><span class="token punctuation">:</span>\n <span class="token key atrule">terminationGracePeriodSeconds</span><span class="token punctuation">:</span> <span class="token number">60</span>\n <span class="token key atrule">containers</span><span class="token punctuation">:</span>\n <span class="token punctuation">-</span> <span class="token key atrule">name</span><span class="token punctuation">:</span> rails\n <span class="token key atrule">image</span><span class="token punctuation">:</span> your<span class="token punctuation">-</span>app<span class="token punctuation">:</span>latest\n <span class="token key atrule">command</span><span class="token punctuation">:</span> <span class="token punctuation">[</span><span class="token string">\'bundle\'</span><span class="token punctuation">,</span> <span class="token string">\'exec\'</span><span class="token punctuation">,</span> <span class="token string">\'rails\'</span><span class="token punctuation">,</span> <span class="token string">\'server\'</span><span class="token punctuation">,</span> <span class="token string">\'-b\'</span><span class="token punctuation">,</span> <span class="token string">\'0.0.0.0\'</span><span class="token punctuation">]</span>\n <span class="token key atrule">ports</span><span class="token punctuation">:</span>\n <span class="token punctuation">-</span> <span class="token key atrule">containerPort</span><span class="token punctuation">:</span> <span class="token number">3000</span>\n <span class="token key atrule">env</span><span class="token punctuation">:</span>\n <span class="token punctuation">-</span> <span class="token key atrule">name</span><span class="token punctuation">:</span> RENDERER_URL\n <span class="token key atrule">value</span><span class="token punctuation">:</span> <span class="token string">\'http://localhost:3800\'</span>\n <span class="token punctuation">-</span> <span class="token key atrule">name</span><span class="token punctuation">:</span> LD_PRELOAD\n <span class="token key atrule">value</span><span class="token punctuation">:</span> <span class="token string">\'/usr/lib/x86_64-linux-gnu/libjemalloc.so.2\'</span>\n <span class="token punctuation">-</span> <span class="token key atrule">name</span><span class="token punctuation">:</span> MALLOC_CONF\n <span class="token key atrule">value</span><span class="token punctuation">:</span> <span class="token string">\'dirty_decay_ms:1000,muzzy_decay_ms:1000\'</span>\n <span class="token key atrule">resources</span><span class="token punctuation">:</span>\n <span class="token key atrule">requests</span><span class="token punctuation">:</span>\n <span class="token key atrule">cpu</span><span class="token punctuation">:</span> <span class="token string">\'1\'</span>\n <span class="token key atrule">memory</span><span class="token punctuation">:</span> <span class="token string">\'4Gi\'</span>\n <span class="token key atrule">limits</span><span class="token punctuation">:</span>\n <span class="token key atrule">cpu</span><span class="token punctuation">:</span> <span class="token string">\'2\'</span>\n <span class="token key atrule">memory</span><span class="token punctuation">:</span> <span class="token string">\'4Gi\'</span>\n\n <span class="token punctuation">-</span> <span class="token key atrule">name</span><span class="token punctuation">:</span> node<span class="token punctuation">-</span>
1renderer\n <span class="token key atrule">image</span><span class="token punctuation">:</span> your<span class="token punctuation">-</span>app<span class="token punctuation">:</span>latest <span class="token comment"># Same image as Rails</span>\n <span class="token key atrule">command</span><span class="token punctuation">:</span> <span class="token punctuation">[</span><span class="token string">\'node\'</span><span class="token punctuation">,</span> <span class="token string">\'renderer/node-renderer.js\'</span><span class="token punctuation">]</span>\n <span class="token key atrule">ports</span><span class="token punctuation">:</span>\n <span class="token punctuation">-</span> <span class="token key atrule">containerPort</span><span class="token punctuation">:</span> <span class="token number">3800</span>\n <span class="token key atrule">env</span><span class="token punctuation">:</span>\n <span class="token punctuation">-</span> <span class="token key atrule">name</span><span class="token punctuation">:</span> RENDERER_HOST\n <span class="token key atrule">value</span><span class="token punctuation">:</span> <span class="token string">\'0.0.0.0\'</span> <span class="token comment"># Bind to all interfaces for pod-network access</span>\n <span class="token punctuation">-</span> <span class="token key atrule">name</span><span class="token punctuation">:</span> NODE_OPTIONS\n <span class="token key atrule">value</span><span class="token punctuation">:</span> <span class="token string">\'--max-old-space-size=512\'</span>\n <span class="token punctuation">-</span> <span class="token key atrule">name</span><span class="token punctuation">:</span> RENDERER_WORKERS_COUNT\n <span class="token key atrule">value</span><span class="token punctuation">:</span> <span class="token string">\'4\'</span>\n <span class="token key atrule">resources</span><span class="token punctuation">:</span>\n <span class="token key atrule">requests</span><span class="token punctuation">:</span>\n <span class="token key atrule">cpu</span><span class="token punctuation">:</span> <span class="token string">\'1\'</span>\n <span class="token key atrule">memory</span><span class="token punctuation">:</span> <span class="token string">\'4Gi\'</span>\n <span class="token key atrule">limits</span><span class="token punctuation">:</span>\n <span class="token key atrule">cpu</span><span class="token punctuation">:</span> <span class="token string">\'2\'</span>\n <span class="token key atrule">memory</span><span class="token punctuation">:</span> <span class="token string">\'4Gi\'</span>\n <span class="token key atrule">startupProbe</span><span class="token punctuation">:</span>\n <span class="token key atrule">tcpSocket</span><span class="token punctuation">:</span>\n <span class="token key atrule">port</span><span class="token punctuation">:</span> <span class="token number">3800</span>\n <span class="token key atrule">initialDelaySeconds</span><span class="token punctuation">:</span> <span class="token number">10</span>\n <span class="token key atrule">periodSeconds</span><span class="token punctuation">:</span> <span class="token number">5</span>\n <span class="token key atrule">failureThreshold</span><span class="token punctuation">:</span> <span class="token number">6</span>\n <span class="token key atrule">timeoutSeconds</span><span class="token punctuation">:</span> <span class="token number">1</span>\n <span class="token key atrule">readinessProbe</span><span class="token punctuation">:</span>\n <span class="token comment"># Add initialDelaySeconds here if no startupProbe is configured.</span>\n <span class="token comment"># Kubernetes 1.20+ defers readiness/liveness until the startupProbe succeeds.</span>\n <span class="token key atrule">exec</span><span class="token punctuation">:</span>
1\n <span class="token key atrule">command</span><span class="token punctuation">:</span>\n <span class="token punctuation">-</span> curl\n <span class="token punctuation">-</span> <span class="token punctuation">-</span>sf\n <span class="token punctuation">-</span> <span class="token punctuation">-</span><span class="token punctuation">-</span>max<span class="token punctuation">-</span>time\n <span class="token punctuation">-</span> <span class="token string">\'3\'</span>\n <span class="token punctuation">-</span> <span class="token punctuation">-</span><span class="token punctuation">-</span>http2<span class="token punctuation">-</span>prior<span class="token punctuation">-</span>knowledge\n <span class="token punctuation">-</span> http<span class="token punctuation">:</span>//localhost<span class="token punctuation">:</span>3800/info\n <span class="token key atrule">timeoutSeconds</span><span class="token punctuation">:</span> <span class="token number">5</span>\n <span class="token key atrule">periodSeconds</span><span class="token punctuation">:</span> <span class="token number">5</span>\n <span class="token key atrule">failureThreshold</span><span class="token punctuation">:</span> <span class="token number">3</span>\n <span class="token key atrule">livenessProbe</span><span class="token punctuation">:</span>\n <span class="token comment"># Add initialDelaySeconds here if no startupProbe is configured.</span>\n <span class="token comment"># Kubernetes 1.20+ defers readiness/liveness until the startupProbe succeeds.</span>\n <span class="token key atrule">tcpSocket</span><span class="token punctuation">:</span>\n <span class="token key atrule">port</span><span class="token punctuation">:</span> <span class="token number">3800</span>\n <span class="token comment"># TCP handshakes should complete quickly; exec/H2 uses timeoutSeconds: 5.</span>\n <span class="token key atrule">timeoutSeconds</span><span class="token punctuation">:</span> <span class="token number">1</span>\n <span class="token key atrule">periodSeconds</span><span class="token punctuation">:</span> <span class="token number">10</span>\n <span class="token key atrule">failureThreshold</span><span class="token punctuation">:</span> <span class="token number">3</span></code></pre></div>'}}),"\n",s.createElement(n.blockquote,null,"\n",s.createElement(n.p,null,s.createElement(n.strong,null,"Readiness endpoint:")," The manifest uses ",s.createElement(n.span,{dangerouslySetInnerHTML:{__html:'<code class="language-text">/info</code>'}})," for copy-paste safety because that endpoint is built in. Replace\n",s.createElement(n.span,{dangerouslySetInnerHTML:{__html:'<code class="language-text">/info</code>'}})," with ",s.createElement(n.span,{dangerouslySetInnerHTML:{__html:'<code class="language-text">/health</code>'}})," in the readiness probe after registering that route via ",s.createElement(n.span,{dangerouslySetInnerHTML:{__html:'<code class="language-text">configureFastify</code>'}})," if readiness should\nwait for renderer-specific warm-up checks."),"\n"),"\n",s.createElement(n.blockquote,null,"\n",s.createElement(n.p,null,s.createElement(n.strong,null,"Note:")," Both containers use the same Docker image, ensuring the React on Rails gem and Node Renderer package versions are always aligned."),"\n"),"\n",s.createElement(n.h2,{id:"troubleshooting",style:{position:"relative"}},s.createElement(n.a,{href:"#troubleshooting","aria-label":"troubleshooting permalink",className:"anchor before"},s.createElement(n.span,{dangerouslySetInnerHTML:{__html:'<svg aria-hidden="true" height="20" version="1.1" viewBox="0 0 16 16" width="20"><path fill-rule="evenodd" d="M4 9h1v1H4c-1.5 0-3-1.69-3-3.5S2.55 3 4 3h4c1.45 0 3 1.69 3 3.5 0 1.41-.91 2.72-2 3.25V8.59c.58-.45 1-1.27 1-2.09C10 5.22 8.98 4 8 4H4c-.98 0-2 1.22-2 2.5S3 9 4 9zm9-3h-1v1h1c1 0 2 1.22 2 2.5S13.98 12 13 12H9c-.98 0-2-1.22-2-2.5 0-.83.42-1.64 1-2.09V6.25c-1.09.53-2 1.84-2 3.25C6 11.31 7.55 13 9 13h4c1.45 0 3-1.69 3-3.5S14.5 6 13 6z"></path></svg>'}})),"Troubleshooting"),"\n",s.createElement(n.h3,{id:"container-restarts-frequently-oom",style:{position:"relative"}}
1,s.createElement(n.a,{href:"#container-restarts-frequently-oom","aria-label":"container restarts frequently oom permalink",className:"anchor before"},s.createElement(n.span,{dangerouslySetInnerHTML:{__html:'<svg aria-hidden="true" height="20" version="1.1" viewBox="0 0 16 16" width="20"><path fill-rule="evenodd" d="M4 9h1v1H4c-1.5 0-3-1.69-3-3.5S2.55 3 4 3h4c1.45 0 3 1.69 3 3.5 0 1.41-.91 2.72-2 3.25V8.59c.58-.45 1-1.27 1-2.09C10 5.22 8.98 4 8 4H4c-.98 0-2 1.22-2 2.5S3 9 4 9zm9-3h-1v1h1c1 0 2 1.22 2 2.5S13.98 12 13 12H9c-.98 0-2-1.22-2-2.5 0-.83.42-1.64 1-2.09V6.25c-1.09.53-2 1.84-2 3.25C6 11.31 7.55 13 9 13h4c1.45 0 3-1.69 3-3.5S14.5 6 13 6z"></path></svg>'}})),"Container restarts frequently (OOM)"),"\n",s.createElement(n.ol,null,"\n",s.createElement(n.li,null,s.createElement(n.strong,null,"Check which container is OOM-killed")," â Use your orchestrator's events/logs to identify if it's Rails or the Node Renderer."),"\n",s.createElement(n.li,null,s.createElement(n.strong,null,"If Node Renderer")," â Set ",s.createElement(n.span,{dangerouslySetInnerHTML:{__html:'<code class="language-text">NODE_OPTIONS="--max-old-space-size=512"</code>'}})," and enable rolling restarts with both ",s.createElement(n.span,{dangerouslySetInnerHTML:{__html:'<code class="language-text">allWorkersRestartInterval</code>'}})," and ",s.createElement(n.span,{dangerouslySetInnerHTML:{__html:'<code class="language-text">delayBetweenIndividualWorkerRestarts</code>'}}),"."),"\n",s.createElement(n.li,null,s.createElement(n.strong,null,"If Rails")," â Consider jemalloc and review ",s.createElement(n.span,{dangerouslySetInnerHTML:{__html:'<code class="language-text">WEB_CONCURRENCY</code>'}})," / ",s.createElement(n.span,{dangerouslySetInnerHTML:{__html:'<code class="language-text">RAILS_MAX_THREADS</code>'}})," settings."),"\n",s.createElement(n.li,null,s.createElement(n.strong,null,"Monitor over time")," â Memory growth is gradual. Monitor for 8+ hours to see the steady-state memory usage."),"\n"),"\n",s.createElement(n.h3,{id:"renderer-connection-refused-after-deploy",style:{position:"relative"}},s.createElement(n.a,{href:"#renderer-connection-refused-after-deploy","aria-label":"renderer connection refused after deploy permalink",className:"anchor before"},s.createElement(n.span,{dangerouslySetInnerHTML:{__html:'<svg aria-hidden="true" height="20" version="1.1" viewBox="0 0 16 16" width="20"><path fill-rule="evenodd" d="M4 9h1v1H4c-1.5 0-3-1.69-3-3.5S2.55 3 4 3h4c1.45 0 3 1.69 3 3.5 0 1.41-.91 2.72-2 3.25V8.59c.58-.45 1-1.27 1-2.09C10 5.22 8.98 4 8 4H4c-.98 0-2 1.22-2 2.5S3 9 4 9zm9-3h-1v1h1c1 0 2 1.22 2 2.5S13.98 12 13 12H9c-.98 0-2-1.22-2-2.5 0-.83.42-1.64 1-2.09V6.25c-1.09.53-2 1.84-2 3.25C6 11.31 7.55 13 9 13h4c1.45 0 3-1.69 3-3.5S14.5 6 13 6z"></path></svg>'}})),"Renderer connection refused after deploy"),"\n",s.createElement(n.ul,null,"\n",s.createElement(n.li,null,"Verify the renderer container is running and healthy."),"\n",s.createElement(n.li,null,"For sidecars: Check that the renderer binds to ",s.createElement(n.span,{dangerouslySetInnerHTML:{__html:'<code class="language-text">localhost</code>'}})," (default) or ",s.createElement(n.span,{dangerouslySetInnerHTML:{__html:'<code class="language-text">0.0.0.0</code>'}}),"."),"\n",s.createElement(n.li,null,"For separate workloads: Verify DNS resolution and that ",s.createElement(n.span,{dangerouslySetInnerHTML:{__html:'<code class="language-text">config.renderer_url</code>'}})," matches the renderer's service endpoint."),"\n",s.createElement(n.li,null,"Check for readiness probe failures that may have removed the renderer from the service."),"\n"),"\n",s.createElement(n.h3,{id:"different-versions-after-deploy-separate-workloads",style:{position:"relative"}},s.createElement(n.a,{href:"#different-versions-after-deploy-separate-workloads","aria-label":"different versions after deploy separate workloads permalink",className:"anchor before"},s.createElement(n.span,{dangerouslySetInnerHTML:{__html:'<svg aria-hidden="true" height="20" version="1.1" viewBox="0 0 16 16" width="20"><path fill-rule="evenodd" d="M4 9h1v1H4c-1.5 0-3-1.69-3-3.5S2.55 3 4 3h4c1.45 0 3 1.69 3 3.5 0 1.41-.91 2.72-2 3.25V8.59c.58-.45 1-1.27 1-2.09C10 5.22 8.98 4 8 4H4c-.98 0-2 1.22-2 2.5S3 9 4 9zm9-3h-1v1h1c1 0 2 1.22 2 2.5S13.98 12 13 12H9c-.98 0-2-1.22-2-2.5 0-.83.42-1.64 1-2.09V6.25c-1.09.53-2 1.84-2 3.25C6 11.31 7.55 13 9 13h4c1.45 0 3-1.69 3-3.5S14.5 6 13 6z"></path></svg>'}})),"Different versions after deploy (separate workloads)"),"\n",s.createElement(n.p,null,"The React on Rails gem and Node Renderer package have a protocol version. If they mismatch, render requests return an error. To avoid this:"),"\n",s.createElement(n.ul,null,"\n",s.createElement(n.li,null,s.createElement(n.strong,null,"Sidecars"),": Both containers use the same image, so versions are always aligned."),"\n",s.createElement(n.li,null,s.createElement(n.strong,null,"Separate workloads"),": Deploy both workloads simultaneously. If your orchestrator doesn't support atomic multi-workload deploys, consider switching to sidecars."),"\n"),"\n",s.createElement(n.h3,{id:"high-latency-on-ssr-requests",style:{position:"relative"}}
1,s.createElement(n.a,{href:"#high-latency-on-ssr-requests","aria-label":"high latency on ssr requests permalink",className:"anchor before"},s.createElement(n.span,{dangerouslySetInnerHTML:{__html:'<svg aria-hidden="true" height="20" version="1.1" viewBox="0 0 16 16" width="20"><path fill-rule="evenodd" d="M4 9h1v1H4c-1.5 0-3-1.69-3-3.5S2.55 3 4 3h4c1.45 0 3 1.69 3 3.5 0 1.41-.91 2.72-2 3.25V8.59c.58-.45 1-1.27 1-2.09C10 5.22 8.98 4 8 4H4c-.98 0-2 1.22-2 2.5S3 9 4 9zm9-3h-1v1h1c1 0 2 1.22 2 2.5S13.98 12 13 12H9c-.98 0-2-1.22-2-2.5 0-.83.42-1.64 1-2.09V6.25c-1.09.53-2 1.84-2 3.25C6 11.31 7.55 13 9 13h4c1.45 0 3-1.69 3-3.5S14.5 6 13 6z"></path></svg>'}})),"High latency on SSR requests"),"\n",s.createElement(n.ol,null,"\n",s.createElement(n.li,null,s.createElement(n.strong,null,"Check worker count")," â If all workers are busy, requests queue up. Increase ",s.createElement(n.span,{dangerouslySetInnerHTML:{__html:'<code class="language-text">workersCount</code>'}})," or scale replicas."),"\n",s.createElement(n.li,null,s.createElement(n.strong,null,"Check ",s.createElement(n.span,{dangerouslySetInnerHTML:{__html:'<code class="language-text">max-old-space-size</code>'}}))," â If set too low, frequent GC pauses increase latency. Increase the limit."),"\n",s.createElement(n.li,null,s.createElement(n.strong,null,"Profile components")," â Use ",s.createElement(n.span,{dangerouslySetInnerHTML:{__html:'<code class="language-text">node --inspect</code>'}})," to profile server-rendering code. See ",s.createElement(n.a,{href:"/linked-assets/bf9217c6af5361d37abc81459a8c7f2a/profiling-server-side-rendering-code.md"},"Profiling Server-Side Rendering Code"),"."),"\n"),"\n",s.createElement(n.h3,{id:"startup-race-condition-between-rails-and-node-renderer",style:{position:"relative"}},s.createElement(n.a,{href:"#startup-race-condition-between-rails-and-node-renderer","aria-label":"startup race condition between rails and node renderer permalink",className:"anchor before"},s.createElement(n.span,{dangerouslySetInnerHTML:{__html:'<svg aria-hidden="true" height="20" version="1.1" viewBox="0 0 16 16" width="20"><path fill-rule="evenodd" d="M4 9h1v1H4c-1.5 0-3-1.69-3-3.5S2.55 3 4 3h4c1.45 0 3 1.69 3 3.5 0 1.41-.91 2.72-2 3.25V8.59c.58-.45 1-1.27 1-2.09C10 5.22 8.98 4 8 4H4c-.98 0-2 1.22-2 2.5S3 9 4 9zm9-3h-1v1h1c1 0 2 1.22 2 2.5S13.98 12 13 12H9c-.98 0-2-1.22-2-2.5 0-.83.42-1.64 1-2.09V6.25c-1.09.53-2 1.84-2 3.25C6 11.31 7.55 13 9 13h4c1.45 0 3-1.69 3-3.5S14.5 6 13 6z"></path></svg>'}})),"Startup race condition between Rails and Node Renderer"),"\n",s.createElement(n.p,null,"In sidecar configurations, Rails may start before the Node Renderer is ready. Configure ",s.createElement(n.span,{dangerouslySetInnerHTML:{__html:'<code class="language-text">renderer_request_retry_limit</code>'}})," in Rails to retry failed connections:"),"\n",s.createElement(n.span,{dangerouslySetInnerHTML:{__html:'<div class="gatsby-highlight" data-language="ruby"><pre class="language-ruby"><code class="language-ruby">ReactOnRailsPro<span class="token punctuation">.</span>configure <span class="token keyword">do</span> <span class="token operator">|</span>config<span class="token operator">|</span>\n config<span class="token punctuation">.</span>renderer_request_retry_limit <span class="token operator">=</span> <span class="token number">5</span> <span class="token comment"># default: 5</span>\n<span class="token keyword">end</span></code></pre></div>'}}),"\n",s.createElement(n.p,null,"This handles transient startup ordering issues. For a more robust solution, add a startup dependency or init container that waits for the renderer port to accept TCP connections, or queries ",s.createElement(n.span,{dangerouslySetInnerHTML:{__html:'<code class="language-text">/info</code>'}})," with an h2c-aware client (for example, ",s.createElement(n.span,{dangerouslySetInnerHTML:{__html:'<code class="language-text">curl --http2-prior-knowledge</code>'}}),")."),"\n",s.createElement(n.h2,{id:"controlplane-specific-notes",style:{position:"relative"}},s.createElement(n.a,{href:"#controlplane-specific-notes","aria-label":"controlplane specific notes permalink",className:"anchor before"},s.createElement(n.span,{dangerouslySetInnerHTML:{__html:'<svg aria-hidden="true" height="20" version="1.1" viewBox="0 0 16 16" width="20"><path fill-rule="evenodd" d="M4 9h1v1H4c-1.5 0-3-1.69-3-3.5S2.55 3 4 3h4c1.45 0 3 1.69 3 3.5 0 1.41-.91 2.72-2 3.25V8.59c.58-.45 1-1.27 1-2.09C10 5.22 8.98 4 8 4H4c-.98 0-2 1.22-2 2.5S3 9 4 9zm9-3h-1v1h1c1 0 2 1.22 2 2.5S13.98 12 13 12H9c-.98 0-2-1.22-2-2.5 0-.83.42-1.64 1-2.09V6.25c-1.09.53-2 1.84-2 3.25C6 11.31 7.55 13 9 13h4c1.45 0 3-1.69 3-3.5S14.5 6 13 6z"></path></svg>'}})),"ControlPlane-Specific Notes"),"\n",s.createElement(n.p,null,"When deploying on ",s.createElement(n.a,{href:"https://controlplane.com"},"ControlPlane"),":"),"\n",s.createElement(n.ul,null,"\n",s.createElement(n.li,null,s.createElement(n.strong,null,"Port configuration:")," Use ",s.createElement(n.span,{dangerouslySetInnerHTML:{__html:'<code class="language-text">process.env.PORT</code>'}})," for the renderer port if running as a standalone workload. ControlPlane injects the ",s.createElement(n.span,{dangerouslySetInnerHTML:{__html:'<code class="language-text">PORT</code>'}})," environment variable for the primary container."),"\n",s.createElement(n.li,null,s.createElement(n.strong,null,"Sidecar setup:")," Configure separate CPU and memory limits per container in your workload definition. In ControlPlane, sidecar containers are defined alongside the main container in the same workload spec."),"\n",s.createElement(n.li,null,s.createElement(n.strong,null,"Autoscaling:")," ControlPlane uses workload-level metrics for autoscaling â see ",s.createElement(n.a,{href:"#autoscaling-considerations"},"Autoscaling Considerations")," above. If you need per-container scaling, use separate workloads instead of sidecars."),"\n",s.createElement(n.li,null,s.createElement(n.strong,null,"Health checks:")," ControlPlane supports readiness and liveness probes in the same format as Kubernetes. Configure them as shown in the ",s.createElement(n.a,{href:"#kubernetes-sidecar-manifest"}
1,"Kubernetes Sidecar Manifest")," section."),"\n"),"\n",s.createElement(n.h2,{id:"references",style:{position:"relative"}},s.createElement(n.a,{href:"#references","aria-label":"references permalink",className:"anchor before"},s.createElement(n.span,{dangerouslySetInnerHTML:{__html:'<svg aria-hidden="true" height="20" version="1.1" viewBox="0 0 16 16" width="20"><path fill-rule="evenodd" d="M4 9h1v1H4c-1.5 0-3-1.69-3-3.5S2.55 3 4 3h4c1.45 0 3 1.69 3 3.5 0 1.41-.91 2.72-2 3.25V8.59c.58-.45 1-1.27 1-2.09C10 5.22 8.98 4 8 4H4c-.98 0-2 1.22-2 2.5S3 9 4 9zm9-3h-1v1h1c1 0 2 1.22 2 2.5S13.98 12 13 12H9c-.98 0-2-1.22-2-2.5 0-.83.42-1.64 1-2.09V6.25c-1.09.53-2 1.84-2 3.25C6 11.31 7.55 13 9 13h4c1.45 0 3-1.69 3-3.5S14.5 6 13 6z"></path></svg>'}})),"References"),"\n",s.createElement(n.ul,null,"\n",s.createElement(n.li,null,s.createElement(n.a,{href:"/linked-assets/b6490192988c8a036f273ba296fd689e/basics.md"},"Node Renderer Basics")),"\n",s.createElement(n.li,null,s.createElement(n.a,{href:"/linked-assets/3f622c8d69dadb55215173974f74c90e/js-configuration.md"},"JS Configuration")),"\n",s.createElement(n.li,null,s.createElement(n.a,{href:"/linked-assets/3cc15217b5218a5e8de246cae7cb94b0/error-reporting-and-tracing.md"},"Error Reporting and Tracing")),"\n",s.createElement(n.li,null,s.createElement(n.a,{href:"/linked-assets/67e780b018a542c108a832524fca5486/heroku.md"},"Heroku Deployment")),"\n",s.createElement(n.li,null,s.createElement(n.a,{href:"/linked-assets/ecebe3dd62a3bee58ad7a09502f9fb15/js-memory-leaks.md"},"JS Memory Leaks")),"\n",s.createElement(n.li,null,s.createElement(n.a,{href:"/linked-assets/bf9217c6af5361d37abc81459a8c7f2a/profiling-server-side-rendering-code.md"},"Profiling Server-Side Rendering Code")),"\n",s.createElement(n.li,null,s.createElement(n.a,{href:"/linked-assets/d4a3213be664811b0c60740018fa7d48/troubleshooting.md"},"Pro Troubleshooting")),"\n"))}var l=function(e){void 0===e&&(e={});const{wrapper:n}=Object.assign({},(0,t.R)(),e.components);return n?s.createElement(n,e,s.createElement(r,e)):r(e)},o=a(63679),c=a(95197),i=a(47388),p=a(23690),d=a(14487);const u=o.default.p.withConfig({displayName:"doc-article__RedirectFallback",componentId:"sc-tic2w5-0"})(["padding:2rem;"]),m=e=>{let{data:n,location:a,children:t}=e;const{headings:r}=n.article.childMdx,l=(0,c.X9)(a.pathname),[o,p]=s.useState(null),m=a.pathname.split("/"),h=m.indexOf("docs"),g=-1===h?a.pathname.length:m.slice(0,h+1).join("/").length,k=a.pathname.slice(0,g),y=a.pathname.slice(g+1).replace(/\/$/,"").split("/").filter((e=>""!==e)),E=y.length>1?y.slice(0,-1).join("/"):"";s.useEffect((()=>{if(!l)return;const{hash:e}=window.location;p(e);const n=""+l+e;window.location.href!==n&&window.location.replace(n)}),[l]);const f=null===l||null===o?null:""+l+o;return s.createElement(s.Fragment,null,null!==f?s.createElement(u,{role:"status","aria-live":"polite"},"Redirecting..."," ",s.createElement("a",{href:f,target:"_blank",rel:"noopener noreferrer"},"Click here if not redirected.")):s.createElement(i.A,{main:s.createElement(d.A,{body:t,headings:r,transformUrl:e=>{if(e.startsWith("http")||e.startsWith("#"))return e;const n=e.replace(/\.md$/,"/");if(n.startsWith("./"))return k+(""===E?"":"/"+E)+n.slice(1);if(n.startsWith("../")){if(""===E)return""+k+n.slice(2);const e=E.split("/").slice(0,-1).join("/");return k+(""===e?"":"/"+e)+n.slice(2)}return k+"/"+(""===E?"":E+"/")+n}})}))};function h(e){return s.createElement(m,e,s.createElement(l,e))}const g=e=>{let{data:n,location:a,pageContext:t}=e;const{headings:r}=n.article.childMdx,{siteUrl:l}=n.site.siteMetadata,{repoDisplayName:o}=t,i=r.find((e=>1===e.depth)),d=(0,c.X9)(a.pathname)||""+l.replace(/\/+$/,"")+a.pathname;return s.createElement(p.A,{title:i?i.value:o,description:i?i.value:o+" documentation",isArticle:!0,canonical:d,pathname:a.pathname})}}}]); 2//# sourceMappingURL=component---src-template-doc-article-tsx-content-file-path-cache-gatsby-source-git-react-on-rails-docs-oss-building-features-node-renderer-container-deployment-md-bde309b9a186e5c0bfbb.js.map
Line numbers count LF bytes from the start of the resource, as the search results do. Vendor segments are library code the classifier recognised; they are stored but not indexed. Bytes are shown as Latin1 characters, one per byte.