@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.
- package/dist/commands/auth.d.ts +1 -0
- package/dist/commands/cli.test.d.ts +1 -0
- package/dist/commands/db.d.ts +1 -0
- package/dist/commands/dev.d.ts +1 -0
- package/dist/commands/dev.test.d.ts +1 -0
- package/dist/commands/doctor.d.ts +1 -0
- package/dist/commands/generate_sdk.d.ts +18 -0
- package/dist/commands/init.d.ts +4 -0
- package/dist/commands/init.test.d.ts +1 -0
- package/dist/commands/schema.d.ts +1 -0
- package/dist/index.cjs +1064 -35
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.ts +5 -0
- package/dist/index.es.js +1066 -34
- package/dist/index.es.js.map +1 -1
- package/dist/utils/project.d.ts +45 -0
- package/dist/utils/project.test.d.ts +1 -0
- package/package.json +19 -11
- package/templates/template/.dockerignore +28 -0
- package/templates/template/.env.example +96 -0
- package/templates/template/README.md +84 -17
- package/templates/template/backend/Dockerfile +55 -10
- package/templates/template/backend/drizzle.config.ts +24 -4
- package/templates/template/backend/functions/hello.ts +52 -0
- package/templates/template/backend/package.json +30 -34
- package/templates/template/backend/src/env.ts +52 -0
- package/templates/template/backend/src/index.ts +113 -99
- package/templates/template/backend/tsconfig.json +1 -1
- package/templates/template/config/collections/authors.ts +45 -0
- package/templates/template/config/collections/index.ts +5 -0
- package/templates/template/config/collections/posts.ts +91 -0
- package/templates/template/config/collections/tags.ts +27 -0
- package/templates/template/config/package.json +28 -0
- package/templates/template/docker-compose.yml +50 -14
- package/templates/template/frontend/Dockerfile +36 -14
- package/templates/template/frontend/nginx.conf +40 -0
- package/templates/template/frontend/package.json +9 -10
- package/templates/template/frontend/src/App.tsx +24 -110
- package/templates/template/frontend/src/index.css +15 -1
- package/templates/template/frontend/src/main.tsx +2 -2
- package/templates/template/frontend/vite.config.ts +33 -3
- package/templates/template/package.json +12 -16
- package/templates/template/pnpm-workspace.yaml +4 -0
- package/templates/template/scripts/example.ts +91 -0
- package/templates/template/.env.template +0 -31
- package/templates/template/backend/scripts/db-generate.ts +0 -60
- package/templates/template/shared/collections/index.ts +0 -3
- package/templates/template/shared/collections/posts.ts +0 -53
- package/templates/template/shared/package.json +0 -28
- /package/templates/template/{shared → config}/index.ts +0 -0
- /package/templates/template/{shared → config}/tsconfig.json +0 -0
|
@@ -1,134 +1,148 @@
|
|
|
1
|
-
import
|
|
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 {
|
|
6
|
-
|
|
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
|
-
//
|
|
16
|
-
|
|
23
|
+
// ─── App ─────────────────────────────────────────────────────────────
|
|
24
|
+
const app = new Hono<HonoEnv>();
|
|
17
25
|
|
|
18
|
-
const
|
|
19
|
-
const
|
|
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
|
-
|
|
22
|
-
|
|
23
|
-
if (!
|
|
24
|
-
|
|
25
|
-
}
|
|
26
|
-
|
|
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
|
-
|
|
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 =
|
|
34
|
-
|
|
35
|
-
|
|
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, "../../
|
|
53
|
+
collectionsDir: path.resolve(__dirname, "../../config/collections"),
|
|
54
|
+
functionsDir: path.resolve(__dirname, "../functions"),
|
|
41
55
|
server,
|
|
42
56
|
app,
|
|
43
|
-
|
|
44
|
-
|
|
57
|
+
bootstrappers: [
|
|
58
|
+
createPostgresBootstrapper({
|
|
45
59
|
connection: db,
|
|
46
|
-
schema: { tables,
|
|
47
|
-
|
|
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:
|
|
53
|
-
refreshExpiresIn:
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
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:
|
|
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
|
-
|
|
77
|
-
|
|
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
|
-
//
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
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
|
|
94
|
-
if (
|
|
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
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
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
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
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
|
-
|
|
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
|
|
148
|
+
export { app };
|
|
@@ -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,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
|
-
|
|
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:
|
|
18
|
+
image: postgres:18-alpine
|
|
19
|
+
restart: unless-stopped
|
|
6
20
|
environment:
|
|
7
21
|
POSTGRES_USER: rebase
|
|
8
|
-
POSTGRES_PASSWORD:
|
|
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:
|
|
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
|
-
|
|
28
|
-
|
|
29
|
-
|
|
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
|
-
-
|
|
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
|
-
- "
|
|
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
|
-
|
|
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
|
|
10
|
-
COPY package.json ./
|
|
19
|
+
# Copy workspace root files
|
|
20
|
+
COPY package.json pnpm-lock.yaml pnpm-workspace.yaml ./
|
|
11
21
|
|
|
12
|
-
# Copy
|
|
22
|
+
# Copy workspace packages
|
|
13
23
|
COPY frontend ./frontend
|
|
14
|
-
COPY
|
|
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
|
-
|
|
20
|
-
RUN pnpm run build:shared
|
|
21
|
-
RUN pnpm run build:frontend
|
|
45
|
+
EXPOSE 80
|
|
22
46
|
|
|
23
|
-
|
|
24
|
-
|
|
47
|
+
HEALTHCHECK --interval=30s --timeout=5s --retries=3 \
|
|
48
|
+
CMD wget --no-verbose --tries=1 --spider http://localhost:80/ || exit 1
|
|
25
49
|
|
|
26
|
-
|
|
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
|
+
}
|