feat(docker): build and serve with Node and npm instead of Nginx

This commit is contained in:
venus
2026-09-13 01:59:49 -05:00
parent e733f479dd
commit 78b1e0b485
12 changed files with 294 additions and 200 deletions
+136
View File
@@ -0,0 +1,136 @@
import http from "node:http";
import fs from "node:fs";
import path from "node:path";
import { fileURLToPath } from "node:url";
const MIME_TYPES = {
".html": "text/html; charset=utf-8",
".css": "text/css; charset=utf-8",
".js": "application/javascript; charset=utf-8",
".mjs": "application/javascript; charset=utf-8",
".json": "application/json; charset=utf-8",
".xml": "application/xml; charset=utf-8",
".svg": "image/svg+xml",
".png": "image/png",
".jpg": "image/jpeg",
".jpeg": "image/jpeg",
".webp": "image/webp",
".gif": "image/gif",
".ico": "image/x-icon",
".woff": "font/woff",
".woff2": "font/woff2",
".ttf": "font/ttf",
".txt": "text/plain; charset=utf-8"
};
/**
* Creates the HTTP server instance for serving static files from _site
* @param {string} [customSiteDir] Optional custom directory to serve from
* @returns {http.Server}
*/
export function createServer(customSiteDir) {
const siteDir = path.resolve(customSiteDir || path.join(process.cwd(), "_site"));
return http.createServer((req, res) => {
// Standard security headers
res.setHeader("X-Content-Type-Options", "nosniff");
res.setHeader("X-Frame-Options", "DENY");
res.setHeader("Referrer-Policy", "strict-origin-when-cross-origin");
const parsedUrl = new URL(req.url, "http://localhost");
// Health check endpoint for container orchestrators & telemetry
if (parsedUrl.pathname === "/healthz" || parsedUrl.pathname === "/health") {
res.writeHead(200, { "Content-Type": "application/json; charset=utf-8" });
res.end(JSON.stringify({ status: "ok", uptime: process.uptime() }));
return;
}
// Path normalization & security validation against directory traversal
let decodedPath = "";
try {
decodedPath = decodeURIComponent(parsedUrl.pathname);
} catch {
res.writeHead(400, { "Content-Type": "text/plain" });
res.end("Bad Request");
return;
}
const resolvedPath = path.resolve(siteDir, `.${decodedPath}`);
// Boundary check: ensure target path is within siteDir
if (!resolvedPath.startsWith(siteDir)) {
res.writeHead(403, { "Content-Type": "text/plain" });
res.end("Forbidden");
return;
}
let targetFile = resolvedPath;
// Directory index resolution
if (fs.existsSync(targetFile) && fs.statSync(targetFile).isDirectory()) {
targetFile = path.join(targetFile, "index.html");
}
// Clean URL extensionless fallback (/posts/my-post -> /posts/my-post.html)
if (!fs.existsSync(targetFile) && fs.existsSync(`${targetFile}.html`)) {
targetFile = `${targetFile}.html`;
}
// Check if target file exists and is a regular file
if (fs.existsSync(targetFile) && fs.statSync(targetFile).isFile()) {
const ext = path.extname(targetFile).toLowerCase();
const contentType = MIME_TYPES[ext] || "application/octet-stream";
// Cache control: assets get long-term caching, html gets short
if (decodedPath.startsWith("/assets/")) {
res.setHeader("Cache-Control", "public, max-age=2592000, immutable");
} else {
res.setHeader("Cache-Control", "public, max-age=0, must-revalidate");
}
res.writeHead(200, { "Content-Type": contentType });
const stream = fs.createReadStream(targetFile);
stream.pipe(res);
return;
}
// 404 Not Found fallback
const custom404 = path.join(siteDir, "404.html");
if (fs.existsSync(custom404)) {
res.writeHead(404, { "Content-Type": "text/html; charset=utf-8" });
fs.createReadStream(custom404).pipe(res);
} else {
res.writeHead(404, { "Content-Type": "text/plain; charset=utf-8" });
res.end("404 Not Found");
}
});
}
// Direct CLI execution handler
const currentFilePath = fileURLToPath(import.meta.url);
if (process.argv[1] && path.resolve(process.argv[1]) === currentFilePath) {
const port = parseInt(process.env.PORT || "8080", 10);
const host = process.env.HOST || "0.0.0.0";
const siteDir = path.resolve(process.cwd(), "_site");
if (!fs.existsSync(siteDir)) {
console.warn(`[Static Server] Warning: '${siteDir}' does not exist yet. Run 'npm run build' first.`);
}
const server = createServer();
server.listen(port, host, () => {
console.log(`[Static Server] Serving '${siteDir}' on http://${host}:${port}`);
});
// Graceful termination handling
const shutdown = (signal) => {
console.log(`\n[Static Server] Received ${signal}. Shutting down gracefully...`);
server.close(() => {
process.exit(0);
});
};
process.on("SIGINT", () => shutdown("SIGINT"));
process.on("SIGTERM", () => shutdown("SIGTERM"));
}