From 2ebf0d0a83e024ffb055314ef44fadf49e86a141 Mon Sep 17 00:00:00 2001 From: lila Date: Wed, 25 Mar 2026 18:56:04 +0100 Subject: [PATCH] infra: add Docker Compose setup for local development - Configure PostgreSQL 18 and Valkey 9.1 services - Create multi-stage Dockerfiles for API and Web apps - Set up pnpm workspace support in container builds - Configure hot reload via volume mounts for both services - Add healthchecks for service orchestration - Support dev/production stage targets (tsx watch vs compiled) --- .dockerignore | 11 +++++ .env.example | 6 ++- apps/api/Dockerfile | 42 +++++++++++++++++++ apps/web/Dockerfile | 19 +++++++++ docker-compose.yml | 76 +++++++++++++++++++++++++++++++++++ documentation/notes.md | 3 ++ documentation/roadmap.md | 2 +- documentation/spec.md | 4 +- mise.toml | 2 + packages/db/package.json | 3 ++ packages/shared/package.json | 8 +++- packages/shared/src/index.ts | 1 + packages/shared/tsconfig.json | 4 +- 13 files changed, 174 insertions(+), 7 deletions(-) create mode 100644 .dockerignore create mode 100644 apps/api/Dockerfile create mode 100644 apps/web/Dockerfile create mode 100644 docker-compose.yml create mode 100644 documentation/notes.md create mode 100644 mise.toml diff --git a/.dockerignore b/.dockerignore new file mode 100644 index 0000000..5574c21 --- /dev/null +++ b/.dockerignore @@ -0,0 +1,11 @@ +**/node_modules +**/dist +**/build +**/coverage + +.env +*.log +npm-debug.log* +.git +.gitignore +*.tsbuildinfo diff --git a/.env.example b/.env.example index daf3312..e5e7fb7 100644 --- a/.env.example +++ b/.env.example @@ -1 +1,5 @@ -DATABASE_URL=postgres://postgres:mypassword@localhost:5432/postgres +DATABASE_URL=postgres://postgres:mypassword@db-host:5432/postgres + +POSTGRES_USER=postgres +POSTGRES_PASSWORD=postgres +POSTGRES_DB=databasename diff --git a/apps/api/Dockerfile b/apps/api/Dockerfile new file mode 100644 index 0000000..9966c00 --- /dev/null +++ b/apps/api/Dockerfile @@ -0,0 +1,42 @@ +# 1. select image and install pnpm +FROM node:24-alpine AS base +RUN npm install -g pnpm + +# 2. dependencies +FROM base AS deps +WORKDIR /app +COPY pnpm-lock.yaml pnpm-workspace.yaml package.json ./ +COPY apps/api/package.json ./apps/api/ +COPY packages/shared/package.json ./packages/shared/ +COPY packages/db/package.json ./packages/db/ +RUN pnpm install --frozen-lockfile + +# 3. Development (only stage used) +FROM base AS dev +WORKDIR /app +COPY --from=deps /app/node_modules ./node_modules +COPY . ./ +EXPOSE 3000 +CMD ["pnpm", "--filter", "api", "dev"] + +# 4. build +FROM base AS builder +WORKDIR /app +COPY --from=deps /app/node_modules ./node_modules +COPY . . +RUN pnpm install +RUN pnpm --filter shared build +RUN pnpm --filter db build +RUN pnpm --filter api build + +# 5. run +FROM base AS runner +WORKDIR /app +COPY --from=deps /app/node_modules ./node_modules +COPY --from=deps /app/packages/shared/package.json /app/packages/shared/package.json +COPY --from=deps /app/packages/db/package.json /app/packages/db/package.json +COPY --from=builder /app/apps/api/dist ./dist +COPY --from=builder /app/packages/shared/dist /app/packages/shared/dist +COPY --from=builder /app/packages/db/dist /app/packages/db/dist +EXPOSE 3000 +CMD ["node", "dist/server.js"] diff --git a/apps/web/Dockerfile b/apps/web/Dockerfile new file mode 100644 index 0000000..e81da18 --- /dev/null +++ b/apps/web/Dockerfile @@ -0,0 +1,19 @@ +# 1. Base +FROM node:24-alpine AS base +RUN npm install -g pnpm + +# 2. Deps +FROM base AS deps +WORKDIR /app +COPY pnpm-lock.yaml pnpm-workspace.yaml package.json ./ +COPY apps/web/package.json ./apps/web/ +COPY packages/shared/package.json ./packages/shared/ +RUN pnpm install --frozen-lockfile + +# 3. Dev +FROM base AS dev +WORKDIR /app +COPY --from=deps /app/node_modules ./node_modules +COPY . ./ +EXPOSE 5173 +CMD ["pnpm", "--filter", "web", "dev", "--host"] diff --git a/docker-compose.yml b/docker-compose.yml new file mode 100644 index 0000000..074356d --- /dev/null +++ b/docker-compose.yml @@ -0,0 +1,76 @@ +services: + database: + container_name: glossa-database + image: postgres:18.3-alpine3.23 + env_file: + - .env + ports: + - "5432:5432" + volumes: + - glossa-db:/var/lib/postgresql/data + restart: unless-stopped + healthcheck: + test: ["CMD-SHELL", "pg_isready -U ${POSTGRES_USER} -d ${POSTGRES_DB}"] + interval: 5s + timeout: 5s + retries: 5 + + valkey: + container_name: glossa-valkey + image: valkey/valkey:9.1-alpine3.23 + ports: + - "6379:6379" + restart: unless-stopped + healthcheck: + test: ["CMD", "valkey-cli", "ping"] + interval: 5s + timeout: 3s + retries: 5 + + api: + container_name: glossa-api + build: + context: . + dockerfile: ./apps/api/Dockerfile + target: dev + env_file: + - .env + ports: + - "3000:3000" + volumes: + - ./apps/api:/app/apps/api # Hot reload API code + - ./packages/shared:/app/packages/shared # Hot reload shared + - /app/node_modules + restart: unless-stopped + healthcheck: + test: + ["CMD-SHELL", "wget -qO- http://localhost:3000/api/health || exit 1"] + interval: 5s + timeout: 3s + retries: 5 + depends_on: + database: + condition: service_healthy + valkey: + condition: service_healthy + + web: + container_name: glossa-web + build: + context: . + dockerfile: ./apps/web/Dockerfile + target: dev + ports: + - "5173:5173" + volumes: + - ./apps/web:/app/apps/web # Hot reload: local edits reflect immediately + - /app/node_modules # Protect container's node_modules from being overwritten + environment: + - VITE_API_URL=http://localhost:3000 + restart: unless-stopped + depends_on: + api: + condition: service_healthy + +volumes: + glossa-db: diff --git a/documentation/notes.md b/documentation/notes.md new file mode 100644 index 0000000..6cec6c8 --- /dev/null +++ b/documentation/notes.md @@ -0,0 +1,3 @@ +# notes + +- pinning dependencies in package.json files diff --git a/documentation/roadmap.md b/documentation/roadmap.md index 30d02ab..41e71a9 100644 --- a/documentation/roadmap.md +++ b/documentation/roadmap.md @@ -17,7 +17,7 @@ Each phase produces a working, deployable increment. Nothing is built speculativ - [x] Scaffold Vite + React app with TanStack Router (single root route) - [x] Configure Drizzle ORM + connection to local PostgreSQL - [x] Write first migration (empty — just validates the pipeline works) -- [ ] `docker-compose.yml` for local dev: `api`, `web`, `postgres`, `valkey` +- [x] `docker-compose.yml` for local dev: `api`, `web`, `postgres`, `valkey` - [ ] `.env.example` files for `apps/api` and `apps/web` - [ ] update decisions.md diff --git a/documentation/spec.md b/documentation/spec.md index 5fe4984..86dcf3a 100644 --- a/documentation/spec.md +++ b/documentation/spec.md @@ -24,9 +24,9 @@ A multiplayer English–Italian vocabulary trainer with a Duolingo-style quiz in | Styling | Tailwind CSS + shadcn/ui | | Backend | Node.js, Express, TypeScript | | Realtime | WebSockets (`ws` library) | -| Database | PostgreSQL 16 | +| Database | PostgreSQL 18 | | ORM | Drizzle ORM | -| Cache / Queue | Valkey 8 | +| Cache / Queue | Valkey 9 | | Auth | OpenAuth (Google + GitHub) | | Validation | Zod (shared schemas) | | Testing | Vitest, React Testing Library | diff --git a/mise.toml b/mise.toml new file mode 100644 index 0000000..8c1f295 --- /dev/null +++ b/mise.toml @@ -0,0 +1,2 @@ +[tools] +node = "24.14.0" diff --git a/packages/db/package.json b/packages/db/package.json index 5ecf86b..55c3b40 100644 --- a/packages/db/package.json +++ b/packages/db/package.json @@ -3,6 +3,9 @@ "version": "1.0.0", "private": true, "type": "module", + "scripts": { + "build": "tsc" + }, "dependencies": { "dotenv": "^17.3.1", "drizzle-orm": "^0.45.1", diff --git a/packages/shared/package.json b/packages/shared/package.json index b51dbbc..af89eb4 100644 --- a/packages/shared/package.json +++ b/packages/shared/package.json @@ -2,5 +2,11 @@ "name": "@glossa/shared", "version": "1.0.0", "private": true, - "type": "module" + "type": "module", + "scripts": { + "build": "tsc" + }, + "exports": { + ".": "./src/index.ts" + } } diff --git a/packages/shared/src/index.ts b/packages/shared/src/index.ts index e69de29..1bda49b 100644 --- a/packages/shared/src/index.ts +++ b/packages/shared/src/index.ts @@ -0,0 +1 @@ +export const placeholder = true; diff --git a/packages/shared/tsconfig.json b/packages/shared/tsconfig.json index 0e85f05..3ed0091 100644 --- a/packages/shared/tsconfig.json +++ b/packages/shared/tsconfig.json @@ -5,7 +5,7 @@ "moduleResolution": "NodeNext", "outDir": "./dist", "resolveJsonModule": true, - "types": ["vitest/globals"] + "types": ["vitest/globals"], }, - "include": ["src", "vitest.config.ts"] + "include": ["src", "vitest.config.ts"], }