Skip to content

SSR release handoff

Production PHP requests use the SSR socket under their own resolved ROOT. Keep the renderer paired with that immutable release rather than following public_html from one long-running service.

Deployment sequence

  1. Build and move the candidate into its final public_html@VERSION directory. Confirm exec-guard readiness as before.
  2. Start ssr-server@VERSION.service, pinned to that directory and its js/ssr.sock socket.
  3. Wait for all four workers to load. js/bin/ssr-ready performs a real HelloWorld render, validates the response/context and checks that the socket belongs to the expected primary PID. This component makes no backend or provider requests. The readiness deadline is 30 seconds; normal render/fetch timeouts are unchanged.
  4. Atomically replace public_html with a symlink rename. Preparation failure leaves the old release live and discards only the unpublished candidate renderer.
  5. Immediately schedule ssr-retire@VERSION.timer, independently of other post-deploy hooks. Existing PHP requests can continue using the previous renderer/socket.
  6. After at least 90 seconds, retirement takes the deployment lock, checks that this is still the current publication and verifies its renderer again. It stops/disables older instances, drains the legacy service if still running, and only then applies the existing ten-old-directory retention policy.

The 90-second retention period exceeds the production FPM request termination budget (30 seconds). An old Node primary then allows accepted requests up to ten seconds to drain. KillMode=mixed initially signals only the primary; systemd enforces a 15-second stop bound for the whole instance.

A timer queued behind another deployment cannot retire that newer publication: its expected version must still match after it acquires the lock. Each publication has its own timer. Retirement timers are enabled for boot until completion, so a reboot during the grace period does not leave old instances permanently enabled. Failed retirement retries after 30 seconds; it never stops the current instance as a fallback.

This temporarily runs two renderer generations (occasionally more during rapid successive deployments). PHP worker capacity, nginx routing, authentication, caches, request retries and request timeouts are not changed.

First-rollout bootstrap

The deploy script already running when this change is first installed may be the old script. It cannot execute a new pre-publication step retroactively. The new post-deploy hook therefore also prepares the current renderer without restarting the old one. That first switch still inherits the legacy timing window; subsequent deployments use pre-publication readiness.

Migration enables an instance pinned to the old release for boot without starting it on top of the live legacy socket, then disables the legacy unit's boot enablement without stopping it. The candidate is separately enabled before publication. A reboot on either side of publication can therefore serve the matching old/new socket. Retirement eventually removes old boot enablements.

An installation with SSR disabled remains disabled.

Operations

After migration, ssr-server.service is intentionally disabled and eventually inactive. Renderer status/restarts belong to the release instance, not that legacy unit:

release=$(readlink -f "$HOME/public_html")
version=${release##*@}
systemctl --user status "ssr-server@$version.service"

A direct restart is not a zero-gap deployment. Use the deployment handoff for code changes. Do not manually start the legacy service alongside an instance: its symlink-following socket can collide with the current renderer. The new server refuses to unlink another live listener.

Readiness failures do not publish the candidate. Failures in unrelated post-deploy hooks retain the existing incomplete-deployment retry behavior; retirement is already scheduled and does not depend on those hooks succeeding. Completed releases still skip rebuilds/restarts on no-change polls.

Rollback by preparing/publishing an older release requires the new handoff scripts and readiness protocol in that release. Reverting to a pre-handoff release needs an explicit legacy-service migration plan; simply changing the symlink is not a supported rollback procedure.

Tests

make test runs sandbox deployment tests and real Node cluster subprocess tests through DeployRecoveryTest. They use temporary Git repositories, fake systemd state and temporary renderer fixtures, never production services or business records. Tests cover preparation/publication failures, bootstrap/reboot enablement, post-hook failure, stale retirement, socket ownership, actual in-flight response draining and the hard shutdown bound. The readiness program is additionally checked against the built production SSR bundle during local validation.

These changes address rollout interruption, not unrelated runtime backend latency or SSR timeouts.