Skip to content

Commit 066798f

Browse files
committed
feat(server): serve SPA under / and mount API under /api; multi-stage Dockerfile builds frontend + server
1 parent 385d799 commit 066798f

File tree

2 files changed

+56
-29
lines changed

2 files changed

+56
-29
lines changed

server/Dockerfile

Lines changed: 31 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -1,24 +1,37 @@
1-
# Dockerfile for editor mock API (TypeScript/Express)
2-
FROM node:18-alpine AS build
3-
WORKDIR /app
4-
# Copy only package.json from the build context. The repo root package-lock.json
5-
# is not available when building from the subfolder context, which caused
6-
# BuildKit checksum errors. Use npm ci when a lockfile exists, otherwise
7-
# fall back to npm install so the build doesn't fail.
8-
COPY package.json ./
9-
RUN npm ci --production=false || npm install --production=false
10-
COPY tsconfig.json ./
11-
COPY src ./src
1+
# Multi-stage Dockerfile: build frontend and server, produce single image
2+
3+
# 1) Build web (editor)
4+
FROM node:20-alpine AS web-build
5+
WORKDIR /web
6+
COPY editor/package.json editor/package-lock.json* ./
7+
WORKDIR /web
8+
RUN if [ -f package-lock.json ]; then npm ci; else npm install; fi
9+
COPY editor/ ./editor
10+
WORKDIR /web/editor
11+
RUN npm run build
12+
13+
# 2) Build server
14+
FROM node:18-alpine AS server-build
15+
WORKDIR /srv
16+
COPY server/package.json server/package-lock.json* ./
17+
RUN if [ -f package-lock.json ]; then npm ci --production=false; else npm install --production=false; fi
18+
COPY server/ ./server
19+
WORKDIR /srv/server
1220
RUN npm run build
1321

22+
# 3) Final image
1423
FROM node:18-alpine
1524
WORKDIR /app
16-
# Install only production deps
17-
COPY package.json ./
18-
# If a package-lock.json is present, npm ci will be used; if not, fall back
19-
# to npm install so the build doesn't fail when the lockfile is absent in the
20-
# build context (common when CI checkouts or subfolders don't include it).
21-
RUN npm ci --production || npm install --production
22-
COPY --from=build /app/dist ./dist
25+
# Install production deps for server
26+
COPY server/package.json ./
27+
RUN if [ -f package-lock.json ]; then npm ci --production; else npm install --production; fi
28+
29+
# Copy built server
30+
COPY --from=server-build /srv/server/dist ./dist
31+
32+
# Copy built frontend to editor-dist so server can serve it
33+
COPY --from=web-build /web/editor/dist ./editor-dist
34+
2335
EXPOSE 4000
36+
ENV NODE_ENV=production
2437
CMD ["node", "dist/index.js"]

server/src/index.ts

Lines changed: 25 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,5 @@
1+
import path from "path";
2+
import fs from "fs";
13
import express from "express";
24
import cors from "cors";
35

@@ -7,27 +9,39 @@ const PORT = process.env.PORT ? parseInt(process.env.PORT, 10) : 4000;
79
app.use(cors({ origin: true }));
810
app.use(express.json());
911

10-
// Simple health endpoint
11-
app.get("/health", (_req, res) => {
12-
res.json({ status: "ok", uptime: process.uptime() });
13-
});
14-
15-
// Mock auth endpoints for the editor
16-
let mockUser = {
12+
// Mock user for auth endpoints
13+
const mockUser = {
1714
id: "user-1",
1815
name: "Demo User",
19-
avatar: "https://www.gravatar.com/avatar/00000000000000000000000000000000?d=mp&s=128"
16+
avatar:
17+
"https://www.gravatar.com/avatar/00000000000000000000000000000000?d=mp&s=128",
2018
};
2119

22-
app.get("/me", (_req, res) => {
20+
// API mounted under /api to make it easy to host frontend and backend together
21+
app.get("/api/health", (_req, res) => {
22+
res.json({ status: "ok", uptime: process.uptime() });
23+
});
24+
25+
app.get("/api/me", (_req, res) => {
2326
res.json({ authenticated: true, profile: mockUser });
2427
});
2528

26-
app.post("/logout", (_req, res) => {
27-
// for a mock server just return success
29+
app.post("/api/logout", (_req, res) => {
2830
res.json({ ok: true });
2931
});
3032

33+
// Serve static frontend if present in the final image at '../editor-dist'
34+
const staticPath = path.join(__dirname, "..", "editor-dist");
35+
if (fs.existsSync(staticPath)) {
36+
app.use(express.static(staticPath));
37+
38+
// SPA fallback: any non-API route should return index.html so the client-side router can handle it
39+
app.get("/*", (req, res, next) => {
40+
if (req.path.startsWith("/api/")) return next();
41+
res.sendFile(path.join(staticPath, "index.html"));
42+
});
43+
}
44+
3145
app.listen(PORT, () => {
3246
console.log(`Server running on http://localhost:${PORT}`);
3347
});

0 commit comments

Comments
 (0)