From 553fb51f792480ded1baaa6a6a9943ad460fc4b0 Mon Sep 17 00:00:00 2001 From: Mateo Galic Date: Sun, 1 Mar 2026 16:37:54 +0100 Subject: [PATCH 1/2] add docker --- .claude/commands/code-review.md | 1 - .dockerignore | 12 +++++++ Dockerfile | 36 ++++++++++++++++++++ docker-compose.yml | 58 +++++++++++++++++++++++++++++++++ package.json | 3 +- src/app.ts | 7 ++++ src/config/env.ts | 6 +++- 7 files changed, 120 insertions(+), 3 deletions(-) delete mode 100644 .claude/commands/code-review.md create mode 100644 .dockerignore create mode 100644 Dockerfile create mode 100644 docker-compose.yml diff --git a/.claude/commands/code-review.md b/.claude/commands/code-review.md deleted file mode 100644 index 7032d06..0000000 --- a/.claude/commands/code-review.md +++ /dev/null @@ -1 +0,0 @@ -Run code review on current git changes diff --git a/.dockerignore b/.dockerignore new file mode 100644 index 0000000..9bffcd2 --- /dev/null +++ b/.dockerignore @@ -0,0 +1,12 @@ +node_modules +dist +coverage +logs +.git +.env.local +.env.test +*.test.ts +jest.config.js +eslint.config.mjs +start.sh +.claude diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 0000000..9389746 --- /dev/null +++ b/Dockerfile @@ -0,0 +1,36 @@ +# Stage 1 — build +FROM node:22-alpine AS builder + +RUN apk add --no-cache python3 make g++ + +WORKDIR /app + +COPY package.json yarn.lock ./ +RUN yarn install --frozen-lockfile + +COPY tsconfig.json ./ +COPY src ./src +RUN yarn build + +# Stage 2 — production +FROM node:22-alpine + +RUN apk add --no-cache python3 make g++ + +WORKDIR /app + +COPY package.json yarn.lock ./ +RUN yarn install --frozen-lockfile --production && apk del python3 make g++ + +RUN addgroup -S appgroup && adduser -S appuser -G appgroup + +COPY --from=builder /app/dist ./dist +COPY migrations ./migrations + +RUN mkdir -p logs && chown -R appuser:appgroup /app + +USER appuser + +EXPOSE 4000 + +CMD ["node", "dist/server.js"] diff --git a/docker-compose.yml b/docker-compose.yml new file mode 100644 index 0000000..328da2d --- /dev/null +++ b/docker-compose.yml @@ -0,0 +1,58 @@ +services: + db: + image: pgvector/pgvector:pg17 + env_file: .env + environment: + POSTGRES_USER: ${POSTGRES_USER} + POSTGRES_PASSWORD: ${POSTGRES_PASSWORD} + POSTGRES_DB: ${POSTGRES_DB} + volumes: + - pgdata:/var/lib/postgresql/data + networks: + - app-network + healthcheck: + test: ["CMD-SHELL", "pg_isready -U ${POSTGRES_USER} -d ${POSTGRES_DB}"] + interval: 5s + timeout: 5s + retries: 5 + + migrate: + build: . + env_file: .env + environment: + POSTGRES_HOST: db + command: ["node", "dist/database/migrations.js"] + networks: + - app-network + depends_on: + db: + condition: service_healthy + restart: "no" + + app: + build: . + env_file: .env + environment: + POSTGRES_HOST: db + NODE_ENV: production + ports: + - "4000:4000" + networks: + - app-network + depends_on: + migrate: + condition: service_completed_successfully + restart: unless-stopped + healthcheck: + test: ["CMD", "wget", "--no-verbose", "--tries=1", "--spider", "http://localhost:4000/api/v1/health"] + interval: 30s + timeout: 10s + retries: 3 + start_period: 10s + +volumes: + pgdata: + +networks: + app-network: + driver: bridge diff --git a/package.json b/package.json index 9a9e048..cdba4b4 100644 --- a/package.json +++ b/package.json @@ -20,7 +20,8 @@ }, "tsup": { "entry": [ - "src/server.ts" + "src/server.ts", + "src/database/migrations.ts" ], "sourcemap": true }, diff --git a/src/app.ts b/src/app.ts index 3624c54..e1e93ed 100644 --- a/src/app.ts +++ b/src/app.ts @@ -24,12 +24,19 @@ class App { }); this.initializeMiddlewares(); + this.initializeHealthCheck(); this.initializeControllers(httpControllers); this.initializeSocketHandlers(socketControllers); this.app.use(errorMiddleware); } + private initializeHealthCheck() { + this.app.get("/api/v1/health", (_req, res) => { + res.status(200).json({ status: "ok" }); + }); + } + private initializeMiddlewares() { // Apply raw body parsing for Stripe webhook endpoint before JSON parsing this.app.use("/api/v1/payments/orders", express.raw({ type: "application/json" })); diff --git a/src/config/env.ts b/src/config/env.ts index 37499b1..fc6d205 100644 --- a/src/config/env.ts +++ b/src/config/env.ts @@ -1,7 +1,11 @@ import { config } from "dotenv"; import { cleanEnv, port, str, url } from "envalid"; -config({ path: process.env.NODE_ENV === "test" ? ".env.test" : ".env.local" }); +if (process.env.NODE_ENV === "test") { + config({ path: ".env.test" }); +} else if (process.env.NODE_ENV !== "production") { + config({ path: ".env.local" }); +} export const env = cleanEnv(process.env, { POSTGRES_USER: str(), From 0980e8bbd4e53848f0fabf533520a3675c803520 Mon Sep 17 00:00:00 2001 From: Mateo Galic Date: Thu, 5 Mar 2026 09:49:42 +0100 Subject: [PATCH 2/2] update roles --- src/__tests__/constants.ts | 7 +++++++ src/__tests__/globalSetup.ts | 9 ++------- 2 files changed, 9 insertions(+), 7 deletions(-) diff --git a/src/__tests__/constants.ts b/src/__tests__/constants.ts index 8e311af..7041919 100644 --- a/src/__tests__/constants.ts +++ b/src/__tests__/constants.ts @@ -1,5 +1,12 @@ +import { RoleName } from "../roles/roles.validation"; + export const TEST_ADMIN_USER = { username: "admin", email: "admin@example.com", password: "password" }; + +export const TEST_ROLES = new Map([ + [RoleName.ADMIN, "Admin role"], + [RoleName.USER, "User role"] +]); diff --git a/src/__tests__/globalSetup.ts b/src/__tests__/globalSetup.ts index fde12af..5b52406 100644 --- a/src/__tests__/globalSetup.ts +++ b/src/__tests__/globalSetup.ts @@ -3,12 +3,7 @@ import { Client } from "pg"; import { migrate } from "../database/setup"; import bcrypt from "bcrypt"; import { RoleName } from "../roles/roles.validation"; -import { TEST_ADMIN_USER } from "./constants"; - -const DEFAULT_ROLES = new Map([ - [RoleName.ADMIN, "Admin role"], - [RoleName.USER, "User role"] -]); +import { TEST_ROLES, TEST_ADMIN_USER } from "./constants"; export default async function globalSetup() { // Start the postgres container once for all tests @@ -23,7 +18,7 @@ export default async function globalSetup() { await migrate(client); // Insert default roles - for (const [name, description] of DEFAULT_ROLES) { + for (const [name, description] of TEST_ROLES) { await client.query(`INSERT INTO roles (name, description) VALUES ($1, $2)`, [ name, description