Add Dockerfile for Storybook Railway deployment and fix landing page#72
Conversation
Co-Authored-By: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
Original prompt from Kyle
|
🤖 Devin AI EngineerI'll be helping with this pull request! Here's what you should know: ✅ I will automatically:
Note: I can only respond to comments from users who have write access to this repository. ⚙️ Control Options:
|
Co-Authored-By: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
Co-Authored-By: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
Co-Authored-By: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
Co-Authored-By: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
Co-Authored-By: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
serve's cleanUrls feature strips query params from iframe.html redirects, breaking Storybook's preview iframe communication. http-server serves files as-is without URL rewriting, which Storybook requires. Also removes the managerHead redirect hack since the root cause was the server, not the URL routing. With http-server, Storybook correctly auto-selects the first story (DeepUI.dev) on initial load. Co-Authored-By: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
Co-Authored-By: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
…g and cached 301 redirects Co-Authored-By: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
There was a problem hiding this comment.
Pull request overview
Adds an opinionated Docker-based deployment path for hosting the repo’s Storybook build on Railway, plus Storybook manager tweaks to ensure the root URL lands on the intended docs page instead of “No Preview”.
Changes:
- Added a multi-stage
Dockerfileand.dockerignorefor building and servingstorybook-static/in Railway. - Introduced a custom Node static server (
server.cjs) intended to avoid URL rewriting / redirect behavior that breaks Storybook iframe URLs. - Updated Storybook config to pin the docs landing page and inject a manager redirect from
/to the docs path.
Reviewed changes
Copilot reviewed 4 out of 4 changed files in this pull request and generated 4 comments.
| File | Description |
|---|---|
server.cjs |
New minimal static server for serving storybook-static/ on $PORT without “clean URL” redirects. |
Dockerfile |
Multi-stage build (build Storybook) + runtime stage (run node server.cjs). |
.storybook/main.cjs |
Pins Main.mdx and injects managerHead redirect to a docs path when path= is absent. |
.dockerignore |
Reduces Docker build context size by ignoring node artifacts and VCS metadata. |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
You can also share your feedback on Copilot code review. Take the survey.
server.cjs
Outdated
| // Resolve file path (no URL rewriting, no clean URLs) | ||
| let filePath = path.join(ROOT, pathname); | ||
|
|
||
| // Prevent directory traversal | ||
| if (!filePath.startsWith(ROOT)) { | ||
| res.writeHead(403); | ||
| res.end('Forbidden'); | ||
| return; | ||
| } |
…er.cjs Co-Authored-By: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
Co-Authored-By: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
Co-Authored-By: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
Co-Authored-By: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
| } else { | ||
| headers['Cache-Control'] = 'public, max-age=31536000, immutable'; | ||
| } |
There was a problem hiding this comment.
🟡 Immutable caching applied to non-content-hashed assets causes stale content after redeployment
All non-HTML files are served with Cache-Control: public, max-age=31536000, immutable (server.cjs:40). Storybook's build output includes files at fixed URLs without content hashes — most notably index.json (the stories manifest), as well as other metadata files. After redeploying the Docker container with updated stories, browsers that previously visited will serve the stale cached index.json (and similar files) without even making a conditional request, because immutable tells the browser the resource will never change. This causes the story sidebar to show outdated entries until the user hard-refreshes or clears their cache.
A safer approach is to only apply aggressive caching to files whose paths contain a content hash (e.g., files in assets/ with hash segments), and use a short max-age or no-cache for everything else.
Prompt for agents
In server.cjs, the else branch at lines 39-41 applies immutable caching to ALL non-HTML files. This is too aggressive for files that are not content-hashed (e.g. Storybook's index.json, favicon.ico, etc.).
Suggested fix: Change the caching logic to only apply immutable caching to files whose paths suggest they are content-hashed (e.g. files under /assets/ or filenames containing a hash pattern). For all other non-HTML files, use a shorter max-age or no-cache.
For example, in the serveFile function around line 37-41:
if (ext === '.html') {
headers['Cache-Control'] = 'no-cache, no-store, must-revalidate';
} else if (filePath.includes('/assets/') || /\.[a-f0-9]{8,}\./i.test(path.basename(filePath))) {
headers['Cache-Control'] = 'public, max-age=31536000, immutable';
} else {
headers['Cache-Control'] = 'no-cache';
}
Was this helpful? React with 👍 or 👎 to provide feedback.
There was a problem hiding this comment.
Good point — index.json and other non-hashed files would get stale with immutable caching. This is a valid improvement but not a crash/security bug, so leaving it for the PR reviewer to decide whether to address now or in a follow-up.
Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com>
There was a problem hiding this comment.
Pull request overview
This PR adds the container/build plumbing needed to deploy the project’s built Storybook (storybook-static) to Railway, and updates Storybook manager behavior to avoid the “No Preview” landing experience at the root URL.
Changes:
- Add a multi-stage Dockerfile +
.dockerignoreto build Storybook and run a minimal Node static server. - Introduce
server.cjsstatic server intended to avoid URL rewriting/redirect issues. - Add a
managerHeadredirect in Storybook config to send/to a docs path.
Reviewed changes
Copilot reviewed 4 out of 4 changed files in this pull request and generated 4 comments.
| File | Description |
|---|---|
server.cjs |
New custom Node static server for serving storybook-static on Railway’s PORT. |
Dockerfile |
Multi-stage build/serve containerization for Storybook Railway deployment. |
.storybook/main.cjs |
Adds managerHead redirect logic for the Storybook manager landing page. |
.dockerignore |
Reduces Docker build context for faster/smaller builds. |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
You can also share your feedback on Copilot code review. Take the survey.
| stories: ['./stories/**/*.mdx', '../lib/**/*.stories.@(ts|tsx)'], | ||
| managerHead: (head) => ` |
Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com>
Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com>
Summary
Adds a multi-stage Dockerfile,
.dockerignore, and a custom static file server (server.cjs) to deploy Storybook to Railway. The build stage installs dependencies and runsnpm run build-storybook, then the serve stage runs a minimal Node.js static server to host the output on the Railway-assignedPORT.A "storybook" service has been created in the deeptrust Railway project (production environment) and deployed via
railway up. The live Storybook is available at:https://deep-ui-storybook.up.railway.app
Landing page fixes
Three changes to resolve the "No Preview" error on the root URL:
Custom static file server (
server.cjs) with no URL rewriting — off-the-shelf servers likeserve(with itscleanUrlsfeature) were stripping.htmlextensions and query parameters from URLs via 301 redirects. When Storybook's manager loadediframe.html?viewMode=docs&id=deepui-dev--docs, those redirects broke the iframe communication entirely. The custom server serves files as-is, setsCache-Control: no-cache, no-store, must-revalidateon HTML responses to prevent stale redirect caches, and includes a.htmlextension fallback for compatibility with any browser-cached redirects from earlier deployments.Pinned
Main.mdxfirst in the stories array (.storybook/main.cjs) — ensures the "DeepUI.dev" docs page is the first entry in Storybook's index.Added
managerHeadredirect (.storybook/main.cjs) — injects a small<script>into the Storybook manager<head>that redirects the root URL (/) to/?path=/docs/deepui-dev--docswhen nopath=query parameter is present. Without this, Storybook does not auto-select the first story and shows "No Preview".Security hardening of
server.cjsFive bugs fixed across review iterations:
filePath.startsWith(ROOT)was insufficient becauseROOThas no trailing separator; a sibling directory like/app/storybook-static-evil/would pass the check. Fixed tofilePath.startsWith(ROOT + path.sep) && filePath !== ROOT.res.writeHead(200)was called synchronously before the stream opened, making thewriteHead(500)error branch dead code. Moved intostream.on('open')so TOCTOU errors (file deleted betweenstatandopen) correctly return 500.URIErrorcrash —decodeURIComponenton a malformed percent-encoded path (e.g.GET /%E0%A4%A) threw an uncaught exception, crashing the process. Wrapped in try/catch, returning400 Bad Request.res.headersSentguard — Stream error handler now checksres.headersSentbefore callingwriteHead(500), preventingERR_HTTP_HEADERS_SENTcrashes on partial writes.%00in the URL path produced a string with an embedded null byte, causingfs.statto throw synchronously withERR_INVALID_ARG_VALUEand crash the process. Added an explicit null byte check after decode, returning400 Bad Request.Review & Testing Checklist for Human
server.cjsfor security — this is a hand-rolled static server with no test suite. It has been hardened through 5 review iterations, but a careful read is warranted. In particular: (a) does the.htmlfallback on line 109 allow serving unintended files? (b) is the directory traversal check on line 82 truly sufficient for allpath.joinedge cases? (c) are there other input vectors (e.g. double-encoding, overlong UTF-8) that bypass the null byte or URI decode checks?servedeployment left cached 301 redirects).Main.mdxappearing both explicitly ('./stories/Main.mdx') and in the'./stories/**/*.mdx'glob doesn't cause duplicate "DeepUI.dev" sidebar entriesmain) vs. manual CLI deploys. Currently, redeployments require runningrailway upmanually or triggering from the Railway dashboard.Notes
node:20-alpinefor both stages. The serve stage has zero npm dependencies — justnode server.cjs.PORTvia environment variable;server.cjsfalls back to3000if unset.managerHeadredirect useswindow.location.replace(replaces history entry, so the browser back button won't loop back to/). It only fires when the URL has nopath=parameter, so direct links to specific stories are unaffected.Cache-Control: public, max-age=31536000, immutablefor performance. HTML files useno-cacheto prevent stale redirect issues.Link to Devin session: https://app.devin.ai/sessions/86565d75cfa0484a81fc937d83cdb5a4
Requested by: @kyleknighted