diff --git a/.dockerignore b/.dockerignore new file mode 100644 index 0000000..9b49524 --- /dev/null +++ b/.dockerignore @@ -0,0 +1,15 @@ +node_modules +Dockerfile* +docker-compose* +.dockerignore +.git +.gitignore +README.md +LICENSE +.vscode +Makefile +helm-charts +.env +.editorconfig +.idea +coverage* \ No newline at end of file diff --git a/.gitignore b/.gitignore index 36e6ab1..6c52909 100644 --- a/.gitignore +++ b/.gitignore @@ -42,4 +42,7 @@ db/ # Dist folders dist-static -dist-server \ No newline at end of file +dist-server + +# Docker Compose environment file +.env.docker \ No newline at end of file diff --git a/Dockerfile.dev b/Dockerfile.dev new file mode 100644 index 0000000..b731d66 --- /dev/null +++ b/Dockerfile.dev @@ -0,0 +1,8 @@ +FROM oven/bun:1.3-alpine + +WORKDIR /app + +COPY package.json bun.lock ./ +RUN bun install --frozen-lockfile + +COPY . . \ No newline at end of file diff --git a/README.md b/README.md index f57d3ec..9c8ed19 100644 --- a/README.md +++ b/README.md @@ -45,10 +45,19 @@ Configure your Infisical project or create a `.env` file with required variables bun dev ``` -This command starts: -- PostgreSQL database (via Docker) -- Vite dev server with HMR -- Drizzle Studio for database management +This command starts the following services: +- **PostgreSQL**: Database service on port `5432`. +- **Migrator**: Automatically runs pending migrations. +- **Vite**: Development server with Hot Module Replacement at [localhost:5173](http://localhost:5173/). +- **Drizzle Studio**: Database GUI at [local.drizzle.studio](https://local.drizzle.studio) + +The Vite server and Drizzle Studio run inside Docker containers. To ensure the stack remains fast, a custom development image is used. We bind the current directory to a Docker volume, so code changes are immediately reflected in the container. + +**Note on dependencies**: Since `node_modules` are cached, you must rebuild the containers if you add or change packages. You can do this by running: + +```bash +bun dev --build +``` ## Scripts diff --git a/bun.lock b/bun.lock index b5c50aa..3b6dde0 100644 --- a/bun.lock +++ b/bun.lock @@ -57,7 +57,6 @@ "@types/react": "^19.2.7", "@types/react-dom": "^19.2.3", "@vitejs/plugin-react": "^5.1.2", - "concurrently": "^9.0.0", "react-router-dom": "^6.26.2", "tw-animate-css": "^1.4.0", "vite": "^7.3.0", @@ -632,8 +631,6 @@ "chai": ["chai@6.2.2", "", {}, "sha512-NUPRluOfOiTKBKvWPtSD4PhFvWCqOi0BGStNWs57X9js7XGTprSmFoz5F0tWhR4WPjNeR9jXqdC7/UpSJTnlRg=="], - "chalk": ["chalk@4.1.2", "", { "dependencies": { "ansi-styles": "^4.1.0", "supports-color": "^7.1.0" } }, "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA=="], - "cjs-module-lexer": ["cjs-module-lexer@1.4.3", "", {}, "sha512-9z8TZaGM1pfswYeXrUpzPrkx8UnWYdhJclsiYMm6x/w5+nN+8Tf/LnAgfLGQCm59qAOxU8WwHEq2vNwF6i4j+Q=="], "class-variance-authority": ["class-variance-authority@0.7.1", "", { "dependencies": { "clsx": "^2.1.1" } }, "sha512-Ka+9Trutv7G8M6WT6SeiRWz792K5qEqIGEGzXKhAE6xOWAY6pPH8U+9IY3oCMv6kqTmLsv7Xh/2w2RigkePMsg=="], @@ -648,8 +645,6 @@ "combined-stream": ["combined-stream@1.0.8", "", { "dependencies": { "delayed-stream": "~1.0.0" } }, "sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg=="], - "concurrently": ["concurrently@9.2.1", "", { "dependencies": { "chalk": "4.1.2", "rxjs": "7.8.2", "shell-quote": "1.8.3", "supports-color": "8.1.1", "tree-kill": "1.2.2", "yargs": "17.7.2" }, "bin": { "conc": "dist/bin/concurrently.js", "concurrently": "dist/bin/concurrently.js" } }, "sha512-fsfrO0MxV64Znoy8/l1vVIjjHa29SZyyqPgQBwhiDcaW8wJc2W3XWVOGx4M3oJBnv/zdUZIIp1gDeS98GzP8Ng=="], - "convert-source-map": ["convert-source-map@2.0.0", "", {}, "sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg=="], "cssstyle": ["cssstyle@4.6.0", "", { "dependencies": { "@asamuzakjp/css-color": "^3.2.0", "rrweb-cssom": "^0.8.0" } }, "sha512-2z+rWdzbbSZv6/rhtvzvqeZQHrBaqgogqt85sqFNbabZOuFbCVFb8kPeEtZjiKkbrm395irpNKiYeFeLiQnFPg=="], @@ -738,8 +733,6 @@ "graceful-fs": ["graceful-fs@4.2.11", "", {}, "sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ=="], - "has-flag": ["has-flag@4.0.0", "", {}, "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ=="], - "has-symbols": ["has-symbols@1.1.0", "", {}, "sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ=="], "has-tostringtag": ["has-tostringtag@1.0.2", "", { "dependencies": { "has-symbols": "^1.0.3" } }, "sha512-NqADB8VjPFLM2V0VvHUewwwsw0ZWBaIdgo+ieHtK3hasLz4qeCRjYcqfB6AQrBggRKppKF8L52/VqdVsO47Dlw=="], @@ -952,8 +945,6 @@ "rrweb-cssom": ["rrweb-cssom@0.7.1", "", {}, "sha512-TrEMa7JGdVm0UThDJSx7ddw5nVm3UJS9o9CCIZ72B1vSyEZoziDqBYP3XIoi/12lKrJR8rE3jeFHMok2F/Mnsg=="], - "rxjs": ["rxjs@7.8.2", "", { "dependencies": { "tslib": "^2.1.0" } }, "sha512-dhKf903U/PQZY6boNNtAGdWbG85WAbjT/1xYoZIC7FAY0yWapOBQVsVrDl58W86//e1VpMNBtRV4MaXfdMySFA=="], - "safe-stable-stringify": ["safe-stable-stringify@2.5.0", "", {}, "sha512-b3rppTKm9T+PsVCBEOUR46GWI7fdOs00VKZ1+9c1EWDaDMvjQc6tUwuFyIprgGgTcWoVHSKrU8H31ZHA2e0RHA=="], "safer-buffer": ["safer-buffer@2.1.2", "", {}, "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg=="], @@ -968,8 +959,6 @@ "shallowequal": ["shallowequal@1.1.0", "", {}, "sha512-y0m1JoUZSlPAjXVtPPW70aZWfIL/dSP7AFkRnniLCrK/8MDKog3TySTBmckD+RObVxH0v4Tox67+F14PdED2oQ=="], - "shell-quote": ["shell-quote@1.8.3", "", {}, "sha512-ObmnIF4hXNg1BqhnHmgbDETF8dLPCggZWBjkQfhZpbszZnYur5DUljTcCHii5LC3J5E0yeO/1LIMyH+UvHQgyw=="], - "siginfo": ["siginfo@2.0.0", "", {}, "sha512-ybx0WO1/8bSBLEWXZvEd7gMW3Sn3JFlW3TvX1nREbDLRNQNaeNN8WK0meBwPdAaOI7TtRRRJn/Es1zhrrCHu7g=="], "sonic-boom": ["sonic-boom@4.2.0", "", { "dependencies": { "atomic-sleep": "^1.0.0" } }, "sha512-INb7TM37/mAcsGmc9hyyI6+QR3rR1zVRu36B0NeGXKnOOLiZOfER5SA+N7X7k3yUYRzLWafduTDvJAfDswwEww=="], @@ -990,8 +979,6 @@ "strip-ansi": ["strip-ansi@6.0.1", "", { "dependencies": { "ansi-regex": "^5.0.1" } }, "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A=="], - "supports-color": ["supports-color@8.1.1", "", { "dependencies": { "has-flag": "^4.0.0" } }, "sha512-MpUEN2OodtUzxvKQl72cUF7RQ5EiHsGvSsVG0ia9c5RbWGL2CI4C7EpPS8UTBIplnlzZiNuV56w+FuNxy3ty2Q=="], - "symbol-tree": ["symbol-tree@3.2.4", "", {}, "sha512-9QNk5KwDF+Bvz+PyObkmSYjI5ksVUYtjW7AU22r2NKcfLJcXp96hkDWU3+XndOsUb+AQ9QhfzfCT2O+CNWT5Tw=="], "tailwind-merge": ["tailwind-merge@3.4.0", "", {}, "sha512-uSaO4gnW+b3Y2aWoWfFpX62vn2sR3skfhbjsEnaBI81WD1wBLlHZe5sWf0AqjksNdYTbGBEd0UasQMT3SNV15g=="], @@ -1014,8 +1001,6 @@ "tr46": ["tr46@5.1.1", "", { "dependencies": { "punycode": "^2.3.1" } }, "sha512-hdF5ZgjTqgAntKkklYw0R03MG2x/bSzTtkxmIRw/sTNV8YXsCJ1tfLAX23lhxhHJlEf3CRCOCGGWw3vI3GaSPw=="], - "tree-kill": ["tree-kill@1.2.2", "", { "bin": { "tree-kill": "cli.js" } }, "sha512-L0Orpi8qGpRG//Nd+H90vFB+3iHnue1zSSGmNOOCh1GLJ7rUKVwV2HvijphGQS2UmhUZewS9VgvxYIdgr+fG1A=="], - "tslib": ["tslib@2.8.1", "", {}, "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w=="], "tw-animate-css": ["tw-animate-css@1.4.0", "", {}, "sha512-7bziOlRqH0hJx80h/3mbicLW7o8qLsH5+RaLR2t+OHM3D0JlWGODQKQ4cxbK7WlvmUxpcj6Kgu6EKqjrGFe3QQ=="], @@ -1250,8 +1235,6 @@ "@types/pg-pool/@types/pg": ["@types/pg@8.15.6", "", { "dependencies": { "@types/node": "*", "pg-protocol": "*", "pg-types": "^2.2.0" } }, "sha512-NoaMtzhxOrubeL/7UZuNTrejB4MPAJ0RpxZqXQf2qXuVlTPuG6Y8p4u9dKRaue4yjmC7ZhzVO2/Yyyn25znrPQ=="], - "chalk/supports-color": ["supports-color@7.2.0", "", { "dependencies": { "has-flag": "^4.0.0" } }, "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw=="], - "cssstyle/rrweb-cssom": ["rrweb-cssom@0.8.0", "", {}, "sha512-guoltQEx+9aMf2gDZ0s62EcV8lsXR+0w8915TC3ITdn2YueuNjdAYh/levpU9nFaoChh9RUS5ZdQMrKfVEN9tw=="], "drizzle-kit/esbuild": ["esbuild@0.25.12", "", { "optionalDependencies": { "@esbuild/aix-ppc64": "0.25.12", "@esbuild/android-arm": "0.25.12", "@esbuild/android-arm64": "0.25.12", "@esbuild/android-x64": "0.25.12", "@esbuild/darwin-arm64": "0.25.12", "@esbuild/darwin-x64": "0.25.12", "@esbuild/freebsd-arm64": "0.25.12", "@esbuild/freebsd-x64": "0.25.12", "@esbuild/linux-arm": "0.25.12", "@esbuild/linux-arm64": "0.25.12", "@esbuild/linux-ia32": "0.25.12", "@esbuild/linux-loong64": "0.25.12", "@esbuild/linux-mips64el": "0.25.12", "@esbuild/linux-ppc64": "0.25.12", "@esbuild/linux-riscv64": "0.25.12", "@esbuild/linux-s390x": "0.25.12", "@esbuild/linux-x64": "0.25.12", "@esbuild/netbsd-arm64": "0.25.12", "@esbuild/netbsd-x64": "0.25.12", "@esbuild/openbsd-arm64": "0.25.12", "@esbuild/openbsd-x64": "0.25.12", "@esbuild/openharmony-arm64": "0.25.12", "@esbuild/sunos-x64": "0.25.12", "@esbuild/win32-arm64": "0.25.12", "@esbuild/win32-ia32": "0.25.12", "@esbuild/win32-x64": "0.25.12" }, "bin": { "esbuild": "bin/esbuild" } }, "sha512-bbPBYYrtZbkt6Os6FiTLCTFxvq4tt3JKall1vRwshA3fdVztsLAatFaZobhkBC8/BrPetoa0oksYoKXoG4ryJg=="], diff --git a/docker-compose.dev.yml b/docker-compose.dev.yml index e36ecd3..c451012 100644 --- a/docker-compose.dev.yml +++ b/docker-compose.dev.yml @@ -16,3 +16,55 @@ services: interval: 10s timeout: 5s retries: 5 + + # One-time task to run migrations + migrator: + build: + context: . + dockerfile: Dockerfile.dev + command: bun drizzle-kit migrate + depends_on: + postgres: + condition: service_healthy + env_file: .env.docker + environment: + - DATABASE_URL=postgresql://${POSTGRES_USER:-postgres}:${POSTGRES_PASSWORD:-postgres}@postgres:${POSTGRES_PORT:-5432}/${POSTGRES_DB:-postgres} + + vite: + build: + context: . + dockerfile: Dockerfile.dev + env_file: .env.docker + environment: + - WATCHPACK_POLLING=true + - DATABASE_URL=postgresql://${POSTGRES_USER:-postgres}:${POSTGRES_PASSWORD:-postgres}@postgres:${POSTGRES_PORT:-5432}/${POSTGRES_DB:-postgres} + command: bun run dev:app --host=0.0.0.0 + ports: + - "5173:5173" + volumes: + - .:/app # Edits on host are reflected in container + - /app/node_modules + depends_on: + postgres: + condition: service_healthy + migrator: + condition: service_completed_successfully + + studio: + build: + context: . + dockerfile: Dockerfile.dev + env_file: .env.docker + environment: + - DATABASE_URL=postgresql://${POSTGRES_USER:-postgres}:${POSTGRES_PASSWORD:-postgres}@postgres:${POSTGRES_PORT:-5432}/${POSTGRES_DB:-postgres} + command: bun run db:studio --port 4983 --host 0.0.0.0 + ports: + - "4983:4983" + volumes: + - .:/app # Edits on host are reflected in container + - /app/node_modules + depends_on: + postgres: + condition: service_healthy + migrator: + condition: service_completed_successfully diff --git a/package.json b/package.json index 4089b3a..018a8ee 100644 --- a/package.json +++ b/package.json @@ -4,8 +4,8 @@ "type": "module", "private": true, "scripts": { - "dev": "infisical run -- bun -b concurrently --names \"DOCKER,APP\" -c \"blue,magenta\" \"bun run db:start\" \"bun run db:wait && bun run dev:app\"", - "dev:app": "concurrently --names \"VITE,STUDIO\" -c \"cyan,yellow\" \"vite dev\" \"bun run db:studio\"", + "dev": "infisical export --format=dotenv > .env.docker && docker compose -f docker-compose.dev.yml up", + "dev:app": "vite dev", "db:studio": "drizzle-kit studio", "db:start": "docker compose -f docker-compose.dev.yml up", "db:wait": "bash -c 'until docker compose -f docker-compose.dev.yml exec -T postgres pg_isready -U ${POSTGRES_USER:-postgres} > /dev/null 2>&1; do sleep 1; done'", @@ -36,7 +36,6 @@ "@types/react": "^19.2.7", "@types/react-dom": "^19.2.3", "@vitejs/plugin-react": "^5.1.2", - "concurrently": "^9.0.0", "react-router-dom": "^6.26.2", "tw-animate-css": "^1.4.0", "vite": "^7.3.0", @@ -90,4 +89,4 @@ "tailwindcss": "^4.1.18", "zod": "^4.3.5" } -} +} \ No newline at end of file diff --git a/scripts/dev.ts b/scripts/dev.ts deleted file mode 100644 index caba343..0000000 --- a/scripts/dev.ts +++ /dev/null @@ -1,108 +0,0 @@ -/** - * Development orchestrator script. - * - * Starts the full dev environment with dynamic port allocation, allowing - * multiple instances to run simultaneously without port conflicts. - * - * Flow: - * 1. Find available ports for PostgreSQL, Vite, and Drizzle Studio - * 2. Start PostgreSQL in Docker with a unique project name - * 3. Wait for the database to be ready - * 4. Run Vite dev server and Drizzle Studio concurrently - * 5. Clean up all processes and containers on exit (Ctrl+C) - */ - -import { $ } from "bun"; -import concurrently from "concurrently"; - -const COMPOSE_FILE = "docker-compose.dev.yml"; - -// --- Port finding using Bun.listen --- -function findPort(start: number, max: number): number { - for (let port = start; port <= max; port++) { - try { - const server = Bun.listen({ - hostname: "127.0.0.1", - port, - socket: { data() {} }, - }); - server.stop(); - return port; - } catch { - // Port in use, try next - } - } - throw new Error(`No available port found in range ${start}-${max}`); -} - -// --- Main --- -const dbPort = findPort(5432, 5500); -const vitePort = findPort(5173, 5200); -const studioPort = findPort(4983, 5100); - -console.log( - `[dev] Ports - DB: ${dbPort}, Vite: ${vitePort}, Studio: ${studioPort}`, -); - -// Use DB port to create unique project name (so multiple instances don't clash) -const projectName = `dev-${dbPort}`; - -// Override DATABASE_URL with new port -const originalDbUrl = process.env.DATABASE_URL; -if (!originalDbUrl) { - throw new Error("DATABASE_URL environment variable is required"); -} -const dbUrl = new URL(originalDbUrl); -dbUrl.port = String(dbPort); -process.env.DATABASE_URL = dbUrl.toString(); - -// Start docker with unique project name -const docker = Bun.spawn( - ["docker", "compose", "-p", projectName, "-f", COMPOSE_FILE, "up"], - { - stdout: "inherit", - stderr: "inherit", - env: { ...process.env, POSTGRES_PORT: String(dbPort) }, - }, -); - -// Cleanup handler -const cleanup = async () => { - console.log("\n[dev] Shutting down..."); - docker.kill(); - await $`docker compose -p ${projectName} -f ${COMPOSE_FILE} down`.quiet(); - process.exit(0); -}; -process.on("SIGINT", cleanup); -process.on("SIGTERM", cleanup); - -// Wait for DB -console.log("[dev] Waiting for database..."); -while (true) { - const r = - await $`docker compose -p ${projectName} -f ${COMPOSE_FILE} exec -T postgres pg_isready -U postgres` - .quiet() - .nothrow(); - if (r.exitCode === 0) break; - await Bun.sleep(1000); -} -console.log("[dev] Database ready!"); - -// Run dev servers -const { result } = concurrently( - [ - { - command: `vite dev --port ${vitePort}`, - name: "VITE", - prefixColor: "cyan", - }, - { - command: `drizzle-kit studio --port ${studioPort}`, - name: "STUDIO", - prefixColor: "yellow", - }, - ], - { killOthersOn: ["failure"] }, -); - -await result;