Skip to content

Add Dockerfile for Storybook Railway deployment and fix landing page#72

Merged
kyleknighted merged 16 commits intomainfrom
devin/1773842536-railway-storybook-deploy
Mar 18, 2026
Merged

Add Dockerfile for Storybook Railway deployment and fix landing page#72
kyleknighted merged 16 commits intomainfrom
devin/1773842536-railway-storybook-deploy

Conversation

@kyleknighted
Copy link
Collaborator

@kyleknighted kyleknighted commented Mar 18, 2026

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 runs npm run build-storybook, then the serve stage runs a minimal Node.js static server to host the output on the Railway-assigned PORT.

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:

  1. Custom static file server (server.cjs) with no URL rewriting — off-the-shelf servers like serve (with its cleanUrls feature) were stripping .html extensions and query parameters from URLs via 301 redirects. When Storybook's manager loaded iframe.html?viewMode=docs&id=deepui-dev--docs, those redirects broke the iframe communication entirely. The custom server serves files as-is, sets Cache-Control: no-cache, no-store, must-revalidate on HTML responses to prevent stale redirect caches, and includes a .html extension fallback for compatibility with any browser-cached redirects from earlier deployments.

  2. Pinned Main.mdx first in the stories array (.storybook/main.cjs) — ensures the "DeepUI.dev" docs page is the first entry in Storybook's index.

  3. Added managerHead redirect (.storybook/main.cjs) — injects a small <script> into the Storybook manager <head> that redirects the root URL (/) to /?path=/docs/deepui-dev--docs when no path= query parameter is present. Without this, Storybook does not auto-select the first story and shows "No Preview".

Security hardening of server.cjs

Five bugs fixed across review iterations:

  • Directory traversal hardeningfilePath.startsWith(ROOT) was insufficient because ROOT has no trailing separator; a sibling directory like /app/storybook-static-evil/ would pass the check. Fixed to filePath.startsWith(ROOT + path.sep) && filePath !== ROOT.
  • Stream error handler: deferred 200 headersres.writeHead(200) was called synchronously before the stream opened, making the writeHead(500) error branch dead code. Moved into stream.on('open') so TOCTOU errors (file deleted between stat and open) correctly return 500.
  • Uncaught URIError crashdecodeURIComponent on a malformed percent-encoded path (e.g. GET /%E0%A4%A) threw an uncaught exception, crashing the process. Wrapped in try/catch, returning 400 Bad Request.
  • res.headersSent guard — Stream error handler now checks res.headersSent before calling writeHead(500), preventing ERR_HTTP_HEADERS_SENT crashes on partial writes.
  • Null byte injection crash — A %00 in the URL path produced a string with an embedded null byte, causing fs.stat to throw synchronously with ERR_INVALID_ARG_VALUE and crash the process. Added an explicit null byte check after decode, returning 400 Bad Request.

Review & Testing Checklist for Human

  • Review server.cjs for 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 .html fallback on line 109 allow serving unintended files? (b) is the directory traversal check on line 82 truly sufficient for all path.join edge cases? (c) are there other input vectors (e.g. double-encoding, overlong UTF-8) that bypass the null byte or URI decode checks?
  • Visit https://deep-ui-storybook.up.railway.app/ (root URL, no query params) and confirm it redirects to the DeepUI.dev docs page — not the "No Preview" error. If you previously visited the site and still see "No Preview", clear your browser cache or use an incognito window (the old serve deployment left cached 301 redirects).
  • Click through several other stories (e.g. "01. Color", Avatar, Table) to confirm navigation and iframe rendering work correctly after the redirect
  • Check that Main.mdx appearing both explicitly ('./stories/Main.mdx') and in the './stories/**/*.mdx' glob doesn't cause duplicate "DeepUI.dev" sidebar entries
  • Decide whether Railway should be configured for auto-deploy from GitHub (e.g., on pushes to main) vs. manual CLI deploys. Currently, redeployments require running railway up manually or triggering from the Railway dashboard.

Notes

  • The Dockerfile uses node:20-alpine for both stages. The serve stage has zero npm dependencies — just node server.cjs.
  • Railway dynamically assigns PORT via environment variable; server.cjs falls back to 3000 if unset.
  • The managerHead redirect uses window.location.replace (replaces history entry, so the browser back button won't loop back to /). It only fires when the URL has no path= parameter, so direct links to specific stories are unaffected.
  • Static assets (JS, CSS, fonts, images) are served with Cache-Control: public, max-age=31536000, immutable for performance. HTML files use no-cache to prevent stale redirect issues.
  • No CI/CD workflow for Railway is included in this PR — this is just the Dockerfile config + landing page fix. CI integration can be added separately if desired.

Link to Devin session: https://app.devin.ai/sessions/86565d75cfa0484a81fc937d83cdb5a4
Requested by: @kyleknighted


Open with Devin

Co-Authored-By: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
@devin-ai-integration
Copy link
Contributor

Original prompt from Kyle

Using Railway, i want to deploy storybook from deep-ui into the "DeepTrust Stack" environment. Let me know what information you need from me to make it happen?

You only need to look in the following repo: deeptrust-ai/deep-ui

@devin-ai-integration
Copy link
Contributor

🤖 Devin AI Engineer

I'll be helping with this pull request! Here's what you should know:

✅ I will automatically:

  • Address comments on this PR. Add '(aside)' to your comment to have me ignore it.
  • Look at CI failures and help fix them

Note: I can only respond to comments from users who have write access to this repository.

⚙️ Control Options:

  • Disable automatic comment and CI monitoring

kyleknighted and others added 5 commits March 18, 2026 14:33
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>
Copy link
Contributor

@devin-ai-integration devin-ai-integration bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

✅ Devin Review: No Issues Found

Devin Review analyzed this PR and found no potential bugs to report.

View in Devin Review to see 3 additional findings.

Open in Devin Review

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>
@devin-ai-integration devin-ai-integration bot changed the title Add Dockerfile for Storybook Railway deployment Add Dockerfile for Storybook Railway deployment and fix landing page Mar 18, 2026
kyleknighted and others added 2 commits March 18, 2026 15:23
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>
@kyleknighted kyleknighted marked this pull request as ready for review March 18, 2026 17:29
Copilot AI review requested due to automatic review settings March 18, 2026 17:29
devin-ai-integration[bot]

This comment was marked as resolved.

Copy link
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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 Dockerfile and .dockerignore for building and serving storybook-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
Comment on lines +56 to +64
// 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>
devin-ai-integration[bot]

This comment was marked as resolved.

Co-Authored-By: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
devin-ai-integration[bot]

This comment was marked as resolved.

Co-Authored-By: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
devin-ai-integration[bot]

This comment was marked as resolved.

Co-Authored-By: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
Copy link
Contributor

@devin-ai-integration devin-ai-integration bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Devin Review found 1 new potential issue.

View 10 additional findings in Devin Review.

Open in Devin Review

Comment on lines +39 to +41
} else {
headers['Cache-Control'] = 'public, max-age=31536000, immutable';
}
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟡 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';
  }
Open in Devin Review

Was this helpful? React with 👍 or 👎 to provide feedback.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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>
Copy link
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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 + .dockerignore to build Storybook and run a minimal Node static server.
  • Introduce server.cjs static server intended to avoid URL rewriting/redirect issues.
  • Add a managerHead redirect 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.

Comment on lines 7 to +8
stories: ['./stories/**/*.mdx', '../lib/**/*.stories.@(ts|tsx)'],
managerHead: (head) => `
kyleknighted and others added 2 commits March 18, 2026 13:57
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>
@kyleknighted kyleknighted merged commit abc476e into main Mar 18, 2026
4 checks passed
@kyleknighted kyleknighted deleted the devin/1773842536-railway-storybook-deploy branch March 18, 2026 19:01
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants