Skip to content

Commit 3569b04

Browse files
Docker Compose Database Setup (#379)
Co-authored-by: Aman Varshney <[email protected]>
1 parent 1f2f150 commit 3569b04

File tree

27 files changed

+479
-140042
lines changed

27 files changed

+479
-140042
lines changed

.changeset/seven-cobras-design.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
"create-better-t-stack": minor
3+
---
4+
5+
Added support for local database setup using Docker Compose for PostgreSQL, MySQL, and MongoDB.

apps/cli/src/constants.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
import path from "node:path";
22
import { fileURLToPath } from "node:url";
3-
import type { ProjectConfig, Frontend } from "./types";
3+
import type { Frontend, ProjectConfig } from "./types";
44
import { getUserPkgManager } from "./utils/get-package-manager";
55

66
const __filename = fileURLToPath(import.meta.url);
Lines changed: 51 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,51 @@
1+
import path from "node:path";
2+
import type { Database, ProjectConfig } from "../../types";
3+
import {
4+
addEnvVariablesToFile,
5+
type EnvVariable,
6+
} from "../project-generation/env-setup";
7+
8+
export async function setupDockerCompose(config: ProjectConfig): Promise<void> {
9+
const { database, projectDir, projectName } = config;
10+
11+
if (database === "none" || database === "sqlite") {
12+
return;
13+
}
14+
15+
try {
16+
await writeEnvFile(projectDir, database, projectName);
17+
} catch (error) {
18+
if (error instanceof Error) {
19+
console.error(`Error: ${error.message}`);
20+
}
21+
}
22+
}
23+
24+
async function writeEnvFile(
25+
projectDir: string,
26+
database: Database,
27+
projectName: string,
28+
) {
29+
const envPath = path.join(projectDir, "apps/server", ".env");
30+
const variables: EnvVariable[] = [
31+
{
32+
key: "DATABASE_URL",
33+
value: getDatabaseUrl(database, projectName),
34+
condition: true,
35+
},
36+
];
37+
await addEnvVariablesToFile(envPath, variables);
38+
}
39+
40+
function getDatabaseUrl(database: Database, projectName: string): string {
41+
switch (database) {
42+
case "postgres":
43+
return `postgresql://postgres:password@localhost:5432/${projectName}`;
44+
case "mysql":
45+
return `mysql://user:password@localhost:3306/${projectName}`;
46+
case "mongodb":
47+
return `mongodb://root:password@localhost:27017/${projectName}?authSource=admin`;
48+
default:
49+
return "";
50+
}
51+
}

apps/cli/src/helpers/project-generation/create-project.ts

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,7 @@ import {
2828
setupBackendFramework,
2929
setupDbOrmTemplates,
3030
setupDeploymentTemplates,
31+
setupDockerComposeTemplates,
3132
setupExamplesTemplate,
3233
setupFrontendTemplates,
3334
} from "./template-manager";
@@ -44,6 +45,7 @@ export async function createProject(options: ProjectConfig) {
4445
await setupBackendFramework(projectDir, options);
4546
if (!isConvex) {
4647
await setupDbOrmTemplates(projectDir, options);
48+
await setupDockerComposeTemplates(projectDir, options);
4749
await setupAuthTemplate(projectDir, options);
4850
}
4951
if (options.examples.length > 0 && options.examples[0] !== "none") {
@@ -94,7 +96,7 @@ export async function createProject(options: ProjectConfig) {
9496

9597
await initializeGit(projectDir, options.git);
9698

97-
displayPostInstallInstructions({
99+
await displayPostInstallInstructions({
98100
...options,
99101
depsInstalled: options.install,
100102
});

apps/cli/src/helpers/project-generation/env-setup.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -187,7 +187,8 @@ export async function setupEnvironmentVariables(
187187
dbSetup === "mongodb-atlas" ||
188188
dbSetup === "neon" ||
189189
dbSetup === "supabase" ||
190-
dbSetup === "d1";
190+
dbSetup === "d1" ||
191+
dbSetup === "docker";
191192

192193
if (database !== "none" && !specializedSetup) {
193194
switch (database) {

apps/cli/src/helpers/project-generation/post-installation.ts

Lines changed: 37 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -7,9 +7,10 @@ import type {
77
ProjectConfig,
88
Runtime,
99
} from "../../types";
10+
import { getDockerStatus } from "../../utils/docker-utils";
1011
import { getPackageExecutionCommand } from "../../utils/package-runner";
1112

12-
export function displayPostInstallInstructions(
13+
export async function displayPostInstallInstructions(
1314
config: ProjectConfig & { depsInstalled: boolean },
1415
) {
1516
const {
@@ -34,7 +35,7 @@ export function displayPostInstallInstructions(
3435

3536
const databaseInstructions =
3637
!isConvex && database !== "none"
37-
? getDatabaseInstructions(database, orm, runCmd, runtime, dbSetup)
38+
? await getDatabaseInstructions(database, orm, runCmd, runtime, dbSetup)
3839
: "";
3940

4041
const tauriInstructions = addons?.includes("tauri")
@@ -193,15 +194,24 @@ function getLintingInstructions(runCmd?: string): string {
193194
)} Format and lint fix: ${`${runCmd} check`}\n`;
194195
}
195196

196-
function getDatabaseInstructions(
197+
async function getDatabaseInstructions(
197198
database: Database,
198199
orm?: ORM,
199200
runCmd?: string,
200201
runtime?: Runtime,
201202
dbSetup?: DatabaseSetup,
202-
): string {
203+
): Promise<string> {
203204
const instructions = [];
204205

206+
if (dbSetup === "docker") {
207+
const dockerStatus = await getDockerStatus(database);
208+
209+
if (dockerStatus.message) {
210+
instructions.push(dockerStatus.message);
211+
instructions.push("");
212+
}
213+
}
214+
205215
if (runtime === "workers" && dbSetup === "d1") {
206216
const packageManager = runCmd === "npm run" ? "npm" : runCmd || "npm";
207217

@@ -255,10 +265,26 @@ function getDatabaseInstructions(
255265
)} Prisma with Bun may require additional configuration. If you encounter errors,\nfollow the guidance provided in the error messages`,
256266
);
257267
}
258-
268+
if (database === "mongodb" && dbSetup === "docker") {
269+
instructions.push(
270+
`${pc.yellow(
271+
"WARNING:",
272+
)} Prisma + MongoDB + Docker combination may not work.`,
273+
);
274+
}
275+
if (dbSetup === "docker") {
276+
instructions.push(
277+
`${pc.cyan("•")} Start docker container: ${`${runCmd} db:start`}`,
278+
);
279+
}
259280
instructions.push(`${pc.cyan("•")} Apply schema: ${`${runCmd} db:push`}`);
260281
instructions.push(`${pc.cyan("•")} Database UI: ${`${runCmd} db:studio`}`);
261282
} else if (orm === "drizzle") {
283+
if (dbSetup === "docker") {
284+
instructions.push(
285+
`${pc.cyan("•")} Start docker container: ${`${runCmd} db:start`}`,
286+
);
287+
}
262288
instructions.push(`${pc.cyan("•")} Apply schema: ${`${runCmd} db:push`}`);
263289
instructions.push(`${pc.cyan("•")} Database UI: ${`${runCmd} db:studio`}`);
264290
if (database === "sqlite" && dbSetup !== "d1") {
@@ -268,6 +294,12 @@ function getDatabaseInstructions(
268294
)} Start local DB (if needed): ${`cd apps/server && ${runCmd} db:local`}`,
269295
);
270296
}
297+
} else if (orm === "mongoose") {
298+
if (dbSetup === "docker") {
299+
instructions.push(
300+
`${pc.cyan("•")} Start docker container: ${`${runCmd} db:start`}`,
301+
);
302+
}
271303
} else if (orm === "none") {
272304
instructions.push(
273305
`${pc.yellow("NOTE:")} Manual database schema setup required.`,

apps/cli/src/helpers/project-generation/project-config.ts

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -80,6 +80,12 @@ async function updateRootPackageJson(
8080
scripts["db:migrate"] = `turbo -F ${backendPackageName} db:migrate`;
8181
}
8282
}
83+
if (options.dbSetup === "docker") {
84+
scripts["db:start"] = `turbo -F ${backendPackageName} db:start`;
85+
scripts["db:watch"] = `turbo -F ${backendPackageName} db:watch`;
86+
scripts["db:stop"] = `turbo -F ${backendPackageName} db:stop`;
87+
scripts["db:down"] = `turbo -F ${backendPackageName} db:down`;
88+
}
8389
} else if (options.packageManager === "pnpm") {
8490
scripts.dev = devScript;
8591
scripts.build = "pnpm -r build";
@@ -105,6 +111,12 @@ async function updateRootPackageJson(
105111
`pnpm --filter ${backendPackageName} db:migrate`;
106112
}
107113
}
114+
if (options.dbSetup === "docker") {
115+
scripts["db:start"] = `pnpm --filter ${backendPackageName} db:start`;
116+
scripts["db:watch"] = `pnpm --filter ${backendPackageName} db:watch`;
117+
scripts["db:stop"] = `pnpm --filter ${backendPackageName} db:stop`;
118+
scripts["db:down"] = `pnpm --filter ${backendPackageName} db:down`;
119+
}
108120
} else if (options.packageManager === "npm") {
109121
scripts.dev = devScript;
110122
scripts.build = "npm run build --workspaces";
@@ -132,6 +144,14 @@ async function updateRootPackageJson(
132144
`npm run db:migrate --workspace ${backendPackageName}`;
133145
}
134146
}
147+
if (options.dbSetup === "docker") {
148+
scripts["db:start"] =
149+
`npm run db:start --workspace ${backendPackageName}`;
150+
scripts["db:watch"] =
151+
`npm run db:watch --workspace ${backendPackageName}`;
152+
scripts["db:stop"] = `npm run db:stop --workspace ${backendPackageName}`;
153+
scripts["db:down"] = `npm run db:down --workspace ${backendPackageName}`;
154+
}
135155
} else if (options.packageManager === "bun") {
136156
scripts.dev = devScript;
137157
scripts.build = "bun run --filter '*' build";
@@ -157,6 +177,12 @@ async function updateRootPackageJson(
157177
`bun run --filter ${backendPackageName} db:migrate`;
158178
}
159179
}
180+
if (options.dbSetup === "docker") {
181+
scripts["db:start"] = `bun run --filter ${backendPackageName} db:start`;
182+
scripts["db:watch"] = `bun run --filter ${backendPackageName} db:watch`;
183+
scripts["db:stop"] = `bun run --filter ${backendPackageName} db:stop`;
184+
scripts["db:down"] = `bun run --filter ${backendPackageName} db:down`;
185+
}
160186
}
161187

162188
if (options.addons.includes("biome")) {
@@ -246,6 +272,13 @@ async function updateServerPackageJson(
246272
}
247273
}
248274

275+
if (options.dbSetup === "docker") {
276+
scripts["db:start"] = "docker compose up -d";
277+
scripts["db:watch"] = "docker compose up";
278+
scripts["db:stop"] = "docker compose stop";
279+
scripts["db:down"] = "docker compose down";
280+
}
281+
249282
await fs.writeJson(serverPackageJsonPath, serverPackageJson, {
250283
spaces: 2,
251284
});

apps/cli/src/helpers/project-generation/template-manager.ts

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -833,6 +833,26 @@ export async function handleExtras(
833833
}
834834
}
835835

836+
export async function setupDockerComposeTemplates(
837+
projectDir: string,
838+
context: ProjectConfig,
839+
): Promise<void> {
840+
if (context.dbSetup !== "docker" || context.database === "none") {
841+
return;
842+
}
843+
844+
const serverAppDir = path.join(projectDir, "apps/server");
845+
const dockerSrcDir = path.join(
846+
PKG_ROOT,
847+
`templates/db-setup/docker-compose/${context.database}`,
848+
);
849+
850+
if (await fs.pathExists(dockerSrcDir)) {
851+
await processAndCopyFiles("**/*", dockerSrcDir, serverAppDir, context);
852+
} else {
853+
}
854+
}
855+
836856
export async function setupDeploymentTemplates(
837857
projectDir: string,
838858
context: ProjectConfig,

apps/cli/src/helpers/setup/db-setup.ts

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ import pc from "picocolors";
66
import type { ProjectConfig } from "../../types";
77
import { addPackageDependency } from "../../utils/add-package-deps";
88
import { setupCloudflareD1 } from "../database-providers/d1-setup";
9+
import { setupDockerCompose } from "../database-providers/docker-compose-setup";
910
import { setupMongoDBAtlas } from "../database-providers/mongodb-atlas-setup";
1011
import { setupNeonPostgres } from "../database-providers/neon-setup";
1112
import { setupPrismaPostgres } from "../database-providers/prisma-postgres-setup";
@@ -76,7 +77,9 @@ export async function setupDatabase(config: ProjectConfig): Promise<void> {
7677
});
7778
}
7879

79-
if (database === "sqlite" && dbSetup === "turso") {
80+
if (dbSetup === "docker") {
81+
await setupDockerCompose(config);
82+
} else if (database === "sqlite" && dbSetup === "turso") {
8083
await setupTurso(config);
8184
} else if (database === "sqlite" && dbSetup === "d1") {
8285
await setupCloudflareD1(config);

apps/cli/src/prompts/database-setup.ts

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -65,6 +65,20 @@ export async function getDBSetupChoice(
6565
},
6666
]
6767
: []),
68+
{
69+
value: "docker" as const,
70+
label: "Docker",
71+
hint: "Run locally with docker compose",
72+
},
73+
{ value: "none" as const, label: "None", hint: "Manual setup" },
74+
];
75+
} else if (databaseType === "mysql") {
76+
options = [
77+
{
78+
value: "docker" as const,
79+
label: "Docker",
80+
hint: "Run locally with docker compose",
81+
},
6882
{ value: "none" as const, label: "None", hint: "Manual setup" },
6983
];
7084
} else if (databaseType === "mongodb") {
@@ -74,6 +88,11 @@ export async function getDBSetupChoice(
7488
label: "MongoDB Atlas",
7589
hint: "The most effective way to deploy MongoDB",
7690
},
91+
{
92+
value: "docker" as const,
93+
label: "Docker",
94+
hint: "Run locally with docker compose",
95+
},
7796
{ value: "none" as const, label: "None", hint: "Manual setup" },
7897
];
7998
} else {

0 commit comments

Comments
 (0)