From f0628bdc2b8e562e8bba6ba75d5a9417039e10cb Mon Sep 17 00:00:00 2001 From: innrVoice Date: Tue, 18 Aug 2026 23:14:18 +0300 Subject: [PATCH] Migrate Railway Storybook runtime to Caddy --- .dockerignore | 17 ++++++++++ .gitignore | 2 ++ Caddyfile | 64 +++++++++++++++++++++++++++++++++++++ Dockerfile | 23 +++++++++++++ package.json | 1 - railway.toml | 6 ++-- scripts/serve-storybook.mjs | 59 ---------------------------------- 7 files changed, 109 insertions(+), 63 deletions(-) create mode 100644 .dockerignore create mode 100644 Caddyfile create mode 100644 Dockerfile delete mode 100644 scripts/serve-storybook.mjs diff --git a/.dockerignore b/.dockerignore new file mode 100644 index 0000000..82abe09 --- /dev/null +++ b/.dockerignore @@ -0,0 +1,17 @@ +.git +.githooks +.github +.DS_Store +.env +.env.* +node_modules +.pnpm-store +.npm-cache +lib +storybook-static +coverage +test-results +playwright-report +*.log +AGENTS.md +CONTINUITY.md diff --git a/.gitignore b/.gitignore index 4783399..f954814 100644 --- a/.gitignore +++ b/.gitignore @@ -11,3 +11,5 @@ debug-storybook.log *storybook.log storybook-static coverage +AGENTS.md +CONTINUITY.md diff --git a/Caddyfile b/Caddyfile new file mode 100644 index 0000000..5083140 --- /dev/null +++ b/Caddyfile @@ -0,0 +1,64 @@ +{ + admin off + persist_config off + auto_https off + + servers { + trusted_proxies static private_ranges 100.0.0.0/8 + trusted_proxies_strict + } +} + +:{$PORT:3000} { + log { + format json + } + + header { + Content-Security-Policy "frame-ancestors 'self';" + Referrer-Policy "strict-origin-when-cross-origin" + Strict-Transport-Security "max-age=63072000; includeSubDomains" + X-Content-Type-Options "nosniff" + X-Frame-Options "SAMEORIGIN" + -Server + } + + @health { + method GET HEAD + path /health + } + handle @health { + header { + Cache-Control "no-store" + Content-Type "text/plain; charset=utf-8" + } + respond "ok" 200 + } + + @unsupportedMethod not method GET HEAD + handle @unsupportedMethod { + header Cache-Control "no-store" + respond "Not found" 404 + } + + handle { + route { + root * /srv + encode zstd gzip + + header Cache-Control "public, max-age=0, s-maxage=60, stale-while-revalidate=300" + + @immutable path /assets/* + header @immutable Cache-Control "public, max-age=31536000, immutable" + + @root path / + rewrite @root /index.html + + @notFound not file + header @notFound Cache-Control "no-store" + respond @notFound "Not found" 404 + + file_server + } + } +} diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 0000000..7313d25 --- /dev/null +++ b/Dockerfile @@ -0,0 +1,23 @@ +FROM node:22-alpine AS build + +WORKDIR /workspace + +RUN corepack enable && corepack prepare pnpm@11.1.2 --activate + +COPY package.json pnpm-lock.yaml pnpm-workspace.yaml ./ +COPY scripts/setup-git-hooks.mjs ./scripts/setup-git-hooks.mjs + +RUN pnpm install --frozen-lockfile + +COPY . . + +RUN pnpm build:sb + +FROM caddy:2-alpine AS runtime + +WORKDIR /srv + +COPY Caddyfile /etc/caddy/Caddyfile +COPY --from=build /workspace/storybook-static /srv + +CMD ["caddy", "run", "--config", "/etc/caddy/Caddyfile", "--adapter", "caddyfile"] diff --git a/package.json b/package.json index 99aa32e..baba0ef 100644 --- a/package.json +++ b/package.json @@ -56,7 +56,6 @@ "lint": "oxlint . --ignore-pattern node_modules --ignore-pattern lib", "prepare": "node scripts/setup-git-hooks.mjs", "preview:sb": "pnpm build:sb && npx serve -s storybook-static -l 3000", - "start:storybook": "node scripts/serve-storybook.mjs", "precommit:checks": "pnpm format:check && pnpm lint && pnpm test", "test": "pnpm test:all", "test:all": "pnpm test:unit && pnpm test:storybook", diff --git a/railway.toml b/railway.toml index eb9ca48..49a4927 100644 --- a/railway.toml +++ b/railway.toml @@ -1,9 +1,9 @@ [build] -builder = "RAILPACK" -buildCommand = "pnpm build:sb" +builder = "DOCKERFILE" +dockerfilePath = "Dockerfile" [deploy] -startCommand = "pnpm start:storybook" healthcheckPath = "/health" +healthcheckTimeout = 60 restartPolicyType = "ON_FAILURE" restartPolicyMaxRetries = 10 diff --git a/scripts/serve-storybook.mjs b/scripts/serve-storybook.mjs deleted file mode 100644 index a8fb741..0000000 --- a/scripts/serve-storybook.mjs +++ /dev/null @@ -1,59 +0,0 @@ -import { createReadStream, existsSync, statSync } from 'node:fs'; -import { extname, join, normalize, resolve } from 'node:path'; -import { createServer } from 'node:http'; - -const rootDir = resolve('storybook-static'); -const host = process.env.HOST ?? '0.0.0.0'; -const port = Number(process.env.PORT ?? 3000); - -const contentTypes = { - '.css': 'text/css; charset=utf-8', - '.html': 'text/html; charset=utf-8', - '.js': 'text/javascript; charset=utf-8', - '.json': 'application/json; charset=utf-8', - '.map': 'application/json; charset=utf-8', - '.png': 'image/png', - '.svg': 'image/svg+xml', - '.txt': 'text/plain; charset=utf-8', - '.webp': 'image/webp', - '.woff': 'font/woff', - '.woff2': 'font/woff2', -}; - -function resolveFilePath(urlPathname) { - const trimmedPath = urlPathname.replace(/^\/+/, ''); - const decodedPath = decodeURIComponent(trimmedPath || 'index.html'); - const normalizedPath = normalize(decodedPath).replace(/^(\.\.(\/|\\|$))+/, ''); - let filePath = join(rootDir, normalizedPath); - - if (existsSync(filePath) && statSync(filePath).isDirectory()) { - filePath = join(filePath, 'index.html'); - } - - if (existsSync(filePath)) { - return filePath; - } - - return join(rootDir, 'index.html'); -} - -const server = createServer((request, response) => { - const requestUrl = new URL(request.url ?? '/', `http://${request.headers.host ?? 'localhost'}`); - - if (requestUrl.pathname === '/health') { - response.writeHead(200, { 'content-type': 'text/plain; charset=utf-8' }); - response.end('ok'); - return; - } - - const filePath = resolveFilePath(requestUrl.pathname); - const extension = extname(filePath); - const contentType = contentTypes[extension] ?? 'application/octet-stream'; - - response.writeHead(200, { 'content-type': contentType }); - createReadStream(filePath).pipe(response); -}); - -server.listen(port, host, () => { - console.log(`Serving Storybook from ${rootDir} on http://${host}:${port}`); -});