From c42b8e2904b43845c695c5d2056c74f13ebc92d7 Mon Sep 17 00:00:00 2001 From: Syahdan Date: Mon, 25 May 2026 13:40:09 +0700 Subject: [PATCH] ci: docker compose for easy access --- .dockerignore | 17 +++++++++ .env.example | 15 ++++++++ README.md | 21 ++++++++++- apps/server/Dockerfile | 33 ++++++++++++++++ apps/web/Dockerfile | 21 +++++++++++ apps/web/nginx.conf | 11 ++++++ docker-compose.yml | 64 ++++++++++++++++++++++++++++++++ docs/run-instructions.md | 21 ++++++++++- packages/db/package.json | 1 + packages/db/src/seed-if-empty.ts | 34 +++++++++++++++++ packages/db/src/seed.ts | 24 ++++++------ 11 files changed, 249 insertions(+), 13 deletions(-) create mode 100644 .dockerignore create mode 100644 .env.example create mode 100644 apps/server/Dockerfile create mode 100644 apps/web/Dockerfile create mode 100644 apps/web/nginx.conf create mode 100644 docker-compose.yml create mode 100644 packages/db/src/seed-if-empty.ts diff --git a/.dockerignore b/.dockerignore new file mode 100644 index 0000000..2e71340 --- /dev/null +++ b/.dockerignore @@ -0,0 +1,17 @@ +.git +.jj +node_modules +**/node_modules +.turbo +.cache +coverage +**/dist +**/build +**/*.tsbuildinfo +.env* +**/.env* +!.env.example +!**/.env.example +logs +*.log +docs diff --git a/.env.example b/.env.example new file mode 100644 index 0000000..2e78d98 --- /dev/null +++ b/.env.example @@ -0,0 +1,15 @@ +POSTGRES_DB=minmon +POSTGRES_USER=postgres +POSTGRES_PASSWORD=postgres +POSTGRES_PORT=5432 + +DATABASE_URL=postgresql://postgres:postgres@postgres:5432/minmon + +BETTER_AUTH_SECRET=minmon-compose-secret-change-me-123456 +BETTER_AUTH_URL=http://localhost:3000 +CORS_ORIGIN=http://localhost:3001 +NODE_ENV=development + +SERVER_PORT=3000 +WEB_PORT=3001 +VITE_SERVER_URL=http://localhost:3000 diff --git a/README.md b/README.md index e06f18a..228bcd9 100644 --- a/README.md +++ b/README.md @@ -159,6 +159,25 @@ bun run dev - Web: `http://localhost:5173` - API: `http://localhost:3000` +## Docker Compose Deployment + +Run the full stack with PostgreSQL, database initialization, API, and web UI: + +```bash +cp .env.example .env +docker compose up -d +``` + +Default endpoints: + +- Web: `http://localhost:3001` +- API: `http://localhost:3000` + +On first startup, the database initializer applies the schema and seeds the sample admin only when the users table is empty. + +Docker Compose reads the root `.env` file automatically. Edit it before startup if you need different ports, public URLs, credentials, or secrets. + + ## Project Structure ```text @@ -186,5 +205,5 @@ See: ## Notes - DNS checking is limited to A record / IPv4 as required. -- This is not a deployment platform and does not include SSH, Docker orchestration, or reverse proxy automation. +- Docker Compose is available for running the app stack; the app itself does not include SSH, reverse proxy, or host orchestration automation. - Full workspace typecheck is still blocked by a pre-existing desktop typing issue for `three`, but the web app and server app are verified. diff --git a/apps/server/Dockerfile b/apps/server/Dockerfile new file mode 100644 index 0000000..0c49b3b --- /dev/null +++ b/apps/server/Dockerfile @@ -0,0 +1,33 @@ +# syntax=docker/dockerfile:1 + +FROM oven/bun:1.3.12 AS base +WORKDIR /app +ENV CI=1 + +COPY package.json bun.lock turbo.json tsconfig.json bts.jsonc ./ +COPY apps ./apps +COPY packages ./packages + +FROM base AS deps +RUN bun install --frozen-lockfile + +FROM deps AS tools +ENV NODE_ENV=development +RUN touch apps/server/.env + +FROM deps AS build +ENV NODE_ENV=production +RUN bun run --filter server build + +FROM base AS prod-deps +RUN bun install --frozen-lockfile --production + +FROM oven/bun:1.3.12 AS runtime +WORKDIR /app +ENV NODE_ENV=production + +COPY --from=prod-deps /app/node_modules ./node_modules +COPY --from=build /app/apps/server/dist ./apps/server/dist + +EXPOSE 3000 +CMD ["bun", "apps/server/dist/index.mjs"] diff --git a/apps/web/Dockerfile b/apps/web/Dockerfile new file mode 100644 index 0000000..f3e637b --- /dev/null +++ b/apps/web/Dockerfile @@ -0,0 +1,21 @@ +# syntax=docker/dockerfile:1 + +FROM oven/bun:1.3.12 AS build +WORKDIR /app +ENV CI=1 + +ARG VITE_SERVER_URL=http://localhost:3000 +ENV VITE_SERVER_URL=${VITE_SERVER_URL} + +COPY package.json bun.lock turbo.json tsconfig.json bts.jsonc ./ +COPY apps ./apps +COPY packages ./packages + +RUN bun install --frozen-lockfile +RUN bun run --filter web build + +FROM nginx:1.27-alpine AS runtime +COPY apps/web/nginx.conf /etc/nginx/conf.d/default.conf +COPY --from=build /app/apps/web/dist /usr/share/nginx/html + +EXPOSE 80 diff --git a/apps/web/nginx.conf b/apps/web/nginx.conf new file mode 100644 index 0000000..79fd959 --- /dev/null +++ b/apps/web/nginx.conf @@ -0,0 +1,11 @@ +server { + listen 80; + server_name _; + + root /usr/share/nginx/html; + index index.html; + + location / { + try_files $uri $uri/ /index.html; + } +} diff --git a/docker-compose.yml b/docker-compose.yml new file mode 100644 index 0000000..c3139d9 --- /dev/null +++ b/docker-compose.yml @@ -0,0 +1,64 @@ +name: minmon + +services: + postgres: + image: postgres:17-alpine + environment: + POSTGRES_DB: ${POSTGRES_DB:?POSTGRES_DB must be set in root .env} + POSTGRES_USER: ${POSTGRES_USER:?POSTGRES_USER must be set in root .env} + POSTGRES_PASSWORD: ${POSTGRES_PASSWORD:?POSTGRES_PASSWORD must be set in root .env} + ports: + - "${POSTGRES_PORT:?POSTGRES_PORT must be set in root .env}:5432" + volumes: + - postgres_data:/var/lib/postgresql/data + healthcheck: + test: ["CMD-SHELL", "pg_isready -U $${POSTGRES_USER} -d $${POSTGRES_DB}"] + interval: 10s + timeout: 5s + retries: 5 + start_period: 10s + restart: unless-stopped + + db-init: + build: + context: . + dockerfile: apps/server/Dockerfile + target: tools + environment: + <<: *server-environment + command: ["sh", "-c", "bun run db:push && bun run --filter @minmon/db db:seed:if-empty"] + depends_on: + postgres: + condition: service_healthy + restart: "no" + + server: + build: + context: . + dockerfile: apps/server/Dockerfile + environment: + <<: *server-environment + ports: + - "${SERVER_PORT:?SERVER_PORT must be set in root .env}:3000" + depends_on: + postgres: + condition: service_healthy + db-init: + condition: service_completed_successfully + restart: unless-stopped + + web: + build: + context: . + dockerfile: apps/web/Dockerfile + args: + VITE_SERVER_URL: ${VITE_SERVER_URL:?VITE_SERVER_URL must be set in root .env} + ports: + - "${WEB_PORT:?WEB_PORT must be set in root .env}:80" + depends_on: + server: + condition: service_started + restart: unless-stopped + +volumes: + postgres_data: diff --git a/docs/run-instructions.md b/docs/run-instructions.md index d59428e..ddbd54a 100644 --- a/docs/run-instructions.md +++ b/docs/run-instructions.md @@ -114,6 +114,25 @@ bun run dev - Web: `http://localhost:5173` - API: `http://localhost:3000` +## Docker Compose Deployment + +Run the deployable stack with one command: + +```bash +cp .env.example .env +docker compose up -d +``` + +Default Docker endpoints: + +- Web: `http://localhost:3001` +- API: `http://localhost:3000` + +The Compose stack starts PostgreSQL, applies the schema, and seeds the sample admin only if the users table is empty. + +Docker Compose reads the root `.env` file automatically. Edit it before startup if you need different ports, public URLs, credentials, or secrets. + + ## Current Behavior Notes ### List Ordering @@ -157,4 +176,4 @@ bun x tsc --noEmit -p packages/api/tsconfig.json - Full workspace typecheck may still fail because of a pre-existing `apps/desktop` typing issue for `three`. - The verified implementation path is the web app + server app + database push + database seed. - DNS checking is limited to A record / IPv4. -- This project is not a deployment platform and does not include SSH, Docker orchestration, or reverse proxy automation. +- Docker Compose is available for running the app stack; the app itself does not include SSH, reverse proxy, or host orchestration automation. diff --git a/packages/db/package.json b/packages/db/package.json index 2b29b35..6e42483 100644 --- a/packages/db/package.json +++ b/packages/db/package.json @@ -15,6 +15,7 @@ "db:studio": "drizzle-kit studio", "db:migrate": "drizzle-kit migrate", "db:seed": "bun --env-file=../../apps/server/.env run ./src/seed.ts", + "db:seed:if-empty": "bun --env-file=../../apps/server/.env run ./src/seed-if-empty.ts", "db:start": "docker compose up -d", "db:watch": "docker compose up", "db:stop": "docker compose stop", diff --git a/packages/db/src/seed-if-empty.ts b/packages/db/src/seed-if-empty.ts new file mode 100644 index 0000000..ee6306e --- /dev/null +++ b/packages/db/src/seed-if-empty.ts @@ -0,0 +1,34 @@ +import dotenv from "dotenv"; + +dotenv.config({ + path: new URL("../../../apps/server/.env", import.meta.url).pathname, +}); + +import { db } from "./index"; +import { seed } from "./seed"; + +async function seedIfEmpty() { + const existingUser = await db.query.user.findFirst({ + columns: { + id: true, + }, + }); + + if (existingUser) { + console.info("Database already has users; skipping sample seed."); + return; + } + + await seed(); + console.info("Seeded auth admin and monitoring dashboard sample data."); + console.info("Admin login: admin@minmon.local / admin12345"); +} + +seedIfEmpty() + .then(() => { + process.exit(0); + }) + .catch((error) => { + console.error("Failed to initialize seed data.", error); + process.exit(1); + }); diff --git a/packages/db/src/seed.ts b/packages/db/src/seed.ts index d7aa6d8..86e4063 100644 --- a/packages/db/src/seed.ts +++ b/packages/db/src/seed.ts @@ -261,18 +261,20 @@ async function seedMonitoringData() { ); } -async function seed() { +export async function seed() { await seedAuthAdmin(); await seedMonitoringData(); } -seed() - .then(() => { - console.info("Seeded auth admin and monitoring dashboard sample data."); - console.info("Admin login: admin@minmon.local / admin12345"); - process.exit(0); - }) - .catch((error) => { - console.error("Failed to seed dashboard sample data.", error); - process.exit(1); - }); +if (import.meta.main) { + seed() + .then(() => { + console.info("Seeded auth admin and monitoring dashboard sample data."); + console.info("Admin login: admin@minmon.local / admin12345"); + process.exit(0); + }) + .catch((error) => { + console.error("Failed to seed dashboard sample data.", error); + process.exit(1); + }); +}