@rebasepro/cli 0.0.1-canary.0 → 0.0.1-canary.000dc36

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (51) hide show
  1. package/dist/commands/auth.d.ts +1 -0
  2. package/dist/commands/cli.test.d.ts +1 -0
  3. package/dist/commands/db.d.ts +1 -0
  4. package/dist/commands/dev.d.ts +1 -0
  5. package/dist/commands/dev.test.d.ts +1 -0
  6. package/dist/commands/doctor.d.ts +1 -0
  7. package/dist/commands/generate_sdk.d.ts +18 -0
  8. package/dist/commands/init.d.ts +4 -0
  9. package/dist/commands/init.test.d.ts +1 -0
  10. package/dist/commands/schema.d.ts +1 -0
  11. package/dist/index.cjs +1064 -35
  12. package/dist/index.cjs.map +1 -1
  13. package/dist/index.d.ts +5 -0
  14. package/dist/index.es.js +1066 -34
  15. package/dist/index.es.js.map +1 -1
  16. package/dist/utils/project.d.ts +45 -0
  17. package/dist/utils/project.test.d.ts +1 -0
  18. package/package.json +19 -11
  19. package/templates/template/.dockerignore +28 -0
  20. package/templates/template/.env.example +96 -0
  21. package/templates/template/README.md +84 -17
  22. package/templates/template/backend/Dockerfile +55 -10
  23. package/templates/template/backend/drizzle.config.ts +24 -4
  24. package/templates/template/backend/functions/hello.ts +52 -0
  25. package/templates/template/backend/package.json +30 -34
  26. package/templates/template/backend/src/env.ts +52 -0
  27. package/templates/template/backend/src/index.ts +113 -99
  28. package/templates/template/backend/tsconfig.json +1 -1
  29. package/templates/template/config/collections/authors.ts +45 -0
  30. package/templates/template/config/collections/index.ts +5 -0
  31. package/templates/template/config/collections/posts.ts +91 -0
  32. package/templates/template/config/collections/tags.ts +27 -0
  33. package/templates/template/config/package.json +28 -0
  34. package/templates/template/docker-compose.yml +50 -14
  35. package/templates/template/frontend/Dockerfile +36 -14
  36. package/templates/template/frontend/nginx.conf +40 -0
  37. package/templates/template/frontend/package.json +9 -10
  38. package/templates/template/frontend/src/App.tsx +24 -110
  39. package/templates/template/frontend/src/index.css +15 -1
  40. package/templates/template/frontend/src/main.tsx +2 -2
  41. package/templates/template/frontend/vite.config.ts +33 -3
  42. package/templates/template/package.json +12 -16
  43. package/templates/template/pnpm-workspace.yaml +4 -0
  44. package/templates/template/scripts/example.ts +91 -0
  45. package/templates/template/.env.template +0 -31
  46. package/templates/template/backend/scripts/db-generate.ts +0 -60
  47. package/templates/template/shared/collections/index.ts +0 -3
  48. package/templates/template/shared/collections/posts.ts +0 -53
  49. package/templates/template/shared/package.json +0 -28
  50. /package/templates/template/{shared → config}/index.ts +0 -0
  51. /package/templates/template/{shared → config}/tsconfig.json +0 -0
@@ -1,134 +1,148 @@
1
- import express from "express";
2
- import cors from "cors";
1
+ import { Hono } from "hono";
2
+ import { cors } from "hono/cors";
3
+ import { secureHeaders } from "hono/secure-headers";
4
+ import { getRequestListener } from "@hono/node-server";
5
+ import { createServer } from "http";
3
6
  import path from "path";
4
7
  import { fileURLToPath } from "url";
5
- import { createServer } from "http";
6
- import { createPostgresDatabaseConnection, initializeRebaseAPI, initializeRebaseBackend, serveSPA } from "@rebasepro/backend";
7
-
8
+ import {
9
+ initializeRebaseBackend,
10
+ serveSPA,
11
+ HonoEnv,
12
+ listenWithPortRetry,
13
+ cleanupDevPortFile,
14
+ logger
15
+ } from "@rebasepro/server-core";
16
+ import { createPostgresDatabaseConnection, createPostgresBootstrapper } from "@rebasepro/server-postgresql";
8
17
  import { enums, relations, tables } from "./schema.generated";
9
-
10
- import * as dotenv from "dotenv";
18
+ import { env } from "./env";
11
19
 
12
20
  const __filename = fileURLToPath(import.meta.url);
13
21
  const __dirname = path.dirname(__filename);
14
22
 
15
- // Load environment from project root
16
- dotenv.config({ path: path.resolve(__dirname, "../../.env") });
23
+ // ─── App ─────────────────────────────────────────────────────────────
24
+ const app = new Hono<HonoEnv>();
17
25
 
18
- const app = express();
19
- const server = createServer(app);
26
+ const isProduction = env.NODE_ENV === "production";
27
+ const allowedOrigins = isProduction
28
+ ? (env.CORS_ORIGINS || env.FRONTEND_URL || "https://yourdomain.com").split(",").map(s => s.trim())
29
+ : [];
20
30
 
21
- // PostgreSQL connection
22
- const databaseUrl = process.env.DATABASE_URL;
23
- if (!databaseUrl) {
24
- throw new Error("DATABASE_URL environment variable is not set");
25
- }
26
- const db = createPostgresDatabaseConnection(databaseUrl);
31
+ app.use("/*", cors({
32
+ origin: (origin) => {
33
+ if (!isProduction) return origin || "*";
34
+ return allowedOrigins.includes(origin) ? origin : null;
35
+ },
36
+ credentials: true
37
+ }));
27
38
 
28
- // Middleware
29
- app.use(cors());
30
- app.use(express.json({ limit: "10mb" }));
39
+ app.use("/*", secureHeaders());
31
40
 
41
+ // ─── Database ────────────────────────────────────────────────────────
42
+ const databaseUrl = env.DATABASE_URL;
43
+
44
+ const { db, pool, connectionString } = createPostgresDatabaseConnection(databaseUrl);
45
+
46
+ // ─── Start ───────────────────────────────────────────────────────────
32
47
  async function startServer() {
33
- const jwtSecret = process.env.JWT_SECRET;
34
- if (!jwtSecret) {
35
- throw new Error("JWT_SECRET environment variable is not set");
36
- }
48
+ const jwtSecret = env.JWT_SECRET;
49
+ const PORT = env.PORT;
50
+ const server = createServer(getRequestListener(app.fetch));
37
51
 
38
- // Initialize Rebase Backend with auth
39
52
  const backend = await initializeRebaseBackend({
40
- collectionsDir: path.resolve(__dirname, "../../shared/collections"),
53
+ collectionsDir: path.resolve(__dirname, "../../config/collections"),
54
+ functionsDir: path.resolve(__dirname, "../functions"),
41
55
  server,
42
56
  app,
43
- datasource: {
44
- "(default)": {
57
+ bootstrappers: [
58
+ createPostgresBootstrapper({
45
59
  connection: db,
46
- schema: { tables, enums, relations },
47
- adminConnectionString: process.env.ADMIN_CONNECTION_STRING || process.env.DATABASE_URL
48
- }
49
- },
60
+ schema: { tables,
61
+ enums,
62
+ relations },
63
+ adminConnectionString: env.ADMIN_CONNECTION_STRING || databaseUrl,
64
+ connectionString
65
+ })
66
+ ],
50
67
  auth: {
51
68
  jwtSecret,
52
- accessExpiresIn: process.env.JWT_ACCESS_EXPIRES_IN || "1h",
53
- refreshExpiresIn: process.env.JWT_REFRESH_EXPIRES_IN || "30d",
54
- google: process.env.GOOGLE_CLIENT_ID ? {
55
- clientId: process.env.GOOGLE_CLIENT_ID
56
- } : undefined,
69
+ accessExpiresIn: env.JWT_ACCESS_EXPIRES_IN,
70
+ refreshExpiresIn: env.JWT_REFRESH_EXPIRES_IN,
71
+ serviceKey: env.REBASE_SERVICE_KEY,
72
+ google: env.GOOGLE_CLIENT_ID
73
+ ? { clientId: env.GOOGLE_CLIENT_ID }
74
+ : undefined,
57
75
  seedDefaultRoles: true,
58
- allowRegistration: process.env.ALLOW_REGISTRATION === "true",
59
- email: process.env.SMTP_HOST ? {
60
- from: process.env.SMTP_FROM || "noreply@example.com",
61
- appName: process.env.APP_NAME || "Rebase",
62
- resetPasswordUrl: process.env.FRONTEND_URL || "http://localhost:5173",
63
- verifyEmailUrl: process.env.FRONTEND_URL || "http://localhost:5173",
64
- smtp: {
65
- host: process.env.SMTP_HOST,
66
- port: parseInt(process.env.SMTP_PORT || "587"),
67
- secure: process.env.SMTP_SECURE === "true",
68
- auth: process.env.SMTP_USER ? {
69
- user: process.env.SMTP_USER,
70
- pass: process.env.SMTP_PASS || ""
71
- } : undefined
72
- }
73
- } : undefined
76
+ allowRegistration: env.ALLOW_REGISTRATION
74
77
  },
75
- storage: {
76
- type: "local",
77
- basePath: path.resolve(__dirname, "../../uploads")
78
- }
78
+ storage: env.STORAGE_TYPE === "s3"
79
+ ? {
80
+ type: "s3",
81
+ bucket: env.S3_BUCKET!,
82
+ region: env.S3_REGION || "auto",
83
+ accessKeyId: env.S3_ACCESS_KEY_ID || "",
84
+ secretAccessKey: env.S3_SECRET_ACCESS_KEY || "",
85
+ endpoint: env.S3_ENDPOINT,
86
+ forcePathStyle: env.S3_FORCE_PATH_STYLE
87
+ }
88
+ : {
89
+ type: "local",
90
+ basePath: env.STORAGE_PATH || path.resolve(__dirname, "../../uploads")
91
+ },
92
+ history: true
79
93
  });
80
94
 
81
- // Initialize REST/GraphQL API
82
- await initializeRebaseAPI(app, backend, {
83
- basePath: "/api",
84
- collectionsDir: path.resolve(__dirname, "../../shared/collections"),
85
- enableGraphQL: true,
86
- enableREST: true,
87
- cors: {
88
- origin: true,
89
- credentials: true
90
- }
95
+ // ─── Health check ─────────────────────────────────────────────
96
+ app.get("/health", async (c) => {
97
+ const result = await backend.healthCheck();
98
+ const status = result.healthy ? 200 : 503;
99
+ return c.json({
100
+ status: result.healthy ? "ok" : "degraded",
101
+ latencyMs: result.latencyMs,
102
+ ...(result.details ? { details: result.details } : {})
103
+ }, status);
91
104
  });
92
105
 
93
- // Serve SPA in production
94
- if (process.env.NODE_ENV === "production") {
95
- serveSPA(app, {
96
- frontendPath: path.join(__dirname, "../../frontend/dist"),
97
- excludePaths: ["/health"]
98
- });
106
+ // Serve the frontend in production
107
+ if (isProduction) {
108
+ serveSPA(app, { frontendPath: path.join(__dirname, "../../frontend/dist") });
99
109
  }
100
110
 
101
- app.get("/health", (req, res) => {
102
- res.json({
103
- status: "ok",
104
- timestamp: new Date().toISOString(),
105
- environment: process.env.NODE_ENV
106
- });
107
- });
108
-
109
- // Error handling
110
- app.use((error: Error, _req: express.Request, res: express.Response, _next: express.NextFunction) => {
111
- console.error("Unhandled error:", error);
112
- res.status(500).json({
113
- error: "Internal server error",
114
- message: error.message
111
+ if (!isProduction) {
112
+ // Dev mode: retry the next port if the current one is in use
113
+ const projectRoot = path.resolve(__dirname, "../..");
114
+ const actualPort = await listenWithPortRetry(server, PORT, { portFileDir: projectRoot });
115
+
116
+ // Clean up port file on exit
117
+ const cleanup = () => cleanupDevPortFile(projectRoot);
118
+ process.on("SIGINT", cleanup);
119
+ process.on("SIGTERM", cleanup);
120
+ process.on("exit", cleanup);
121
+
122
+ logger.info(`Server running at http://localhost:${actualPort}`);
123
+ } else {
124
+ server.listen(PORT, () => {
125
+ logger.info(`Server running at http://localhost:${PORT}`);
115
126
  });
116
- });
117
-
118
- const PORT = process.env.PORT || 3001;
127
+ }
119
128
 
120
- server.listen(PORT, () => {
121
- console.log(`🚀 Server running at http://localhost:${PORT}`);
122
- console.log(` • REST API: http://localhost:${PORT}/api`);
123
- console.log(` • GraphQL: http://localhost:${PORT}/api/graphql`);
124
- console.log(` Swagger docs: http://localhost:${PORT}/api/swagger`);
125
- console.log(` • Health: http://localhost:${PORT}/health`);
126
- });
129
+ // ─── Graceful Shutdown ───────────────────────────────────────────────
130
+ // Uses the framework's built-in shutdown() which drains connections,
131
+ // stops the cron scheduler, and force-exits after 15s timeout.
132
+ const gracefulShutdown = async (signal: string) => {
133
+ logger.info(`Received ${signal}, shutting down...`);
134
+ await backend.shutdown();
135
+ await pool.end();
136
+ logger.info("Database pool closed");
137
+ process.exit(0);
138
+ };
139
+ process.on("SIGTERM", () => gracefulShutdown("SIGTERM"));
140
+ process.on("SIGINT", () => gracefulShutdown("SIGINT"));
127
141
  }
128
142
 
129
143
  startServer().catch(err => {
130
- console.error("Failed to start server:", err);
144
+ logger.error("Failed to start server", { error: err instanceof Error ? err : new Error(String(err)) });
131
145
  process.exit(1);
132
146
  });
133
147
 
134
- export { app, server };
148
+ export { app };
@@ -15,5 +15,5 @@
15
15
  "sourceMap": true,
16
16
  "jsx": "preserve"
17
17
  },
18
- "include": ["src/**/*", "../shared/**/*", "drizzle.config.ts"]
18
+ "include": ["src/**/*", "../config/**/*", "drizzle.config.ts"]
19
19
  }
@@ -0,0 +1,45 @@
1
+ import { EntityCollection } from "@rebasepro/types";
2
+
3
+ const authorsCollection: EntityCollection = {
4
+ name: "Authors",
5
+ singularName: "Author",
6
+ slug: "authors",
7
+ table: "authors",
8
+ icon: "Person",
9
+ properties: {
10
+ id: {
11
+ name: "ID",
12
+ type: "number",
13
+ isId: "increment"
14
+ },
15
+ name: {
16
+ name: "Name",
17
+ type: "string",
18
+ validation: {
19
+ required: true
20
+ }
21
+ },
22
+ email: {
23
+ name: "Email",
24
+ type: "string",
25
+ validation: {
26
+ required: true
27
+ }
28
+ },
29
+ picture: {
30
+ name: "Picture",
31
+ type: "string",
32
+ storage: {
33
+ storagePath: "author_pictures/"
34
+ }
35
+ }
36
+ },
37
+ propertiesOrder: [
38
+ "id",
39
+ "name",
40
+ "email",
41
+ "picture"
42
+ ]
43
+ };
44
+
45
+ export default authorsCollection;
@@ -0,0 +1,5 @@
1
+ import postsCollection from "./posts";
2
+ import authorsCollection from "./authors";
3
+ import tagsCollection from "./tags";
4
+
5
+ export const collections = [postsCollection, authorsCollection, tagsCollection];
@@ -0,0 +1,91 @@
1
+ import { EntityCollection } from "@rebasepro/types";
2
+ import authorsCollection from "./authors";
3
+ import tagsCollection from "./tags";
4
+
5
+ const postsCollection: EntityCollection = {
6
+ name: "Posts",
7
+ singularName: "Post",
8
+ slug: "posts",
9
+ table: "posts",
10
+ icon: "Article",
11
+ properties: {
12
+ id: {
13
+ name: "ID",
14
+ type: "number",
15
+ validation: {
16
+ required: true
17
+ }
18
+ },
19
+ title: {
20
+ name: "Title",
21
+ type: "string",
22
+ validation: {
23
+ required: true
24
+ }
25
+ },
26
+ content: {
27
+ name: "Content",
28
+ type: "string",
29
+ multiline: true
30
+ },
31
+ status: {
32
+ name: "Status",
33
+ type: "string",
34
+ enum: [
35
+ {
36
+ id: "draft",
37
+ label: "Draft",
38
+ color: "gray"
39
+ },
40
+ {
41
+ id: "review",
42
+ label: "In Review",
43
+ color: "orange"
44
+ },
45
+ {
46
+ id: "published",
47
+ label: "Published",
48
+ color: "green"
49
+ }
50
+ ]
51
+ },
52
+ author: {
53
+ name: "Author",
54
+ type: "relation",
55
+ relationName: "author",
56
+ relation: {
57
+ relationName: "author",
58
+ cardinality: "one",
59
+ direction: "owning",
60
+ target: () => authorsCollection
61
+ }
62
+ },
63
+ tags: {
64
+ name: "Tags",
65
+ type: "relation",
66
+ relationName: "tags",
67
+ relation: {
68
+ relationName: "tags",
69
+ cardinality: "many",
70
+ direction: "owning",
71
+ target: () => tagsCollection
72
+ }
73
+ }
74
+ },
75
+ relations: [
76
+ {
77
+ relationName: "author",
78
+ target: () => authorsCollection,
79
+ cardinality: "one",
80
+ direction: "owning"
81
+ },
82
+ {
83
+ relationName: "tags",
84
+ target: () => tagsCollection,
85
+ cardinality: "many",
86
+ direction: "owning"
87
+ }
88
+ ]
89
+ };
90
+
91
+ export default postsCollection;
@@ -0,0 +1,27 @@
1
+ import { EntityCollection } from "@rebasepro/types";
2
+
3
+ const tagsCollection: EntityCollection = {
4
+ name: "Tags",
5
+ singularName: "Tag",
6
+ slug: "tags",
7
+ table: "tags",
8
+ icon: "Tag",
9
+ properties: {
10
+ id: {
11
+ name: "ID",
12
+ type: "number",
13
+ validation: {
14
+ required: true
15
+ }
16
+ },
17
+ name: {
18
+ name: "Tag Name",
19
+ type: "string",
20
+ validation: {
21
+ required: true
22
+ }
23
+ }
24
+ }
25
+ };
26
+
27
+ export default tagsCollection;
@@ -0,0 +1,28 @@
1
+ {
2
+ "name": "{{PROJECT_NAME}}-config",
3
+ "version": "1.0.0",
4
+ "description": "Shared collections for frontend and backend",
5
+ "main": "dist/index.js",
6
+ "types": "dist/index.d.ts",
7
+ "type": "module",
8
+ "private": true,
9
+ "scripts": {
10
+ "build": "tsc",
11
+ "dev": "tsc --watch",
12
+ "clean": "rm -rf dist"
13
+ },
14
+ "dependencies": {
15
+ "@rebasepro/types": "workspace:*"
16
+ },
17
+ "devDependencies": {
18
+ "typescript": "^5.9.2"
19
+ },
20
+ "exports": {
21
+ ".": {
22
+ "import": "./dist/index.js",
23
+ "types": "./dist/index.d.ts",
24
+ "default": "./dist/index.js"
25
+ },
26
+ "./package.json": "./package.json"
27
+ }
28
+ }
@@ -1,48 +1,84 @@
1
- version: '3.8'
1
+ # ─── Rebase — Production Docker Compose ──────────────────────────────
2
+ #
3
+ # Quick start:
4
+ # cp .env.example .env # Edit with your secrets
5
+ # docker compose up -d # Start everything
6
+ #
7
+ # This runs:
8
+ # 1. PostgreSQL 18 with persistent data
9
+ # 2. Backend (Node.js / Hono) with health checks
10
+ # 3. Frontend (nginx) serving the Vite build
11
+ #
12
+ # For development, use `pnpm dev` instead.
13
+ # ─────────────────────────────────────────────────────────────────────
2
14
 
3
15
  services:
16
+ # ── PostgreSQL ───────────────────────────────────────────────────────
4
17
  db:
5
- image: postgres:15-alpine
18
+ image: postgres:18-alpine
19
+ restart: unless-stopped
6
20
  environment:
7
21
  POSTGRES_USER: rebase
8
- POSTGRES_PASSWORD: rebasepassword
22
+ POSTGRES_PASSWORD: ${DATABASE_PASSWORD:-changeme}
9
23
  POSTGRES_DB: rebase
10
24
  ports:
11
25
  - "5432:5432"
12
26
  volumes:
13
27
  - postgres_data:/var/lib/postgresql/data
14
28
  healthcheck:
15
- test: ["CMD-SHELL", "pg_isready -U rebase"]
29
+ test: ["CMD-SHELL", "pg_isready -U rebase -d rebase"]
16
30
  interval: 5s
17
31
  timeout: 5s
18
- retries: 5
32
+ retries: 10
33
+ start_period: 10s
34
+ # Production tuning (adjust for your workload)
35
+ command:
36
+ - "postgres"
37
+ - "-c"
38
+ - "shared_buffers=256MB"
39
+ - "-c"
40
+ - "max_connections=100"
41
+ - "-c"
42
+ - "work_mem=4MB"
43
+ - "-c"
44
+ - "effective_cache_size=768MB"
45
+ - "-c"
46
+ - "log_min_duration_statement=1000"
19
47
 
48
+ # ── Backend ──────────────────────────────────────────────────────────
20
49
  backend:
21
- build:
50
+ build:
22
51
  context: .
23
52
  dockerfile: backend/Dockerfile
53
+ restart: unless-stopped
24
54
  ports:
25
- - "3001:3001"
55
+ - "${PORT:-3001}:3001"
56
+ env_file: .env
26
57
  environment:
27
- - DATABASE_URL=postgresql://rebase:rebasepassword@db:5432/rebase
28
- - JWT_SECRET=super-secret-jwt-key
29
- - NODE_ENV=development
58
+ # Override DATABASE_URL to point to the Docker network service
59
+ DATABASE_URL: postgresql://rebase:${DATABASE_PASSWORD:-changeme}@db:5432/rebase
60
+ ADMIN_CONNECTION_STRING: postgresql://rebase:${DATABASE_PASSWORD:-changeme}@db:5432/rebase
61
+ NODE_ENV: production
62
+ PORT: "3001"
30
63
  depends_on:
31
64
  db:
32
65
  condition: service_healthy
33
66
  volumes:
34
- - ./backend/uploads:/app/backend/uploads
67
+ - uploads:/app/backend/uploads
35
68
 
69
+ # ── Frontend ─────────────────────────────────────────────────────────
36
70
  frontend:
37
71
  build:
38
72
  context: .
39
73
  dockerfile: frontend/Dockerfile
74
+ restart: unless-stopped
40
75
  ports:
41
- - "5173:5173"
42
- environment:
43
- - VITE_API_URL=http://localhost:3001
76
+ - "80:80"
44
77
  depends_on:
45
78
  - backend
46
79
 
47
80
  volumes:
48
81
  postgres_data:
82
+ driver: local
83
+ uploads:
84
+ driver: local
@@ -1,28 +1,50 @@
1
- FROM node:24-alpine AS base
1
+ # ─── Multi-stage production Dockerfile for the Rebase frontend ────────
2
+ # Builds the Vite app, then serves via nginx for proper caching/compression.
3
+ #
4
+ # Build context: the project root (where pnpm-workspace.yaml lives)
5
+ # Usage:
6
+ # docker build -t my-app-frontend -f frontend/Dockerfile .
7
+
8
+ # ── Stage 1: Install + Build ─────────────────────────────────────────
9
+ FROM node:24-alpine AS builder
10
+
2
11
  ENV PNPM_HOME="/pnpm"
3
12
  ENV PATH="$PNPM_HOME:$PATH"
4
13
  RUN corepack enable
14
+
5
15
  RUN apk add --no-cache python3 make g++
6
16
 
7
17
  WORKDIR /app
8
18
 
9
- # Copy root configurations
10
- COPY package.json ./
19
+ # Copy workspace root files
20
+ COPY package.json pnpm-lock.yaml pnpm-workspace.yaml ./
11
21
 
12
- # Copy internal workspaces
22
+ # Copy workspace packages
13
23
  COPY frontend ./frontend
14
- COPY shared ./shared
24
+ COPY config ./config
15
25
 
16
26
  # Install dependencies
17
- RUN pnpm install
27
+ RUN pnpm install --frozen-lockfile
28
+
29
+ # Build config first, then frontend
30
+ RUN pnpm --filter "*-config" run build
31
+ RUN pnpm --filter "*-frontend" run build
32
+
33
+ # ── Stage 2: Serve with nginx ────────────────────────────────────────
34
+ FROM nginx:1.27-alpine AS runtime
35
+
36
+ # Remove default nginx page
37
+ RUN rm -rf /usr/share/nginx/html/*
38
+
39
+ # Copy built assets
40
+ COPY --from=builder /app/frontend/dist /usr/share/nginx/html
41
+
42
+ # Custom nginx config for SPA routing + compression
43
+ COPY frontend/nginx.conf /etc/nginx/conf.d/default.conf
18
44
 
19
- # Build shared and frontend
20
- RUN pnpm run build:shared
21
- RUN pnpm run build:frontend
45
+ EXPOSE 80
22
46
 
23
- # Install a simple static server
24
- RUN npm install -g serve
47
+ HEALTHCHECK --interval=30s --timeout=5s --retries=3 \
48
+ CMD wget --no-verbose --tries=1 --spider http://localhost:80/ || exit 1
25
49
 
26
- WORKDIR /app/frontend
27
- EXPOSE 5173
28
- CMD ["serve", "-s", "dist", "-l", "5173"]
50
+ CMD ["nginx", "-g", "daemon off;"]
@@ -0,0 +1,40 @@
1
+ server {
2
+ listen 80;
3
+ server_name _;
4
+
5
+ root /usr/share/nginx/html;
6
+ index index.html;
7
+
8
+ # Gzip compression
9
+ gzip on;
10
+ gzip_vary on;
11
+ gzip_proxied any;
12
+ gzip_comp_level 6;
13
+ gzip_min_length 256;
14
+ gzip_types
15
+ text/plain
16
+ text/css
17
+ text/javascript
18
+ application/javascript
19
+ application/json
20
+ application/xml
21
+ image/svg+xml
22
+ font/woff2;
23
+
24
+ # Cache static assets aggressively (hashed filenames from Vite)
25
+ location ~* \.(js|css|png|jpg|jpeg|gif|ico|svg|woff|woff2|ttf|eot)$ {
26
+ expires 1y;
27
+ add_header Cache-Control "public, immutable";
28
+ try_files $uri =404;
29
+ }
30
+
31
+ # SPA fallback: serve index.html for all non-file routes
32
+ location / {
33
+ try_files $uri $uri/ /index.html;
34
+ }
35
+
36
+ # Security headers
37
+ add_header X-Frame-Options "SAMEORIGIN" always;
38
+ add_header X-Content-Type-Options "nosniff" always;
39
+ add_header Referrer-Policy "strict-origin-when-cross-origin" always;
40
+ }